OOを勉強していた若い同僚が、なぜすべてのオブジェクトが参照で渡されるのかと尋ねました。これは、プリミティブ型または構造体の反対です。これは、JavaやC#などの言語の一般的な特性です。
彼の良い答えが見つかりませんでした。
この設計決定の動機は何ですか?これらの言語の開発者は、毎回ポインターとtypedefを作成することにうんざりしていましたか?
OOを勉強していた若い同僚が、なぜすべてのオブジェクトが参照で渡されるのかと尋ねました。これは、プリミティブ型または構造体の反対です。これは、JavaやC#などの言語の一般的な特性です。
彼の良い答えが見つかりませんでした。
この設計決定の動機は何ですか?これらの言語の開発者は、毎回ポインターとtypedefを作成することにうんざりしていましたか?
回答:
簡単な答え:
どこかに渡されたすべてのオブジェクトのディープコピーを再作成および実行する際のメモリ消費
とCPU時間を最小限に抑えます
。
C ++には、2つの主なオプションがあります。値による戻りまたはポインターによる戻りです。最初のものを見てみましょう:
MyClass getNewObject() {
MyClass newObj;
return newObj;
}
コンパイラが戻り値の最適化を使用するほど賢くないと仮定すると、ここで何が起こるかは次のとおりです。
オブジェクトのコピーを無意味に作成しました。これは処理時間の無駄です。
代わりに、ポインターによるリターンを見てみましょう。
MyClass* getNewObject() {
MyClass newObj = new MyClass();
return newObj;
}
冗長なコピーを削除しましたが、別の問題を導入しました。自動的に破棄されないヒープ上にオブジェクトを作成しました。自分で対処する必要があります。
MyClass someObj = getNewObject();
delete someObj;
この方法で割り当てられたオブジェクトを削除する責任者を知ることは、コメントまたは慣例によってのみ伝えることができるものです。メモリリークが発生しやすくなります。
これらの2つの問題を解決するための多くの回避策が提案されています-戻り値の最適化(コンパイラは、値による戻りで冗長コピーを作成しないほどスマートである)、メソッドへの参照を渡す(関数が新しいオブジェクトを作成するのではなく、既存のオブジェクト)、スマートポインター(所有権の問題は議論の余地がないように)。
Java / C#の作成者は、特に言語がネイティブにオブジェクトをサポートしている場合は、常に参照によってオブジェクトを返すことがより良いソリューションであることに気付きました。ガベージコレクションなど、言語が持つ他の多くの機能と結びついています。
他の多くの答えには良い情報があります。クローン作成に関して、部分的にしか対処されていない重要なポイントを1つ追加します。
参照の使用は賢明です。コピーは危険です。
他の人が言ったように、Javaには自然な「クローン」はありません。 これは単なる欠落機能ではありません。オブジェクト内のすべてのプロパティを(浅くも深くも)自由自在にコピーしたい ことは決してありません。そのプロパティがデータベース接続であった場合はどうなりますか?人間のクローンを作成できる以上に、データベース接続を「クローン」することはできません。 初期化には理由があります。
どのように深いあなたはない-ディープコピーは、独自の問題があり、実際に行きますか?静的なもの(Class
オブジェクトを含む)はコピーできません。
したがって、自然なクローンがない同じ理由で、コピーとして渡されるオブジェクトは狂気を生み出します。DB接続を「クローン」できたとしても、それがどのように閉じられていることを確認しますか?
*コメントを参照-この「決して」ステートメントは、すべてのプロパティを複製する自動クローンを意味します。Javaはそれを提供しませんでした。おそらく、ここに挙げた理由から、言語のユーザーとして独自に作成することはお勧めできません。一時的でないフィールドのみのクローンを作成することは出発点になりますが、それでもtransient
適切な場所の定義については熱心に取り組む必要があります。
clone
も問題ありません。「willy-nilly」とは、意図せずに、考えずにすべてのプロパティをコピーすることを意味します。Java言語の設計者は、ユーザーが作成したの実装を要求することにより、この意図を強制しましたclone
。
オブジェクトは常にJavaで参照されます。彼らは自分自身の周りに渡されることはありません。
1つの利点は、これにより言語が簡素化されることです。C ++オブジェクトは値または参照として表すことができるため、2つの異なる演算子を使用してメンバーにアクセスする必要が生じます: .
および->
。(これを統合できない理由があります。たとえば、スマートポインターは参照である値であり、それらを区別する必要があります.
。)Javaのみが必要です。
もう1つの理由は、多態性は値ではなく参照によって行われる必要があることです。値で扱われるオブジェクトはそこにあり、固定された型を持っています。C ++でこれを台無しにすることは可能です。
また、Javaはデフォルトの割り当て/コピー/その他を切り替えることができます。C ++では多かれ少なかれコピーですが、Javaではコピー.clone()
が必要な場合に備えて、単純なポインターの割り当て/コピー/何でもです。
const
、その値は他のオブジェクトを指すように変更できます。参照はオブジェクトの別の名前であり、NULLにすることはできず、再配置することもできません。通常、ポインターの単純な使用によって実装されますが、それは実装の詳細です。
素早い回答
Javaおよび同様の言語の設計者は、「すべてがオブジェクトである」という概念を適用したいと考えていました。また、データを参照として渡すのは非常に迅速であり、多くのメモリを消費しません。
追加の拡張退屈なコメント
また、これらの言語はオブジェクト参照(Java、Delphi、C#、VB.NET、Vala、Scala、PHP)を使用しますが、実際には、オブジェクト参照は偽装されたオブジェクトへのポインターです。null値、メモリ割り当て、オブジェクトのデータ全体をコピーしない参照のコピー、それらはすべてオブジェクトポインターであり、プレーンオブジェクトではありません!!!
Object Pascal(Delphiではない)、c ++(Javaではなく、C#ではない)では、オブジェクトは静的に割り当てられた変数として、また動的に割り当てられた変数とともに、ポインター(「シュガーシンタックス」)。各ケースは特定の構文を使用しますが、Javaの「および友人」のように混乱する方法はありません。これらの言語では、オブジェクトを値または参照として渡すことができます。
プログラマーは、ポインター構文がいつ必要で、いつ必要ではないかを知っていますが、Javaや同様の言語では、これは混乱を招きます。
Javaが存在するか主流になる前に、多くのプログラマーは、ポインターなしで、必要に応じて値渡しまたは参照渡しのC ++でオブジェクト指向を学びました。学習アプリからビジネスアプリに切り替えると、一般的にオブジェクトポインターを使用します。QTライブラリはその良い例です。
Javaを学んだとき、私はすべてがオブジェクトの概念であることに従おうとしましたが、コーディングで混乱しました。最終的に、「OK、これは静的に割り当てられたオブジェクトの構文を持つポインターで動的に割り当てられたオブジェクトです」と言ったので、再度コーディングするのに苦労しませんでした。
JavaとC#は、低レベルのメモリをユーザーから制御します。作成するオブジェクトが存在する「ヒープ」は、独自の生活を送ります。たとえば、ガベージコレクターは、必要に応じてオブジェクトを取得します。
プログラムとその「ヒープ」の間に別の間接層があるため、値とポインタ(C ++など)でオブジェクトを参照する2つの方法は区別できなくなります。常にオブジェクトを「ポインタ」で参照します。ヒープ内のどこかに。そのため、このような設計アプローチでは、参照渡しが割り当てのデフォルトのセマンティクスになります。Java、C#、Rubyなど。
上記は命令型言語のみに関係します。言語では、メモリの制御が実行時に渡され、上述したが、言語設計も言う、「ちょっと、実際に、そこにあるメモリは、そこにあるオブジェクトは、それらがないメモリを占有」。関数型言語は、定義から「メモリ」の概念を除外することにより、さらに抽象化します。そのため、低レベルのメモリを制御していないすべての言語に参照渡しが必ずしも適用されるわけではありません。
いくつかの理由が考えられます。
プリミティブ型のコピーは簡単で、通常は1つの機械語命令に変換されます。
オブジェクトのコピーは簡単ではありません。オブジェクトには、それ自体がオブジェクトであるメンバーを含めることができます。オブジェクトのコピーは、CPU時間とメモリを消費します。コンテキストに応じて、オブジェクトをコピーする方法は複数あります。
参照によるオブジェクトの受け渡しは安価であり、オブジェクトの複数のクライアント間でオブジェクト情報を共有/更新する場合にも便利です。
複雑なデータ構造(特に再帰的なデータ構造)にはポインターが必要です。オブジェクトを参照渡しすることは、ポインターを渡すより安全な方法です。
Javaはより優れたC ++として設計され、C#はより優れたJavaとして設計されたため、これらの言語の開発者は、オブジェクトが値型である根本的に壊れたC ++オブジェクトモデルにうんざりしていました。
オブジェクト指向プログラミングの3つの基本原則のうち2つは、継承とポリモーフィズムであり、オブジェクトを参照型ではなく値型として扱うと、両方が破壊されます。オブジェクトをパラメーターとして関数に渡すとき、コンパイラーは渡すバイト数を知る必要があります。オブジェクトが参照型の場合、答えは簡単です。すべてのオブジェクトで同じポインターのサイズです。ただし、オブジェクトが値型の場合、値の実際のサイズを渡す必要があります。派生クラスは新しいフィールドを追加できるため、これはsizeof(derived)!= sizeof(base)を意味し、ポリモーフィズムはウィンドウの外に出ます。
この問題を示す簡単なC ++プログラムを次に示します。
#include <iostream>
class Parent
{
public:
int a;
int b;
int c;
Parent(int ia, int ib, int ic) {
a = ia; b = ib; c = ic;
};
virtual void doSomething(void) {
std::cout << "Parent doSomething" << std::endl;
}
};
class Child : public Parent {
public:
int d;
int e;
Child(int id, int ie) : Parent(1,2,3) {
d = id; e = ie;
};
virtual void doSomething(void) {
std::cout << "Child doSomething : D = " << d << std::endl;
}
};
void foo(Parent a) {
a.doSomething();
}
int main(void)
{
Child c(4, 5);
foo(c);
return 0;
}
このプログラムの出力は、ベースオブジェクトを期待する関数に値によって派生オブジェクトを渡すことができないため、コンパイラが隠しコピーコンストラクタを作成して渡すため、健全なOO言語の同等のプログラムの場合とは異なります指示されたようにChildオブジェクトを渡す代わりに、ChildオブジェクトのParent部分のコピー。このような隠されたセマンティックの落とし穴は、オブジェクトを値で渡すことをC ++で避ける必要がある理由であり、他のほとんどすべてのOO言語ではまったく不可能です。
それ以外の場合はポリモーフィズムがないためです。
オブジェクト指向プログラミングでDerived
は、Base
1つからより大きなクラスを作成し、それを期待する関数に渡すことができますBase
。かなり些細なええ?
関数の引数のサイズが固定され、コンパイル時に決定されることを除きます。あなたが望むすべてを議論することができ、実行可能コードはこのようなものであり、言語はある時点で実行される必要があります(純粋に解釈される言語はこれによって制限されません...)
現在、コンピューター上で明確に定義されたデータが1つあります。通常は1つまたは2つの「ワード」として表されるメモリセルのアドレスです。プログラミング言語のポインターまたは参照として表示されます。
したがって、任意の長さのオブジェクトを渡すために行う最も簡単なことは、このオブジェクトへのポインタ/参照を渡すことです。
これは、OOプログラミングの技術的な制限です。
しかし、大規模な型の場合、コピーを回避するために一般に参照を渡すことを好むため、一般的に大きな打撃とは見なされません:)
ただし、JavaまたはC#では、オブジェクトをメソッドに渡すときに、オブジェクトがメソッドによって変更されるかどうかわからないという重要な結果が1つあります。それはデバッグ/並列化をより難しくし、これは機能言語と透過的参照が対処しようとしている問題です->コピーはそれほど悪くはありません(理にかなっている場合)。
申し分なく、これがオブジェクトが参照型または参照渡しである理由を正確に言っているわけではありませんが、これが長期的に非常に良いアイデアである理由の例を提供できます。
間違っていなければ、C ++でクラスを継承すると、そのクラスのすべてのメソッドとプロパティが物理的に子クラスにコピーされます。子クラス内でそのクラスのコンテンツを再度記述するようなものです。
そのため、子クラスのデータの合計サイズは、親クラスと派生クラスのデータの組み合わせになります。
EG:#include
class Top
{
int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};
class Middle : Top
{
int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};
class Bottom : Middle
{
int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};
int main()
{
using namespace std;
int arr[20];
cout << "Size of array of 20 ints: " << sizeof(arr) << endl;
Top top;
Middle middle;
Bottom bottom;
cout << "Size of Top Class: " << sizeof(top) << endl;
cout << "Size of middle Class: " << sizeof(middle) << endl;
cout << "Size of bottom Class: " << sizeof(bottom) << endl;
}
それはあなたを示すでしょう:
Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240
つまり、複数のクラスの大きな階層がある場合、ここで宣言されているオブジェクトの合計サイズは、これらすべてのクラスの組み合わせになります。明らかに、これらのオブジェクトは多くの場合にかなり大きくなります。
解決策は、ヒープ上に作成し、ポインターを使用することだと思います。これは、ある意味で、いくつかの親を持つクラスのオブジェクトのサイズを管理できることを意味します。
これが、参照を使用することがこれを行うためのより望ましい方法である理由です。