連載

C#入門

第6 変数を極めろ

(株)ピーデー
川俣 晶
2001/06/12


第4の答

 さて、まだまだ前半戦が終わったばかりだ。気を抜かずに先に進もう。第4の答は、42〜47行目の処理によって出力される。

38: testClass2a   = testClass1a;
    ・・・

42: TestClass testClass1b,testClass2b;
43: testClass1b   = new TestClass();
44: testClass1b.n = 123;
45: testClass2b   = (TestClass)testClass1b.Clone();
46: testClass1b.n = 456;
47: Console.WriteLine( "Answer4 testClass1b.n={0}, testClass2b.n={1}", testClass1b.n, testClass2b.n );
第4の問題部分

 第3の答と第4の答の差もわずかである。例によって変数名の違いは機能の本質とは関係がない。変数名以外の違いは、38行目と45行目の間に見られる代入文の相違だけと言ってよい。第3の答(38行目)ではただ単に代入だけがなされていたが、第4の答(45行目)では、Clone( )というメソッドの呼び出しが増えている。

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

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

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

 45行目のClose( )メソッドで作られた複製インスタンスは、TestClass型のインスタンスなのだが、戻り値がobject型となっている。そこで、object型からTestClass型に変換するために、データ型の変換を行っている。それが、testClass1b.Close( )の手前に書かれた“(TestClass)”の部分である。括弧でくくったデータ型名を式や値の手前に付けると、それはそのデータ型への変換を指示する意味になる。このような構文を「キャスト」という。

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

