オブジェクトが参照渡しされるのはなぜですか?


31

OOを勉強していた若い同僚が、なぜすべてのオブジェクトが参照で渡されるのかと尋ねました。これは、プリミティブ型または構造体の反対です。これは、JavaやC#などの言語の一般的な特性です。

彼の良い答えが見つかりませんでした。

この設計決定の動機は何ですか?これらの言語の開発者は、毎回ポインターとtypedefを作成することにうんざりしていましたか?


2
JavaとC#で、値ではなく参照で、またはポインターではなく参照でパラメーターを渡す理由を尋ねていますか?
ロバート

@Robert、「ポインタではなく参照」の間に高レベルの違いはありますか?タイトルを「オブジェクトが常に参照される理由」のようなものに変更する必要があると思いますか?
グスタボカルドーソ

参照はポインターです。
compman

2
@Anto:Java参照は、すべての点で適切に使用されるCポインターと同じです(適切に使用されます:型キャストではなく、無効なメモリに設定されず、リテラルで設定されません)。
ザンリンクス

5
また、本当に面白くするために、タイトルは正しくありません(少なくとも.netに関する限り)。オブジェクトは参照渡しではなく、参照は値渡しです。オブジェクトをメソッドに渡すと、参照値がメソッド本体内の新しい参照にコピーされます。「オブジェクトが参照渡しされる」というのは、間違っていると一般的なプログラマーの引用のリストに入ってしまい、新しいプログラマーの参照の理解が不十分になるというのは残念だと思います。
SecretDeveloper

回答:


15

基本的な理由は次のとおりです。

  • ポインターは正しく機能するために技術的です
  • 特定のデータ構造を実装するには、ポインターが必要です
  • メモリーを効率的に使用するにはポインターが必要です
  • あなたは仕事に手動でメモリのインデックス付けを必要としない場合は、あなたが直接ハードウェアを使用していません。

したがって、参照。


32

簡単な答え:

どこかに渡されたすべてのオブジェクトのディープコピーを再作成および実行する際のメモリ消費
CPU時間を最小限に抑えます


あなたに同意しますが、これらに沿って審美的またはオブジェクト指向デザインの動機もあると思います。
グスタボカルドーソ

9
@Gustavo Cardoso:「美学またはオブジェクト指向デザインの動機」。いいえ。それは単に最適化です。
S.Lott

6
@ S.Lott:いいえ、セマンティック的にはオブジェクトのコピーを作成したくないため、オブジェクト指向用語で参照渡しするのは理にかなっています。オブジェクトのコピーではなく、オブジェクトを渡したい場合。値渡しをしている場合、オブジェクトのクローンがすべて、高レベルでは意味をなさない場所全体で生成されているため、オブジェクト指向のメタファーをいくらか破ります。
20:23に

@グスタボ:私たちは同じ点を主張していると思います。OOPのセマンティクスに言及し、OOPの比phorが私自身の追加の理由であることに言及しています。OOPのクリエイターが「CPU時間を保存」それ彼らは「メモリ消費を最小化する」ために行った方法と作られたように私には思える
ティム・

14

C ++には、2つの主なオプションがあります。値による戻りまたはポインターによる戻りです。最初のものを見てみましょう:

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

コンパイラが戻り値の最適化を使用するほど賢くないと仮定すると、ここで何が起こるかは次のとおりです。

  • newObjは一時オブジェクトとして構築され、ローカルスタックに配置されます。
  • newObjのコピーが作成され、返されます。

オブジェクトのコピーを無意味に作成しました。これは処理時間の無駄です。

代わりに、ポインターによるリターンを見てみましょう。

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

冗長なコピーを削除しましたが、別の問題を導入しました。自動的に破棄されないヒープ上にオブジェクトを作成しました。自分で対処する必要があります。

MyClass someObj = getNewObject();
delete someObj;

この方法で割り当てられたオブジェクトを削除する責任者を知ることは、コメントまたは慣例によってのみ伝えることができるものです。メモリリークが発生しやすくなります。

これらの2つの問題を解決するための多くの回避策が提案されています-戻り値の最適化(コンパイラは、値による戻りで冗長コピーを作成しないほどスマートである)、メソッドへの参照を渡す(関数が新しいオブジェクトを作成するのではなく、既存のオブジェクト)、スマートポインター(所有権の問題は議論の余地がないように)。

Java / C#の作成者は、特に言語がネイティブにオブジェクトをサポートしている場合は、常に参照によってオブジェクトを返すことがより良いソリューションであることに気付きました。ガベージコレクションなど、言語が持つ他の多くの機能と結びついています。


