Chienomi

「参照の値渡し」などというものは存在しない

プログラミング::beginners

「参照の値渡し」という語を聞いたのは数年前だと思う。

「なんかよくわからない話だな」と思ったが、読み込むと「わからない話だな」に変わった。

要点としては、「受け取った変数に代入したときに渡した元で値が変われば参照渡し、変わらないなら参照の値渡し」という主張であるらしい。

だが、これは「refernce(名詞)」と「referred」と「reference(動詞)」の関係が分かってないだけだ。 なので、そんなものはない。

といっても、それですぐ分かる人はそんな馬鹿げたことを言うことはないだろう。 確かに、これは理解がなかなか難しいものでもある。 その一因として、これらの操作を手動で行える言語というのがほとんどないからだ。

そこで、ここではこれらの手動操作が可能なPerlを用いて解説する。 Perlは参照をそのままreferenceと呼んでいるが、参照にまつわる操作に関しては独特の用語使いをするので、そのあたりも解説する。

そもそも参照とは

参照(reference)という概念自体が初心者にはなかなか難しい概念だ。

まず最初に思いつくのはC言語のポインタ(pointer)だろうが、あれも参照といえば参照だが、一般的に参照と呼ばれるものとは異なるものだ。

というのも、C言語のポインタというのはメモリアドレスを格納した整数値である。 つまりメモリの物理的な場所……と言いたいところだが、現代のOSにおいてはC言語のメモリ割当で得られるアドレスは仮想アドレスで、メモリの物理的な場所を直接表しているわけではない。 このため、C言語のポインタは「メモリの物理的なアドレスであるかのように扱うことができるアドレス整数値」である。

一方、多くの言語が提供する参照はそうではない。 言語処理系そのものはメモリにマッピングされたポインタを持っている場合が多い1が、言語の変数は言語処理系が管理している。 そして、参照というのは、言語処理系が管理している個々の変数のIDである。

参照(名詞)と参照(動詞)

Perlでは\nameの形で変数への参照を獲得することができる。 例えば次のコード:

#!/bin/perl

my $x = 100;
my $xr = \$x;

print $xr, "\n";

は次のような結果が得られる。

SCALAR(0x55c28394f8f8)

Perlでは$xrはリファレンス(reference)と呼び、$xrを参照することで得られる$xの実体をリファレント(referent)と呼んでいる。 また、リファレンスを参照してリファレントを得ることをデリファレンス(derefernce)と呼んでいる。

$xrはリファレンスであるから、printによって表示されるのはリファレンスであり、リファレントではない。 リファレンスをデリファレンスしてリファレントを得るには$$referenceのようにする。 ……というのは簡単すぎな話で、2番目の記号は変数の型に依存するのだが、ここはスカラー変数だから、この場合に限っては$$でよい。

#!/bin/perl

my $x = 100;
my $xr = \$x;

print $$xr, "\n";

今度は次の結果が得られる。

100

値を渡す

値を渡すのは簡単な話だが、Perlの書き方の問題がちょっとある。

#!/bin/perl

sub a($) {
  my $x = shift(@_);
  return $x * $x;
}

my $num = 8;

print(a($num), "\n");

これで64と出力される。 a($)shift(@_)が曲者だと思うが、a($)というのはスカラー変数をひとつ引数に取るという意味で、引数はリスト変数@_に格納されるため、これをshiftして最初の引数を$xに入れている。

さて、もっと明確にRubyで書こう。 値渡しというのは、要はそれが変数であるとして、変数の値を複製して割り当てることになる。 この「複製」は言語処理系によってコピーオンライトである可能性もあるが、それも含めての複製である。

Rubyは常に参照で渡すため、値渡しするには明示的な複製を行うことになる。 複製されたオブジェクトは元のオブジェクトとは別物であり、破壊的な変更を加えても元のオブジェクトには影響しない。

#!/bin/ruby

def add(x)
  x.push "C"
end

x = ["A", "B"]

add(x.clone)

p x

add["A", "B"]という配列を受け取り、pushするが、そもそも渡されている配列は複製されたもので、元の配列とは別物だ。 そのため、呼び出し元のp xは次の結果となる。

["A", "B"]

参照を渡す

さて、では参照を渡すようにして、「参照の値渡し」と言われていた挙動を実現させよう。

参照を渡すには単純に\をつければ良い。先程と結果は同じだが、コードは次のようになる。

#!/bin/perl

sub a($) {
  my $x = shift(@_);
  return $$x * $$x;
}

my $num = 8;

print(a(\$num), "\n");

では、値を書き換えてみよう。 もらっているのが参照なので話がややこしいが、仮に単純に値に書き換えるコードを書いたとする。

