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#の作成者は、オブジェクトが参照によって常に返される方が、特に言語がネイティブでサポートしている場合は、より優れたソリューションであることに気付きました。ガベージコレクションなど、言語が持つ他の多くの機能に関連付けられます。
他の多くの回答には良い情報があります。部分的にしか取り上げられていないcloningについて重要なポイントを1つ追加したいと思います。
参照の使用は賢明です。物事をコピーすることは危険です。
他の人が言ったように、Javaには自然な「クローン」はありません。 これは単なる欠けている機能ではありません。あなたneverは、オブジェクトのすべてのプロパティを(浅くても深くても)単にコピーしたいだけです。そのプロパティがデータベース接続の場合はどうなりますか?人間のクローンを作成できる以上に、データベース接続を「クローン」することはできません。 初期化が理由で存在しています。
深いコピーはそれ自体の問題です-あなたはどのくらい深く本当に行きますか?静的なもの(Class
オブジェクトを含む)は絶対にコピーできませんでした。
つまり、自然なクローンが存在しないのと同じ理由で、コピーとして渡されるオブジェクトはcreate insanityになります。 DB接続を「クローン」できたとしても、どのようにして接続が閉じられていることを確認できますか?
*コメントを参照してください-この「決して」ステートメントとは、すべてのプロパティを複製するauto-cloneを意味します。 Javaは提供していません。この言語のユーザーが独自に作成することは、ここにリストされている理由のため、おそらくお勧めできません。非一時的なフィールドのみを複製することは、開始ですが、それでも、必要に応じてtransient
を定義するために勤勉にする必要があります。
参照渡しされるC#オブジェクトに関する最初のステートメントは正しくありません。 C#では、オブジェクトは参照型ですが、デフォルトでは、値型と同じように値で渡されます。参照型の場合、値渡しメソッドのパラメータとしてコピーされる「値」は参照自体なので、メソッド内のプロパティへの変更はメソッドスコープの外に反映されます。
ただし、メソッド内でパラメーター変数自体を再割り当てした場合、この変更はメソッドのスコープ外には反映されないことがわかります。対照的に、実際にref
キーワードを使用して参照によってパラメーターを渡す場合、この動作は期待どおりに機能します。
オブジェクトは常にJavaで参照されます。彼らは自分自身の周りを通過することはありません。
1つの利点は、これにより言語が簡略化されることです。 C++オブジェクトは値または参照として表すことができるため、メンバーにアクセスするには2つの異なる演算子_.
_および_->
_を使用する必要があります。 (これを統合できない理由があります。たとえば、スマートポインターは参照であり、それらを区別しておく必要がある値です。)Java必要なのは_.
_だけです。
もう1つの理由は、ポリモーフィズムは値ではなく参照によって行われる必要があることです。値で処理されるオブジェクトはそこにあり、型は固定されています。 C++でこれを台無しにすることは可能です。
また、Javaはデフォルトの割り当て/コピー/何でも切り替えることができます。C++では多かれ少なかれ深いコピーですが、Javaでは単純なポインタ割り当てです/ copy/whatever、コピーが必要な場合のために.clone()
などを使用します。
クイックアンサー
Java=および同様の言語の設計者は、「すべてがオブジェクトである」という概念を適用することを望んでいました。参照としてデータを渡すことは非常に迅速であり、多くのメモリを消費しません。
追加の拡張退屈なコメント
さらに、これらの言語はオブジェクト参照(Java、Delphi、C#、VB.NET、Vala、Scala、PHP)を使用していますが、実際には、オブジェクト参照は変装したオブジェクトへのポインターです。 null値、メモリ割り当て、オブジェクトのデータ全体をコピーせずに参照をコピーしたもの、すべてがプレーンオブジェクトではなくオブジェクトポインタです!!!
Object Pascal(Delphiではない)、C++(Javaではなく、C#ではない)では、オブジェクトは静的に割り当てられた変数として宣言できます。また、動的に割り当てられた変数を使用して、ポインタを使用して(「オブジェクト参照」なしで)砂糖の構文」)。いずれの場合も特定の構文を使用しており、Java "and friends"のように混乱する方法はありません。これらの言語では、オブジェクトは値としても参照としても渡すことができます。
プログラマーは、ポインター構文が必要な場合と必要でない場合を認識していますが、Javaなどの言語では、これは混乱を招きます。
Javaが存在するか主流になる前は、多くのプログラマーはポインターなしでC++でオブジェクト指向を学び、必要に応じて値または参照によって渡していました。学習からビジネスアプリに切り替えたとき、彼らは一般にオブジェクトポインターを使用します。 。QTライブラリはその良い例です。
私がJavaを学んだとき、私はすべてがオブジェクトコンセプトであるようにしようとしましたが、コーディングで混乱しました。結局、私は「これは静的に割り当てられたオブジェクトの構文を持つポインタで動的に割り当てられたオブジェクトです」と言って、再びコーディングするのに問題はありませんでした。
JavaとC#は、低レベルのメモリを制御します。作成したオブジェクトが存在する「ヒープ」には、それ自体が存在します。たとえば、ガベージコレクターは必要なときにいつでもオブジェクトを取得します。
プログラムとその「ヒープ」の間に間接的な層が別にあるので、オブジェクトを値で参照する2つの方法(C++のように)とポインタで区別する方法)は区別できなくなります:常に参照するヒープ内のどこかに「ポインタによって」オブジェクトに。そのため、このような設計アプローチでは、参照渡しが割り当てのデフォルトのセマンティクスになります。 Java、C#、Rubyなど。
上記は命令型言語のみに関係します。上記の言語では、メモリの制御はランタイムに渡されますが、言語設計では「ちょっと、しかし実際にはそこにisメモリがあります」 、そしてそこにareオブジェクトがあり、それらはdoメモリを占有します " 。関数型言語は、「メモリ」の概念を定義から除外することにより、さらに抽象化します。そのため、低レベルのメモリを制御しないすべての言語に参照渡しが必ずしも適用されるわけではありません。
私はいくつかの理由を考えることができます:
プリミティブ型のコピーは簡単です。通常、1つの機械語命令に変換されます。
オブジェクトのコピーは簡単ではありません。オブジェクトには、それ自体がオブジェクトであるメンバーを含めることができます。オブジェクトのコピーには、CPU時間とメモリの負荷がかかります。コンテキストに応じてオブジェクトをコピーする方法は複数あります。
参照によるオブジェクトの受け渡しは安価であり、オブジェクトの複数のクライアント間でオブジェクト情報を共有/更新する場合にも便利です。
複雑なデータ構造(特に再帰的なもの)にはポインターが必要です。参照によるオブジェクトの受け渡しは、ポインタを渡すためのより安全な方法です。
それ以外の場合、関数は、渡されたあらゆる種類のオブジェクトの(明らかに深い)コピーを自動的に作成できる必要があります。そして通常それを作ることを推測することはできません。したがって、すべてのオブジェクト/クラスのコピー/クローンメソッドの実装を定義する必要があります。
それ以外の場合はポリモーフィズムがないためです。
OOプログラミングでは、Derived
oneからより大きなBase
クラスを作成し、それをBase
oneを期待する関数に渡すことができます。かなり簡単ですね?
ただし、関数の引数のサイズは固定されており、コンパイル時に決定されます。あなたはあなたが望むすべてを主張することができ、実行可能コードはこのようなものであり、言語はある時点で実行される必要があります(純粋に解釈された言語はこれによって制限されません...)
これで、コンピュータ上で明確に定義された1つのデータがあります。通常は1つまたは2つの「ワード」として表されるメモリセルのアドレスです。プログラミング言語では、ポインタまたは参照として表示されます。
したがって、任意の長さのオブジェクトを渡すために行う最も簡単なことは、このオブジェクトへのポインタ/参照を渡すことです。
これはOOプログラミング)の技術的な制限です
ただし、大きな型の場合は、コピーを避けるために参照を渡すことを一般に好むため、一般的に大きな打撃とは見なされません:)
ただし、重要な結果が1つあります。JavaまたはC#では、オブジェクトをメソッドに渡すときに、オブジェクトがメソッドによって変更されるかどうかがわかりません。デバッグ/並列化が行われますそれはもっと難しく、これは関数型言語と透過的な参照が対処しようとしている問題です->コピーはそれほど悪くありません(意味がある場合)。
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オブジェクトのParent部分のコピーを渡します。代わりに、指示されたようにChildオブジェクトを渡すのではありません。値はC++では避けるべきであり、他のほとんどすべての言語ではまったく不可能ですOO言語。