連載
» 2002年09月25日 00時00分 公開

連載 改訂版 C#入門:第6章 変数と値型・参照型 (2/3)

[川俣晶(http://www.autumn.org/),(株)ピーデー(http://www.piedey.co.jp/)]

6-4 第3行目の出力の答え

 ここまできて、何だ簡単じゃないか、と思った人がいるかもしれない。だが、そろそろ、理解が不完全だとミスしやすい世界に入って行くので十分注意していただきたい。

 32: testClass2.n = testClass1.n;
      ・・・
 36: TestClass testClass1a,testClass2a;
 37: testClass1a = new TestClass();
 38: testClass1a.n = 123;
 39: testClass2a = testClass1a;
 40: testClass1a.n = 456;
 41: Console.WriteLine( "Answer3 testClass1a.n={0}, testClass2a.n={1}", testClass1a.n, testClass2a.n );

List 6-1 第3行目の出力部分

 「第2行目の出力」と「第3行目の出力」の違いは、表面的には小さなものである。変数名のつづりを変えてあるのは機能とは何の関係もない変更である。newが1回しか行われていないが、39行目で代入される変数にあらかじめnewを用いてインスタンスを作成して代入しておく必要はないということから、これが妥当であることは推測できるだろう。32行目と39行目の代入文がわずかに違うところが、最大の相違点といえる。「第2行目の出力」のほう(32行目)では、インスタンスの中にある変数nの値をコピーしているが、「第3行目の出力」となるほう(39行目)では、インスタンス全体を代入しているように見える。

 一見、インスタンス全体が代入されるなら、中身の値も同時に代入されると考えられるので、「第3行目の出力」も「第2行目の出力」とまったく同じ結果になると考えてしまいたくなる。ところが、32行目と39行目の代入文のほんの些細な記述の差が、結果を驚くほど変えてしまうのである。

 先に結果を書くと、41行目による「第3行目の出力」は、どちらも「456」という数値になる。「第2行目の出力」では、「456」と「123」だったので、結果が違う。

 いったい「第2行目の出力」と「第3行目の出力」では何が違うのか? C#プログラマーとして、まず最初にピンとこなければならないのは、「第2行目の出力」ではnewを2回使っているのに、「第3行目の出力」では1回しか使っていないという事実である。これはインスタンスが1個しか作られていないということを意味する。ということは、クラス内で宣言された変数nも1個しか作られていないことになる。その結果、確かにtestClass1aとtestClass2aという2つの変数が存在し、それぞれを経由してインスタンスを利用できるものの、実は、どちらの変数を経由しても、実際に使われるのはたった1個のインスタンスだったのだ。

 代入してもたった1つの実体を指し示し続けるというのが、参照型の大きな特徴である。クラスは参照型なので、インスタンスを指し示す変数を代入しても、ただ単に別名ができるだけで、データ本体の数が増えるわけではない。その結果、インスタンスの中の変数に値を代入すると、そのインスタンスを参照するすべての変数から見える値が変わるのである。具体的にいえば、40行目の代入によって、testClass1a.nの値が変わるのはもちろん、一見関係なさそうに見えるtestClass2a.nの値も変わってしまうのである。

 このように、代入しても代入しても、もとの変数との縁が切れないのが参照型であるが、悪いことばかりではない。参照型とは、いい換えれば、代入されても複製を作らないで場所だけ伝達するデータ型ということになる。複製を作らないということは、巨大なデータも素早く伝達させられるということである。

 この違いを例え話で表現してみよう。巨大なファイルをたくさんの人に電子メールを用いて渡す必要が生じたとしよう。その際、方法は2つある。1つは、ストレートに巨大ファイルを添付ファイルとして送信する方法である。しかし、ただでさえ巨大なファイルを大人数に送るとなると、これはシステムに大きな負荷がかかり時間もかかる。もう1つの方法は、そのファイルの入手先を記したURLだけを電子メールに書いて送る方法である。この方法ならシステムへの負荷はたいしたことはない。短いテキストを送るだけの手間だからだ。つまり、ファイルそのものを添付して送信するのが値型、URLだけを書いて送るのが参照型、と考えると分かりやすいだろう。

6-5 第4行目の出力の答え

  8: public object Clone()
  9: {
 10:   return MemberwiseClone();
 11: }
      ・・・
 39: testClass2a = testClass1a;
      ・・・
 43: TestClass testClass1b,testClass2b;
 44: testClass1b = new TestClass();
 45: testClass1b.n = 123;
 46: testClass2b = (TestClass)testClass1b.Clone();
 47: testClass1b.n = 456;
 48: Console.WriteLine( "Answer4 testClass1b.n={0}, testClass2b.n={1}", testClass1b.n, testClass2b.n );

List 6-1 第4行目の出力部分

 さて、まだまだ前半戦が終わったばかりだ。気は抜けない。「第4行目の出力」は、43〜48行目の処理によって出力される。「第3行目の出力」と「第4行目の出力」の答えの差もわずかである。例によって変数名の違いは機能の本質とは関係ない。そのほかの相違点は、39行目と46行目の間に見られる代入文の相違だけといってよい。「第3行目の出力」のほう(39行目)ではただ単に代入だけがなされていたが、「第4行目の出力」のほう(46行目)では、Clone()というメソッドの呼び出しが増えている。

 ここで、newの数に着目して、int(整数)が1個しかない以上インスタンスは1個しか作られていないはずだ、だから、「第3行目の出力」と同じ結果になる、と考えた人もいるだろう。だが、それは正しい答えではない。実は、newキーワードを使う以外にもインスタンスを作成する機能が働く場合があるのだ。ここでは、Clone()というメソッドが新しいインスタンスを作るという機能を含んでいるのである。

 Clone()メソッドは、ブラックボックスではなく、このソース・コード上に記述されたメソッドである。8〜11行目がその個所である。ここで処理されているのは、MemberwiseClone()というメソッドの呼び出しである。これはすでに説明したとおり、インスタンスのコピーを作る機能を持っている。もっと分かりやすくいえば、自分自身と同じクラスのインスタンスをnewして、自分自身の中に持っているデータをそっくり丸ごとnewしたインスタンスにコピーしてやる、という機能を持つ。

 その結果、「第4行目の出力」のほうは、表面的にはnewキーワードが1個しか使われていないものの、実際には2個のインスタンスを作成していることになる。

 46行目のClone()メソッドで作られた複製インスタンスは、TestClass型のインスタンスなのだが、戻り値がobject型となっている。そこで、object型からTestClass型に変換するために、TestClass型へキャストしている(キャストについては次章で詳しく取り上げる)。

 さて、46行目を実行終了した時点で、testClass1b.nとtestClass2b.nはどちらも「123」という値を持っている。しかし、それらは別々のインスタンスを指し示している。そのため、47行目でtestClass1b.nの値を変更しても、testClass2b.nの値が変わるわけではない。その結果、出力される値は、「456」と「123」となる。

6-6 第5行目の出力の答え

 24: testInt2 = testInt1;
      ・・・
 46: testClass2b = (TestClass)testClass1b.Clone();
      ・・・
 50: TestStruct testStruct1,testStruct2;
 51: testStruct1.n = 123;
 52: testStruct2 = testStruct1;
 53: testStruct1.n = 456;
 54: Console.WriteLine( "Answer5 testStruct1.n={0}, testStruct2.n ={1}", testStruct1.n, testStruct2.n );

List 6-1 第5行目の出力部分

 「第5行目の出力」は、50〜54行目の処理によって出力される。これは「第3行目の出力」を、クラスではなく構造体で書き直したものである。構造体型の変数は、それ自身がインスタンスを中に持っているようなものなので、あらためてnewを用いる必要はない。もちろん、構造体は値型であって、クラスのように参照型ではない。そのため、代入は丸ごと中身がコピーされる。つまり、51行目の代入文は、機能的に46行目にあるようなClone()メソッドの機能に相当する処理が行われていると考えてよい。もっと正確に表現するなら、「第1行目の出力」のほうの24行目の代入と機能的にまったく同じと理解するほうがよいだろう。実に簡単かつ単純である。

 ただし、簡単かつ単純であるがゆえに、データ量が増えると無駄な処理時間が増える危険もある。すでに述べたとおり、その点に注意が必要である。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。