値渡しは十分に悪いですが、値渡しはオブジェクトに関してはさらに悪いものであり、私はそれが彼らが避けようとしていた本当の問題だったと思います。
メイソンウィーラー

確かにあなたは有効なポイントを持っています。しかし、@ Masonが指摘したオブジェクト指向設計の問題は、変更の最終的な動機でした。参照を使用するだけの場合、参照と値の違いを保持する意味はありませんでした。
グスタボカルドーソ

10

他の多くの答えには良い情報があります。クローン作成に関して、部分的にしか対処されていない重要なポイントを1つ追加します。

参照の使用は賢明です。コピーは危険です。

他の人が言ったように、Javaには自然な「クローン」はありません。 これは単なる欠落機能ではありません。オブジェクト内のすべてのプロパティを(浅くも深くも)自由自在にコピーしたい ことは決してありません。そのプロパティがデータベース接続であった場合はどうなりますか?人間のクローンを作成できる以上に、データベース接続を「クローン」することはできません。 初期化には理由があります。

どのように深いあなたはない-ディープコピーは、独自の問題があり、実際に行きますか?静的なもの(Classオブジェクトを含む)はコピーできません。

したがって、自然なクローンがない同じ理由で、コピーとして渡されるオブジェクトは狂気生み出します。DB接続を「クローン」できたとしても、それがどのように閉じられていることを確認しますか?


*コメントを参照-この「決して」ステートメントは、すべてのプロパティを複製する自動クローンを意味します。Javaはそれを提供しませんでした。おそらく、ここに挙げた理由から、言語のユーザーとして独自に作成することはお勧めできません。一時的でないフィールドのみのクローンを作成することは出発点になりますが、それでもtransient適切な場所の定義については熱心に取り組む必要があります。


特定の条件での良い異議からクローニングへのジャンプが、決して必要ではないという声明へのジャンプを理解するのに苦労しています。そして、正確な複製が必要な状況に遭遇しました。関連する静的関数、IOまたはオープン接続は問題になりません
インカ

2
@Inca-あなたは私を誤解しているかもしれません。意図的に実装してcloneも問題ありません。「willy-nilly」とは、意図せずに、考えずにすべてのプロパティをコピーすることを意味します。Java言語の設計者は、ユーザーが作成したの実装を要求することにより、この意図を強制しましたclone
ニコール

不変オブジェクトへの参照を使用するのは賢明です。Dateのような単純な値を可変にしてから、それらへの複数の参照を作成することはできません。
ケビンクライン

@NickC:「ものごとを自由に複製する」ことが危険な主な理由は、Javaや.netなどの言語/フレームワークには、参照が可変状態、アイデンティティ、両方、または両方をカプセル化するかどうかを宣言的に示す手段がないためです。フィールドに変更可能な状態をカプセル化するオブジェクト参照が含まれるがIDを含まない場合、オブジェクトを複製するには、状態を保持するオブジェクトを複製し、その複製への参照をフィールドに保存する必要があります。参照がIDをカプセル化するが、可変状態ではない場合、コピーのフィールドは元のオブジェクトと同じオブジェクトを参照する必要があります。
-supercat

ディープコピーの側面は重要なポイントです。他のオブジェクトへの参照が含まれている場合、特にオブジェクトグラフに可変オブジェクトが含まれている場合、オブジェクトのコピーは問題があります。
カレブ

4

オブジェクトは常にJavaで参照されます。彼らは自分自身の周りに渡されることはありません。

1つの利点は、これにより言語が簡素化されることです。C ++オブジェクトは値または参照として表すことができるため、2つの異なる演算子を使用してメンバーにアクセスする必要が生じます: .および->。(これを統合できない理由があります。たとえば、スマートポインターは参照である値であり、それらを区別する必要があります.。)Javaのみが必要です。

もう1つの理由は、多態性は値ではなく参照によって行われる必要があることです。値で扱われるオブジェクトはそこにあり、固定された型を持っています。C ++でこれを台無しにすることは可能です。

また、Javaはデフォルトの割り当て/コピー/その他を切り替えることができます。C ++では多かれ少なかれコピーですが、Javaではコピー.clone()が必要な場合に備えて、単純なポインターの割り当て/コピー/何でもです。


時にはそれはあなたが使用している場合、本当に醜いなる「(*オブジェクト) - >」
グスタボ・カルドーゾ

