新しいプログラミング言語を学習するときに遭遇する可能性のある障害の 1 つは、その言語がデフォルトで値渡しか参照渡しかという問題です。
それでは皆さんに質問です、好きな言語で、実際にはどのように行われますか? そして、考えられる落とし穴は何ですか?
もちろん、お気に入りの言語は、これまでに遊んだことがある言語なら何でもかまいません:人気のある、あいまいな、難解な、新しい、古い...
以下は、 Java プログラミング言語に対する私自身の貢献です。
最初のいくつかのコード:
public void swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
このメソッドを呼び出すと、次のようになります。
int pi = 3;
int everything = 42;
swap(pi, everything);
System.out.println("pi: " + pi);
System.out.println("everything: " + everything);
"Output:
pi: 3
everything: 42"
「実際の」オブジェクトを使用しても、同様の結果が表示されます。
public class MyObj {
private String msg;
private int number;
//getters and setters
public String getMsg() {
return this.msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getNumber() {
return this.number;
}
public void setNumber(int number) {
this.number = number;
}
//constructor
public MyObj(String msg, int number) {
setMsg(msg);
setNumber(number);
}
}
public static void swap(MyObj x, MyObj y)
{
MyObj tmp = x;
x = y;
y = tmp;
}
public static void main(String args[]) {
MyObj x = new MyObj("Hello world", 1);
MyObj y = new MyObj("Goodbye Cruel World", -1);
swap(x, y);
System.out.println(x.getMsg() + " -- "+ x.getNumber());
System.out.println(y.getMsg() + " -- "+ y.getNumber());
}
"Output:
Hello world -- 1
Goodbye Cruel World -- -1"
したがって、 piの値とすべてのオブジェクト、およびMyObj オブジェクトが交換されていないため、Java がそのパラメーターを値で渡すことは明らかです。Java でパラメータをメソッドに渡す唯一の方法は「値渡し」であることに注意してください。(たとえば、c++ のような言語では、開発者は、パラメーターの型の後に「 & 」を使用して参照によってパラメーターを渡すことができます)
今はトリッキーな部分、または少なくとも新しい Java 開発者のほとんどを混乱させる部分です: ( javaworldから借用)
原作者: Tony Sintes
public void tricky(Point arg1, Point arg2)
{
arg1.x = 100;
arg1.y = 100;
Point temp = arg1;
arg1 = arg2;
arg2 = temp;
}
public static void main(String [] args)
{
Point pnt1 = new Point(0,0);
Point pnt2 = new Point(0,0);
System.out.println("X: " + pnt1.x + " Y: " +pnt1.y);
System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
System.out.println(" ");
tricky(pnt1,pnt2);
System.out.println("X: " + pnt1.x + " Y:" + pnt1.y);
System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
}
"Output
X: 0 Y: 0
X: 0 Y: 0
X: 100 Y: 100
X: 0 Y: 0"
トリッキーは pnt1 の値を正常に変更します! これは、オブジェクトが参照によって渡されることを意味しますが、そうではありません! 正しいステートメントは次のとおりです。オブジェクト参照は値によって渡されます。
Tony Sintes の詳細:
このメソッドは、pnt1 の値を値で渡しますが、正常に変更します。ただし、pnt1 と pnt2 のスワップは失敗します。これが混乱の主な原因です。main() メソッドでは、pnt1 と pnt2 はオブジェクト参照にすぎません。pnt1 と pnt2 をトリッキー() メソッドに渡すと、Java は他のパラメーターと同様に参照を値で渡します。これは、メソッドに渡される参照が実際には元の参照のコピーであることを意味します。以下の図 1 は、Java がオブジェクトをメソッドに渡した後、同じオブジェクトを指す 2 つの参照を示しています。
(出典: javaworld.com )
結論または簡単な話:
便利なリンク:
これがc#プログラミング言語の別の記事です
c#は引数を値で渡します(デフォルト)
private void swap(string a, string b) {
string tmp = a;
a = b;
b = tmp;
}
したがって、このバージョンのスワップを呼び出しても結果はありません。
string x = "foo";
string y = "bar";
swap(x, y);
"output:
x: foo
y: bar"
ただし、java c#が開発者に参照によってパラメーターを渡す機会を与えるのとは異なり、これはパラメーターのタイプの前に「ref」キーワードを使用して行われます。
private void swap(ref string a, ref string b) {
string tmp = a;
a = b;
b = tmp;
}
このスワップにより、参照されるパラメーターの値が変更されます。
string x = "foo";
string y = "bar";
swap(x, y);
"output:
x: bar
y: foo"
c#にもoutキーワードがあり、refとoutの違いは微妙です。 msdnから:
outパラメーターを受け取るメソッドの呼び出し元は、呼び出しの 前にoutパラメーターとして渡された変数に割り当てる必要はありません。ただし、呼び出し先は、戻る前にoutパラメータに割り当てる必要があります。
と
対照的に、 refパラメータは 、最初に呼び出し先によって割り当てられたと見なされます。そのため、呼び出し先は、 使用前にrefパラメーターに割り当てる必要はありません。Refパラメーターは、メソッドに渡されたり、メソッドから渡されたりします。
小さな落とし穴は、Javaの場合と同様に、値によって渡されたオブジェクトは、内部メソッドを使用して変更できるということです。
結論:
便利なリンク:
Pythonは値渡しを使用しますが、そのような値はすべてオブジェクト参照であるため、最終的な効果は参照渡しに似ています。ただし、Pythonプログラマーは、オブジェクト型が可変であるか不変であるかについてより深く考えます。可変オブジェクトはインプレースで変更できますが(たとえば、辞書、リスト、ユーザー定義オブジェクト)、不変オブジェクトは変更できません(たとえば、整数、文字列、タプル)。
次の例は、2つの引数、不変の文字列、および可変のリストが渡される関数を示しています。
>>> def do_something(a, b):
... a = "Red"
... b.append("Blue")
...
>>> a = "Yellow"
>>> b = ["Black", "Burgundy"]
>>> do_something(a, b)
>>> print a, b
Yellow ['Black', 'Burgundy', 'Blue']
この行は、文字列値a = "Red"
のローカル名を作成するだけで、渡された引数には影響しません(これ以降はローカル名を参照する必要があるため、現在は非表示になっています)。引数が可変であるか不変であるかに関係なく、代入はインプレース操作ではありません。a
"Red"
a
b
パラメータは可変リストオブジェクトへの参照であり、メソッド.append()
はリストのインプレース拡張を実行して、新しい"Blue"
文字列値を追加します。
(文字列オブジェクトは不変であるため、インプレース変更をサポートするメソッドはありません。)
関数が戻ると、の再割り当てはa
効果がありませんが、の拡張はb
参照渡しスタイルの呼び出しセマンティクスを明確に示しています。
前述のように、の引数a
が可変型であっても、関数内での再割り当てはインプレース操作ではないため、渡された引数の値は変更されません。
>>> a = ["Purple", "Violet"]
>>> do_something(a, b)
>>> print a, b
['Purple', 'Violet'] ['Black', 'Burgundy', 'Blue', 'Blue']
呼び出された関数でリストを変更したくない場合は、代わりに、インプレース.append()
メソッドをサポートしない不変のタプルタイプ(角かっこではなく、リテラル形式の括弧で識別されます)を使用します。
>>> a = "Yellow"
>>> b = ("Black", "Burgundy")
>>> do_something(a, b)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in do_something
AttributeError: 'tuple' object has no attribute 'append'
Perl の回答をまだ見ていないので、書いてみようと思いました。
内部では、Perl は参照渡しとして効果的に機能します。関数呼び出しの引数としての変数は参照として渡され、定数は読み取り専用の値として渡され、式の結果は一時値として渡されます。からのリスト割り当てによって@_
、または によって引数リストを構築する通常のイディオムshift
は、これをユーザーから隠して、値渡しのように見せる傾向があります。
sub incr {
my ( $x ) = @_;
$x++;
}
my $value = 1;
incr($value);
say "Value is now $value";
これは、渡された変数ではなく、関数内で宣言されたレキシカル変数をインクリメントしたValue is now 1
ため、出力されます。引数を変更する関数は Perl ではまれであるため、この値渡しスタイルは通常、ほとんどの場合必要とされます。スタイルは避けるべきです。$x++
incr()
ただし、何らかの理由でこの動作が特に必要な場合は、配列の要素を直接操作することで実現できます。これは、@_
配列の要素が関数に渡される変数のエイリアスになるためです。
sub incr {
$_[0]++;
}
my $value = 1;
incr($value);
say "Value is now $value";
今回は、式が実際の変数をインクリメントしたValue is now 2
ため、出力されます。これが機能する方法は、内部では ( によって取得されるような) 他のほとんどの配列のような実際の配列ではなく、その要素が関数呼び出しに渡された引数から直接構築されることです。これにより、必要に応じて参照渡しのセマンティクスを構築できます。プレーンな変数である関数呼び出し引数はそのままこの配列に挿入され、定数またはより複雑な式の結果は読み取り専用の一時変数として挿入されます。$_[0]++
$value
@_
my @array
ただし、Perl は参照値をサポートしているため、実際にこれを行うことは非常にまれです。つまり、他の変数を参照する値です。通常、変数への参照を渡すことによって、変数に明らかな副作用を持つ関数を構築する方がはるかに明確です。これは、参照渡しのセマンティクスが有効であることを、呼び出しサイトの読者に明確に示しています。
sub incr_ref {
my ( $ref ) = @_;
$$ref++;
}
my $value = 1;
incr(\$value);
say "Value is now $value";
ここで、演算子は、C\
のアドレス演算子とほぼ同じ方法で参照を生成します。&
.NETについては、ここに適切な説明があります。
多くの人は、(C# と Java の両方で) 参照オブジェクトが実際に値渡しされることに驚いています。これは、スタック アドレスのコピーです。これにより、オブジェクトが実際に指す場所をメソッドが変更することはできなくなりますが、メソッドはオブジェクトの値を変更できます。C# では、参照によって参照を渡すことができます。つまり、実際のオブジェクトが指す場所を変更できます。
名前による受け渡し、値による受け渡しもあることを忘れないでください-result 。
値渡し-結果は値渡しに似ていますが、パラメーターとして渡された元の変数に値が設定されるという追加の側面があります。ある程度、グローバル変数との干渉を回避できます。参照によるパスがページフォールトを引き起こす可能性があるパーティションメモリでは、明らかに優れています(参照)。
名前による受け渡しとは、値が手順の開始時ではなく、実際に使用されたときにのみ計算されることを意味します。Algolは名前渡しを使用しましたが、興味深い副作用は、スワッププロシージャを作成するのが非常に難しいことです(リファレンス)。また、名前で渡された式は、アクセスされるたびに再評価されるため、副作用が発生する可能性もあります。
Whatever you say as pass-by-value or pass-by-reference must be consistent across languages. The most common and consistent definition used across languages is that with pass-by-reference, you can pass a variable to a function "normally" (i.e. without explicitly taking address or anything like that), and the function can assign to (not mutate the contents of) the parameter inside the function and it will have the same effect as assigning to the variable in the calling scope.
From this view, the languages are grouped as follows; each group having the same passing semantics. If you think that two languages should not be put in the same group, I challenge you to come up with an example that distinguishes them.
The vast majority of languages including C, Java, Python, Ruby, JavaScript, Scheme, OCaml, Standard ML, Go, Objective-C, Smalltalk, etc. are all pass-by-value only. Passing a pointer value (some languages call it a "reference") does not count as pass by reference; we are only concerned about the thing passed, the pointer, not the thing pointed to.
Languages such as C++, C#, PHP are by default pass-by-value like the languages above, but functions can explicitly declare parameters to be pass-by-reference, using &
or ref
.
Perl is always pass-by-reference; however, in practice people almost always copy the values after getting it, thus using it in a pass-by-value way.
Jに関しては、値渡しのAFAIKしかありませんが、大量のデータを移動できる参照渡しの形式があります。ロケールと呼ばれるものを動詞(または関数)に渡すだけです。これは、クラスのインスタンスでも、単なる汎用コンテナーでもかまいません。
spaceused=: [: 7!:5 <
exectime =: 6!:2
big_chunk_of_data =. i. 1000 1000 100
passbyvalue =: 3 : 0
$ y
''
)
locale =. cocreate''
big_chunk_of_data__locale =. big_chunk_of_data
passbyreference =: 3 : 0
l =. y
$ big_chunk_of_data__l
''
)
exectime 'passbyvalue big_chunk_of_data'
0.00205586720663967
exectime 'passbyreference locale'
8.57957102144893e_6
明らかな欠点は、呼び出された関数で何らかの方法で変数の名前を知る必要があることです。しかし、この手法では、多くのデータを簡単に移動できます。そのため、技術的には参照を渡すことはありませんが、私はそれを「ほとんどそれ」と呼んでいます。
値による
参照により
PHPも値渡しです。
<?php
class Holder {
private $value;
public function __construct($value) {
$this->value = $value;
}
public function getValue() {
return $this->value;
}
}
function swap($x, $y) {
$tmp = $x;
$x = $y;
$y = $tmp;
}
$a = new Holder('a');
$b = new Holder('b');
swap($a, $b);
echo $a->getValue() . ", " . $b->getValue() . "\n";
出力:
a b
ただし、PHP4 では、オブジェクトはプリミティブのように扱われました。つまり:
<?php
$myData = new Holder('this should be replaced');
function replaceWithGreeting($holder) {
$myData->setValue('hello');
}
replaceWithGreeting($myData);
echo $myData->getValue(); // Prints out "this should be replaced"
デフォルトでは、ANSI/ISO C はどちらかを使用します。これは、関数とそのパラメーターを宣言する方法によって異なります。
関数パラメーターをポインターとして宣言すると、関数は参照渡しになり、関数パラメーターを非ポインター変数として宣言すると、関数は値渡しになります。
void swap(int *x, int *y); //< Declared as pass-by-reference.
void swap(int x, int y); //< Declared as pass-by-value (and probably doesn't do anything useful.)
関数内で作成された非静的変数へのポインターを返す関数を作成すると、問題が発生する可能性があります。次のコードの戻り値は未定義です。関数で作成された一時変数に割り当てられたメモリ空間が上書きされたかどうかを知る方法はありません。
float *FtoC(float temp)
{
float c;
c = (temp-32)*9/5;
return &c;
}
ただし、パラメーター リストで渡された静的変数またはポインターへの参照を返すことはできます。
float *FtoC(float *temp)
{
*temp = (*temp-32)*9/5;
return temp;
}