第5の答

 第5の答は、49〜53行目の処理によって出力される。これは第3の答を、クラスではなくStructsで書き直したものである(Structsについては第5回「C#のデータ型」で詳しく解説している)。

23: testInt2 = testInt1;
    ・・・
45: testClass2b   = (TestClass)testClass1b.Clone();
    ・・・
49: TestStructs testStructs1,testStructs2;
50: testStructs1.n = 123;
51: testStructs2   = testStructs1;
52: testStructs1.n = 456;
53: Console.WriteLine( "Answer5 testStructs1.n={0}, testStructs2.n={1}", testStructs1.n, testStructs2.n );
第5の問題部分

 Structs型の変数は、それ自身がインスタンスを中に持っているようなものなので、改めてnewを用いる必要はない。このようにStructsは値型であって、クラスのように参照型ではない。そのため、代入は丸ごと中身がコピーされる。つまり、51行目の代入文は、機能的に45行目にあるようなClone( )メソッドの機能に相当する処理が行われていると考えてよい。もっと正確に表現するなら、第1の答の23行目の代入と機能的にまったく同じと理解する方がよいだろう。実に簡単かつ単純である。

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

第6の答

 さて、前回紹介したボクシング機能を使ったのが第6の答である。第6の答は、55〜59行目の処理によって出力される。

21: int testInt1,testInt2;
    ・・・
38: testClass2a   = testClass1a;
    ・・・

55: object testObject1,testObject2;
56: testObject1 = 123;
57: testObject2 = testObject1;
58: testObject1 = 456;
59: Console.WriteLine( "Answer6 testObject1={0}, testObject2={1}", testObject1, testObject2 );
第6の問題部分

 第6の答は、第1の答とそっくりである。変数名の違いは例によって機能には関係ない。本質的な相違は、第1の答が21行目でint型の変数を宣言しているのに対して、第6の答では55行目でobject型の変数を宣言していることである。object型は、すでに述べたとおりSystem.Objectの別名で、すべてのクラスのスーパー・クラスとなる究極の基本クラスである。つまり、object型は参照型である。

 ここまで読んできた読者であれば、この話を聞いてピピッと来ると思う。object型は参照型であり、int型は値型なので、第1の答と第6の答では、表面的に似ていても挙動は違うはずである。まさにそのとおりで、表面的にソースコードが似ているだけで、処理される内容はまったく似てもにつかない。

 次にピピッと来るのは、「参照型なのにnewがない」と言うことだろう。このソースコードが実行可能だとすれば、インスタンスを作成するための機能がどこかになければならないはずだ。newキーワードなしで動作しているとすれば、55〜59行目のどこかに、newに相当する機能が隠れているはずである。問題は、それが何個かと言うことだ。1個なら第3の答に相当する結果になるだろうし、2個なら第2の答に相当する結果になるのではないか? と予測することができる。

 では実際に何が起きているのかを順番に見てみよう。ボクシングは暗黙のうちに実行される処理が多いので、1行ずつ見ていきたいと思う。まず56行目では、int型整数の123という値をobject型の変数に入れようとしている。このような値型を参照型として使おうとする状況があると、コンパイラはボクシング処理を自動的に挿入する。その結果、整数型の変数を包み込むクラスが見えないところで準備され、そのインスタンスが自動的に作成され、ここでは123という値が書き込まれる。そして、そのインスタンスへの参照が変数testObject1に代入されるのである。つまり、56行目でnew相当の機能が1回実行されたのである。さて、次の57行目で行われている代入は、参照型と参照型の代入であり、ボクシングのような機能は入らない。つまり、第3の答の38行目とまったく同じ機能である。この代入が行われた時点で、testObject1とtestObject2は同じインスタンスを指し示していることになる。このまま終われば、第3の答と同じ結果になるのだが、まだまだ逆転の余地がある。

 第6の答を理解する上で最大のポイントは58行目である。もちろん、58行目に書かれた内容は56行目と数値の値以外は同じであり、まったく同じことが行われる。こう書くと簡単だが、これによって大逆転が起こる。つまり、第3の答とは同じ結果にならないのである。58行目では、456という整数値をボクシングして、整数値を包み込むクラスのインスタンスを作成する。つまり、newに相当する機能が実行されるのである。その結果、インスタンスは2つに増え、それぞれが別々の値を保持することになる。そして、2つの変数がそれぞれ別々のインスタンスを参照しているので、出力結果の数値は、456と123となる。結果だけ見ると、第1〜2、第4〜5の答と同じに見えるが挙動はまったく異なっていることに注意が必要だ。

まとめ

 念のために実際に実行した結果の画像ファイルを以下に示す。

サンプル・プログラムの実行結果
参照型と値型、クラスとStructs、ボクシングなどの型や機能によって、似通ったコードでも実行結果はまったく異なることが分かる。C#プログラマは、こうしたクラスや変数などの内部的な挙動を十分に理解しておく必要がある。

 表面的には、第3の答以外はすべて同じに見えるが、内部的な動作はまったく異なっている。最終的に456と123という値に落ち着くにせよ、落ち着くまでのプロセスが異なっているのである。今回のプログラムは分かりやすさを尊重するために、極めてシンプルに仕上げたので、これぐらいなら何となく書いても問題ないと思われたかもしれない。しかし、実際に開発の現場で書かれるプログラムは、複雑に込み入ったものにならざるを得ない。そこで発生するトラブルを解析して、問題点を突き止めるには、今回述べたような内部での正確なデータの挙動を知っている必要がある。そして何より、値型と参照型の区別を正しく扱えるようになれば、より少ないメモリでより素早く実行するプログラムが作成できるのである。

 次回は、データの変換などについて説明したいと考えている。今回少しだけ説明したキャストも、もっと詳しく説明したい。

 それでは次回もLet's See Sharp!End of Article


 INDEX
  C#入門 第6回 変数を極めろ
    1.理解度を確認しよう
    2.第1〜第3の答
  3.第4〜第6の答
 
更新履歴
【2001.6.13】本ページに掲載したサンプル・プログラムのソース・コード内の59行目、およびサンプル・プログラムの実行結果画面に誤りがありました。お詫びし、訂正させていただきます。
 
「C#入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間