#!/bin/perl

sub a($) {
  my $x = shift(@_);
  $x = 500;
}

my $num = 100;
a(\$num);

print $num, "\n";

エラーにならないようにコードを書くよう非常に気を遣ったが、とりあえずこの結果は(500ではなく)100である。

Rubyでは特に気にしないとそのような結果になる。

#!/bin/ruby

def a x
  x = 500
end

num = 100
a num

puts num

これで100になる。

だが、そうなるのは、Rubyの代入の挙動の問題である。 ここでRubyで渡されている値を見てみよう。

#!/bin/ruby

def a x
  p x.object_id
  x = 500
end

num = 100
a num

puts num

結果は次のようになった。

201
201
100

これでxのIDは201であることが分かった。 そして、呼び出し元のnuma上のxは同じオブジェクトであることが分かる。 ではxに代入したあとはどうだろうか。 x = 500のあとにもp x.object_idを追加してみる。

201
201
1001
100

代入によってxは違うオブジェクトになっている。 だが、これは代入によって発生する挙動である。代入せずに破壊的操作をした場合、それは反映される。

#!/bin/ruby

def add(x)
  x.push "C"
end

x = ["A", "B"]

add(x)

p x

さきほどから#cloneを外しただけだが、結果は次のとおり、呼び出し元でも変更されている。

["A", "B", "C"]

代入で被参照オブジェクトを変更する

さて、何度も言っているようにこの動きは「何を渡したか」ではなく、「代入がどのような挙動を取るか」の話なのだが、まだ納得がいかない人のために続けよう。

Perlでは「リファレントを変更する代入」が可能である。 やり方は簡単で、代入左辺をデリファレンスすれば良い。

my $x = 100;
my $rx = \$x;
$$rx = 200;

print $x, "\n";

$xを直接変更することはしていないが、これでprint $xの結果は次のとおり

200

というわけで、別に関数になにか渡すまでもなくリファレントを変更することができた。 では、先程の「参照の値渡し」などと意味不明なことを言われた例を改良して、リファレントを書き換える(これが真の参照渡しだとか言われているやつ)にしてみよう。

#!/bin/perl

sub a($) {
  my $x = shift(@_);
  $$x = 500;
}

my $num = 100;
a(\$num);

print $num, "\n";

$x = 500;$$x = 500;にしただけだが、お望み通り結果はこうなる。

500

Rubyでもやりたいところだが、RubyにおいてIntegerは即値を持つため、Rubyのオブジェクト管理的に無理である。 だが、Rubyは一部オブジェクトに#replaceというメソッドがあり、それを使うと当該オブジェクトIDの実体を置き換えることが可能だ。 Stringにこのインスタンスメソッドがあるため、文字列にしてやってみよう。

#!/bin/ruby

def a x
  x.replace("500")
end

num = "100"
a num

puts num

結果は次のとおり

500

彼らは「Rubyは参照の値渡しだ」と主張するが、このようにRubyでも望む挙動を実現できた。

もう一度説明

参照に関わる処理というのは、言語によっては自動化されているのが普通だ。

基本的に、関数呼び出しの引数は「値で渡すか、参照で渡すか」だ。 言語によって自動的に参照として渡したり、値として渡したりする。 中には場合によってどちらか異なる(例えば値の型によって値で渡したり参照で渡したりする)というものもある。

同様に、代入も参照(動詞=Perlにおけるデリファレンス)を自動的に行うか否かの違いが言語によってある。 行わないのが普通であり、行うものに関しては被参照自体が書き換わる。

単にそれだけの話だ。 Perlはそれらを何も自動で行わず、代わりにユーザーが明示的に行うことができる。 そのため、ここで示したような挙動の違いを出すことができるわけだ。

では、なぜ一般的に代入で参照を行わないのが普通なのか。 それは、多くの言語が代入操作によって被参照を書き換えるべきでないと考えているからだ。 だから、単に「そうなっていない」だけでなく、そもそも被参照を書き換えることを許していない(そのような操作をする方法がない)という言語も多い。

Rubyに関しても基本的に被参照を書き換えることはできないが、例外的にいくつかのオブジェクトに関しては被参照を置き換える手段を提供している。それが#replaceメソッドだ。 このメソッドは使えるクラスが限定されているだけでなく、同一クラスのインスタンスにしか置き換えられないという制約もついている。 Rubyとしては、被参照の書き換えはそのように限定的な場合を除けば行うべきでないと考えているわけだ。

ではまとめ。

  • 「参照の値渡し」などというものはない
  • 代入で被参照が書き換わるかどうかは、言語の代入の挙動の違いである