データを変数に格納するときには、値型と参照型の違いが挙動の違いとして大きく現れる。今回は、この違いを実例を通して確認してみよう。
本記事は、(株)技術評論社が発行する書籍『新プログラミング環境 C#がわかる+使える』から許可を得て転載したものです。同書籍に関する詳しい情報については、本記事の最後に掲載しています。
前章ではデータ型について説明したが、データは主に変数に格納して利用する。変数に格納するときには、値型と参照型の違いが挙動の違いとして大きく現れる。本章では、変数に格納したときの値型と参照型の挙動の違いを実例を通して確認してみよう。
C#でのデータの取り扱いには、分かりにくい面がある。もちろん、無意味に分かりにくいわけではなく、必要だから導入された機能が分かりにくさという副作用を起こしていると考えるべきだろう。もちろん、ほかのプログラミング言語にも、分かりにくい点はある。Visual Basicであれば、データ型の自動変換という初心者泣かせの機能があるし、Cならポインタがハードルになるかもしれない。C#では、値型と参照型の違いを理解することが、そのような位置付けになるだろう。
そのようなわけで、本章のテーマは変数なのだが、値型と参照型で異なる変数の働きを重点的に見てみよう。変数に関しては特に説明は行わない。その代わり、読者の理解度を試す意味で、1個のサンプルプログラムを示して、その結果がどのようになるか、ソース・コードから予測していただこう。
サンプル・ソースはList 6-1である。
1: using System;
2:
3: namespace Sample001
4: {
5: public class TestClass : ICloneable
6: {
7: public int n;
8: public object Clone()
9: {
10: return MemberwiseClone();
11: }
12: }
13: public struct TestStruct
14: {
15: public int n;
16: }
17: class Class1
18: {
19: [STAThread]
20: static void Main(string[] args)
21: {
22: int testInt1,testInt2;
23: testInt1 = 123;
24: testInt2 = testInt1;
25: testInt1 = 456;
26: Console.WriteLine( "Answer1 testInt1={0}, testInt2={1}", testInt1, testInt2 );
27:
28: TestClass testClass1,testClass2;
29: testClass1 = new TestClass();
30: testClass2 = new TestClass();
31: testClass1.n = 123;
32: testClass2.n = testClass1.n;
33: testClass1.n = 456;
34: Console.WriteLine( "Answer2 testClass1.n={0}, testClass2.n={1}", testClass1.n, testClass2.n );
35:
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 );
42:
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 );
49:
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 );
55:
56: object testObject1,testObject2;
57: testObject1 = 123;
58: testObject2 = testObject1;
59: testObject1 = 456;
60: Console.WriteLine( "Answer6 testObject1={0}, testObject2={1}", testObject1, testObject2 );
61: }
62: }
63: }
このプログラムを実行すると、コンソールに6行の文字列を出力する。1行の文字列には、2個の数値が表示される。つまり、計12個の数値が画面に表示される。この12個の数値を出力行ごとに、上記のソース・コードから予測していただきたい。
ちなみに、本質とは関係ない部分でいくつか引っかかるところがあると思うので、それを先に解説しておく。5行目で、TestClassという名前のクラスを宣言しているその後ろにICloneableというキーワードが見えるが、これは、8行目でClone()というメソッドを実装するために付け加えたものである。とはいえ、実はこのプログラムだけに関していえば、ICloneableというインターフェイスの名前をここに書かなくても問題はない。なお、このClone()というメソッドは、46行目で明示的に呼び出されている以外、ほかの個所ではいっさい動作に関与することはないことをいっておこう。10行目でMemberwiseCloneというメソッドを呼び出しているが、これはobject(System.Object)クラスのメンバで、オブジェクト全体の浅いコピーを作成する機能を持ったものである。浅いコピーとは、そのオブジェクトだけの複製を作ることを意味する。逆に、深いコピーとは、オブジェクトから参照されるオブジェクトも含めたコピーを作成することだが、ここではそこまでの知識は必要ない。ともかく、MemberwiseClone()メソッドは、まったく同じ内容を持った別のオブジェクトを作る、と理解してほしい。もう1つ余談を書くなら、5行目のクラス宣言で、スーパークラスの名前は書かれていないが、そのような場合はobjectクラスが自動的に仮定される。そのため、objectクラスのメソッドを呼び出せるというわけである。
では、しばし考えていただきたい。
22: int testInt1,testInt2;
23: testInt1 = 123;
24: testInt2 = testInt1;
25: testInt1 = 456;
26: Console.WriteLine( "Answer1 testInt1={0}, testInt2={1}", testInt1, testInt2 );
まず、List 6-1の26行目の出力を考えてみよう。22行目から26行目までの流れを追えば、すべて理解できる。22行目から読み取れることは、int型、つまり整数型の変数を2個宣言しているということだ。名前は、testInt1とtestInt2である。23行目でtestInt1には「123」という値が代入されている。そして、24行目で、testInt1の値をtestInt2に代入しているので、この時点で、2つの変数はどちらも「123」という値を持つ。25行目ではtestInt1に「456」が代入されているので、この時点では、testInt1は「456」、testInt2は「123」となる。26行目の結果を見ればそのとおりであることが分かるだろう。これには謎の部分はまったくない。Visual Basic経験者もC経験者も納得する結果だと思う。これが最もシンプルな値型である。
28: TestClass testClass1,testClass2;
29: testClass1 = new TestClass();
30: testClass2 = new TestClass();
31: testClass1.n = 123;
32: testClass2.n = testClass1.n;
33: testClass1.n = 456;
34: Console.WriteLine( "Answer2 testClass1.n={0}, testClass2.n={1}", testClass1.n, testClass2.n );
28〜34行目の結果が出力の2行目に出てくる。リストを見て分かるとおり、今度はintの代わりにクラスを使っている。といっても、TestClassクラスの宣言を見て分かるとおり、このクラスが中に持つのは、やはりint型の整数型変数である。つまり、「第1行目の出力」と「第2行目の出力」の差は、クラスの外の整数型変数を使うか、中の整数型変数を使うかという違いということができる。
クラスを使うため、28行目の変数宣言では、intではなくクラス名であるTestClassが記述されている。変数名としては「testClass1,testClass2」を付けている。29〜30行目のコードは「第1行目の出力」のほうにはないものだ。これは、クラスが参照型であり、明示的に作成しなければならないために必要とされるものである。newキーワードを使って、インスタンスを作成して、それぞれ変数に代入している。
インスタンスの中の変数を参照するには、インスタンスを格納した変数名の後にピリオド記号を置き、そしてクラス内の変数の名前を書く。31〜33行目の動作は、「第1行目の出力」のほうの23〜25行目とまったく同じである。結局のところ、「第1行目の出力」と「第2行目の出力」の違いは、int(整数)型変数がクラスの中にあるか外にあるかの差でしかない。置かれた場所によってint(整数)型変数の挙動が変わるわけではないので、結果は「第1行目の出力」と同じである。
Copyright© Digital Advantage Corp. All Rights Reserved.