1
C ++がポインター、参照、および値を区別することは注目に値します。SomeClass *はオブジェクトへのポインターです。SomeClass&はオブジェクトへの参照です。SomeClassは値型です。
アント

最初の質問ですでに@Roberに質問しましたが、ここでも行います。C++での*と&の違いは、単に低レベルの技術的なことではありませんか。それらは、高レベルで、意味的には同じですか?
グスタボカルドーソ

3
@Gustavo Cardoso:違いはセマンティックです。低い技術レベルでは、それらは一般的に同一です。ポインターがオブジェクトを指すか、NULL(定義された不正な値)です。でない限りconst、その値は他のオブジェクトを指すように変更できます。参照はオブジェクトの別の名前であり、NULLにすることはできず、再配置することもできません。通常、ポインターの単純な使用によって実装されますが、それは実装の詳細です。
デヴィッドソーンリー

「多態性は参照によって行われる必要がある」ための+1。これは非常に重要な詳細であり、他のほとんどの回答では無視されています。
ドーバル

4

参照によって渡されるC#オブジェクトに関する最初のステートメントは正しくありません。C#では、オブジェクトは参照型ですが、デフォルトでは値型と同様に値によって渡されます。参照型の場合、値渡しメソッドのパラメーターとしてコピーされる「値」は参照自体であるため、メソッド内のプロパティへの変更はメソッドスコープ外に反映されます。

ただし、メソッド内でパラメータ変数自体を再割り当てする場合、この変更はメソッドスコープ外に反映されないことがわかります。対照的に、実際にrefキーワードを使用して参照によってパラメーターを渡す場合、この動作は期待どおりに機能します。


3

素早い回答

Javaおよび同様の言語の設計者は、「すべてがオブジェクトである」という概念を適用したいと考えていました。また、データを参照として渡すのは非常に迅速であり、多くのメモリを消費しません。

追加の拡張退屈なコメント

