引数の参照渡し(変数渡し)なのか単なるオブジェクトへの参照なのか、それが問題

プログラミングの勉強をしていると、異なる話題なのに似たような名前が付けられていて混乱することがある。

例えば変数における「参照型」と関数/プロシージャ/サブルーチン/メソッド等々の引数における「参照渡し」は異なる概念なのだが、どちらも「参照」という名前が付いている為にこれのように混乱してしまうことがある。

まあこの辺りは知らないと色々と混乱させられる話ではある。

分からない人はVBScriptC#の言語本体の部分を勉強すると理解できるかもしれない。どちらも、

  • 変数(というかデータ型?)に値型と参照型がある。
  • メソッドの引数に値渡しと参照渡し(変数渡し)がある。

といった言語仕様だ。

ちなみに私は基本的にCプログラマなスタンスの人なのだが、C言語の仕様としては、

  • 変数に値型(普通の変数)と参照型(の原始的な実装――ポインタ)がある。
  • 関数の引数は値渡しのみ。

といった特徴があるので、実はRubyの変数とメソッドの引数をめぐる話は結構理解しやすかったりする。

どういうことか? Rubyのメソッドの引数は値渡しで、Rubyの変数は全てオブジェクトを指し示すポインタに似た何かだと考えればよいのだ*1

こんなサンプルコードをでっち上げてみた。

#!/usr/bin/ruby -w

def test1(s)
  s = "bar"
end

def test2(s)
  s.reverse!
end

str = "foo"
test1(str); p str   # "foo" と表示される
test2(str); p str   # "oof" と表示される

test1の「s = "bar"」は、変数sがポイントしているオブジェクトを変更しているが、s自体はstrの値(どのオブジェクトをポイントしているかという情報)を値渡しでコピーしたものにすぎない。strがポイントしているオブジェクト本体は変更されていないので、「p str」で表示されるのは「"foo"」のままだ。

# 矢印はどのオブジェクトを参照しているかを表す。
# {}はオブジェクト。識別用のid(値は適当)と実際の値を格納するvalを持つ。
# (Ruby本来のオブジェクトではなく、便宜上用意した架空のオブジェクト)

# s = "bar" を実行する直前。
# str も s も同じオブジェクトを指し示している。
str --+--> { id: 1, val: "foo" }
      |
s ----+

# s = "bar" を実行した直後。
# s が違うオブジェクトを指し示すようになった。
str -----> { id: 1, val: "foo" }

s -------> { id: 2, val: "bar" }

test2の「s.reverse!」は、変数sがポイントしているオブジェクトの内部状態を変更している。sがポイントしているオブジェクトとstrがポイントしているオブジェクトは同じなので、「p str」での表示結果にも変更が反映される。

# s.reverse! を実行する直前。
# str も s も同じオブジェクトを指し示している。
str --+--> { id: 1, val: "foo" }
      |
s ----+

# s.reverse! を実行した直後。
# str も s も指し示すオブジェクトは変わっていないが、
# 参照先のオブジェクトを破壊的操作した為、内部状態が変化している。
str --+--> { id: 1, val: "oof" }
      |
s ----+

このRubyスクリプトと似たような内容をC++(better C)で書くとこうなる。

#include <algorithm>
#include <iostream>
#include <string>

static void test1(std::string *s)
{
	s = &std::string("bar");
}

static void test2(std::string *s)
{
	std::reverse(s->begin(), s->end());
}

int main()
{
	using std::string;
	using std::cout;
	using std::endl;

	// new に失敗する可能性はあえて無視
	string *str = new string("foo");
	test1(str); cout << *str << endl;   // "foo" と表示される
	test2(str); cout << *str << endl;   // "oof" と表示される
	delete str;

	return 0;
}

C++の参照ではなくポインタを使っている点に注意。これは意図して書いている。

test1の引数sはポインタだ。test1の中でsの値(string("foo")?へのアドレス)を変更しているが、sが元々指していたオブジェクト自体を変更した訳ではない。strがポイントしているオブジェクトは何も変更されていないので、「cout << *str << endl」で表示されるのは「"foo"」のままだ。

その代わりtest2のようにポイントしているオブジェクトの内部状態を変更してしまえば、「cout << *str << endl」での表示結果も変更が反映されたものになる。

(構文から読み取れるニュアンスとしては)Rubyの変数は参照型だ。オブジェクト本体ではなく、オブジェクトを指し示す何かだ。C言語に例えるとポインタに近い。但しC言語のポインタと異なりインクリメント/デクリメントやアドレス値への演算はできないし、構文上ポインタであることを気にする必要はない*2

Rubyにおける変数への代入は、単に指し示すオブジェクトを変更しているに過ぎない。C言語で言うならポインタの値(アドレス値)を変えているようなものだ。

そしてRubyのメソッドの引数は値渡しだ。C言語に例えるとポインタの値(つまりアドレス値)を関数の引数にコピーしている訳だ。ポイント先のオブジェクトを直接弄くれば、そのオブジェクトを指し示している他の変数にも影響が出る*3。しかしメソッド内で引数に他の値を代入してもそのポインタが指し示すオブジェクトが変わるだけだ。引数に指定した元の変数が指し示す先が変わるわけではないし、ましてや指し示すオブジェクトの内部状態が変化するわけでもない。

変数の参照型と引数の参照渡しは、どちらも「参照」という言葉を使っているが、全く別物だ。例えるならヌルポインタとヌル文字ぐらいに違う。

最初にも書いたが、この点が曖昧な人はVBScriptあたりを勉強してみると良いと思う*4VBScriptはプロシージャの引数にByVal(値渡し)ないしByRef(参照渡し)キーワードを指定できる*5。と同時にバリアント型の変数の中身がオブジェクト型(オブジェクトを参照するデータ型)なのかそれ以外なのかによって代入時にSetが必要/不要云々の話がある訳で、引数絡みで都合 (2 * 2) == 4 パターンが存在するという、慣れないと混乱しそうな状況を楽しむことができる。

  • オブジェクト型ではない値を値渡し
  • オブジェクト型の値を値渡し
  • オブジェクト型ではない値を参照渡し
  • オブジェクト型の値を参照渡し

*1:少なくとも変数については『初めてのRuby』初版のP.97に「変数が保持するものはオブジェクトへの参照です。オブジェクトそのもの、値そのものが格納されるわけではありません。これが何を意味するかというと、複数の変数が同じオブジェクトを参照している可能性もあるということです。」と書かれている。

*2:CやC++でしばしば必要となるメモリの手動解放も不要だ。

*3:CやC++のようにオブジェクト自体をコピーする言語に慣れている人がRubyで躓く要因の1つだろう。

*4:特にWindows使いの人ならWindows 2000以降でデフォルトで使える。PerlRubyほどではないけどWindows用のLL代わりにもなる。

*5:省略すると参照渡しになる。