また、これらの言語はオブジェクト参照(Java、Delphi、C#、VB.NET、Vala、Scala、PHP)を使用しますが、実際には、オブジェクト参照は偽装されたオブジェクトへのポインターです。null値、メモリ割り当て、オブジェクトのデータ全体をコピーしない参照のコピー、それらはすべてオブジェクトポインターであり、プレーンオブジェクトではありません!!!

Object Pascal(Delphiではない)、c ++(Javaではなく、C#ではない)では、オブジェクトは静的に割り当てられた変数として、また動的に割り当てられた変数とともに、ポインター(「シュガーシンタックス」)。各ケースは特定の構文を使用しますが、Javaの「および友人」のように混乱する方法はありません。これらの言語では、オブジェクトを値または参照として渡すことができます。

プログラマーは、ポインター構文がいつ必要で、いつ必要ではないかを知っていますが、Javaや同様の言語では、これは混乱を招きます。

Javaが存在するか主流になる前に、多くのプログラマーは、ポインターなしで、必要に応じて値渡しまたは参照渡しのC ++でオブジェクト指向を学びました。学習アプリからビジネスアプリに切り替えると、一般的にオブジェクトポインターを使用します。QTライブラリはその良い例です。

Javaを学んだとき、私はすべてがオブジェクトの概念であることに従おうとしましたが、コーディングで混乱しました。最終的に、「OK、これは静的に割り当てられたオブジェクトの構文を持つポインターで動的に割り当てられたオブジェクトです」と言ったので、再度コーディングするのに苦労しませんでした。


3

JavaとC#は、低レベルのメモリをユーザーから制御します。作成するオブジェクトが存在する「ヒープ」は、独自の生活を送ります。たとえば、ガベージコレクターは、必要に応じてオブジェクトを取得します。

プログラムとその「ヒープ」の間に別の間接層があるため、値とポインタ(C ++など)でオブジェクトを参照する2つの方法は区別できなくなります。常にオブジェクトを「ポインタ」で参照します。ヒープ内のどこかに。そのため、このような設計アプローチでは、参照渡しが割り当てのデフォルトのセマンティクスになります。Java、C#、Rubyなど。

上記は命令型言語のみに関係します。言語では、メモリの制御が実行時に渡され、上述したが、言語設計も言う、「ちょっと、実際に、そこにあるメモリは、そこにあるオブジェクトは、それらがないメモリを占有」。関数型言語は、定義から「メモリ」の概念を除外することにより、さらに抽象化します。そのため、低レベルのメモリを制御していないすべての言語に参照渡しが必ずしも適用されるわけではありません。


2

いくつかの理由が考えられます。

  • プリミティブ型のコピーは簡単で、通常は1つの機械語命令に変換されます。

  • オブジェクトのコピーは簡単ではありません。オブジェクトには、それ自体がオブジェクトであるメンバーを含めることができます。オブジェクトのコピーは、CPU時間とメモリを消費します。コンテキストに応じて、オブジェクトをコピーする方法は複数あります。

  • 参照によるオブジェクトの受け渡しは安価であり、オブジェクトの複数のクライアント間でオブジェクト情報を共有/更新する場合にも便利です。

  • 複雑なデータ構造(特に再帰的なデータ構造)にはポインターが必要です。オブジェクトを参照渡しすることは、ポインターを渡すより安全な方法です。


1

それ以外の場合、関数は、渡されるあらゆる種類のオブジェクトの(明らかに深い)コピーを自動的に作成できるはずです。そして、通常、それを推測することはできません。そのため、すべてのオブジェクト/クラスに対してコピー/クローンメソッドの実装を定義する必要があります。


それは単に浅いコピーを行い、実際の値と他のオブジェクトへのポインタを保持できますか?
グスタボカルドーソ

#Gustavo Cardoso、これを介して他のオブジェクトを変更できますが、参照として渡されないオブジェクトに期待するものは何ですか?
デビッド

0

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言語ではまったく不可能です。


非常に良い点。ただし、問題を回避するにはかなりの労力がかかるため、返品の問題に焦点を当てました。無効のfoo(親会社&A):このプログラムは、単一のアンパサンドを追加して固定することができます
Antの

OOは本当に正しいのポインタなしでは動作しません
グスタボ・カルドーゾ

-1.000000000000
P

5
Javaは値渡しです(オブジェクト参照を値で渡しますが、プリミティブは純粋に値で渡される)ことを覚えておくことが重要です。
ニコール

3
@Pavel Shved-「2つは1つよりも優れています!」または、言い換えれば、より多くのロープを使用して自分自身を掛けます。
ニコール

0

それ以外の場合はポリモーフィズムがないためです。

オブジェクト指向プログラミングでDerivedは、Base1つからより大きなクラスを作成し、それを期待する関数に渡すことができますBase。かなり些細なええ?

関数の引数のサイズが固定され、コンパイル時に決定されることを除きます。あなたが望むすべてを議論することができ、実行可能コードはこのようなものであり、言語はある時点で実行される必要があります(純粋に解釈される言語はこれによって制限されません...)

現在、コンピューター上で明確に定義されたデータが1つあります。通常は1つまたは2つの「ワード」として表されるメモリセルのアドレスです。プログラミング言語のポインターまたは参照として表示されます。

したがって、任意の長さのオブジェクトを渡すために行う最も簡単なことは、このオブジェクトへのポインタ/参照を渡すことです。

これは、OOプログラミングの技術的な制限です。

しかし、大規模な型の場合、コピーを回避するために一般に参照を渡すことを好むため、一般的に大きな打撃とは見なされません:)

ただし、JavaまたはC#では、オブジェクトをメソッドに渡すときに、オブジェクトがメソッドによって変更されるかどうかわからないという重要な結果が1つあります。それはデバッグ/並列化をより難しくし、これは機能言語と透過的参照が対処しようとしている問題です->コピーはそれほど悪くはありません(理にかなっている場合)。


-1

答えは名前にあります(とにかくほとんど)。参照(アドレスなど)は単に他の何かを参照し、値は他の何かの別のコピーです。誰かがおそらく次の効果について何かを述べていると思いますが、一方が適切であり、もう一方が適切ではない状況があります(メモリセキュリティvsメモリ効率)。それは、メモリ、メモリ、メモリの管理についてすべてです......メモリ!:D


-2

申し分なく、これがオブジェクトが参照型または参照渡しである理由を正確に言っているわけではありませんが、これが長期的に非常に良いアイデアである理由の例を提供できます。

間違っていなければ、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

つまり、複数のクラスの大きな階層がある場合、ここで宣言されているオブジェクトの合計サイズは、これらすべてのクラスの組み合わせになります。明らかに、これらのオブジェクトは多くの場合にかなり大きくなります。

解決策は、ヒープ上に作成し、ポインターを使用することだと思います。これは、ある意味で、いくつかの親を持つクラスのオブジェクトのサイズを管理できることを意味します。

これが、参照を使用することがこれを行うためのより望ましい方法である理由です。


1
これは、13の以前の回答で作成され、説明されたポイントに対して実質的なものを提供していないようです
-gnat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.