連載

One Point .NET

エピソード 2 クローンの攻撃

吉松 史彰
2002/09/20


パダワン1号、.NETプログラミングに挑戦する

 ここは.NET Frameworkプログラマー評議会。銀河、もといIT業界の平和と発展のために元老院、もといマイクロソフト(後に暗黒の皇帝が支配する悪の帝国に変わる……かどうかは定かではない)の活動を支援する平和の守護者、.NET Frameworkプログラマーたちを仕切っている偉いプログラマーの集団である。ある日、次期評議会メンバーとも目される中堅.NET Frameworkマスターは、自分が育てている若きパダワン(弟子)から修行の成果を見せられた。

「マスター、このプログラムを見てください。私もいまや.NET Frameworkの騎士になれるくらいまで成長しました」

struct ValueType {
  internal int x;
}

class Target {
  internal ValueType Value;
}

class App {
  static void Main() {
    Target t = new Target();
    ValueType value;

    value.x = 100;
    t.Value = value;
    System.Console.WriteLine(t.Value.x);

    t.Value.x = 200;
    System.Console.WriteLine(t.Value.x);
  }
}
「マスター、このプログラムを見てください。私もいまや.NET Frameworkの騎士になれるくらいまで成長しました」

「どうです。外部のアセンブリに見られないように、フィールドにはinternalアクセス修飾子も付けました。これでカプセル化も完璧です!」

「若きパダワンよ」

 .NET Frameworkマスターはおもむろに口を開いた。

「お前はまだ何も学んではおらん。暗黒面は己のうちに存在するのだ。たとえinternalとはいえ、内部のフィールドを外部のクラスに公開するなどもってのほかだ。.NET Frameworkにはプロパティという構造があるのだ。プロパティとともにあらんことを」

「はい、マスター」

パダワン1号、プロパティを利用する

 パダワンはしぶしぶながらも自分のコードをマスターのいうとおり書き換えた。

struct ValueType {
  internal int x;
}

class Target {
  private ValueType value;
  internal ValueType Value {
    get { return value; }
    set { this.value = value; }
  }

}

class App {
  static void Main() {
    Target t = new Target();
    ValueType value;

    value.x = 100;
    t.Value = value;
    System.Console.WriteLine(t.Value.x);

    t.Value.x = 200;
    System.Console.WriteLine(t.Value.x);
  }
}
パダワンはしぶしぶながらも自分のコードをマスターのいうとおり書き換えた

「マスター!おっしゃるとおりにコーディングしてみましたが、コンパイルができません!」

C:\Padawan>csc clone.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

clone.cs(22,5): error CS1612: 変数ではないため、'Target.Value' の戻り値を変更できません。
「マスター!おっしゃるとおりにコーディングしてみましたが、コンパイルができません!」

「むぅ。暗黒面は見破るのが難しい……(Hmm. Hard to see, the dark side is.)」

 バタッ。

「マスター!」

パダワン1号、値型を理解する

「Hmm. Hard to see, the value type is.」

「与田さん!」

 与田さん(仮名)は.NET Frameworkマスター中のマスターにして評議会の代表者である。327歳にして脂の乗り切った中年技術者である彼は、マスターが見破れなかったこの問題もたちどころに解決した。

「この問題は、値型とC#のプロパティの両方を深く理解していなければ解けないのじゃ」

 C#やVB .NETなどの言語、または.NET Frameworkそのものについての書籍や雑誌記事などを読むと、必ず登場しているのが値型と参照型の違いの解説である。たいてい次のような解説がなされている。

解説:値型と参照型の違い

例えば次のコードを考えてみよう。

int x = 1;

 xという名前のint型の変数に、1という値を代入している。int型は値型なので、int型のxは値を保持する。

 一方、次のコードを考えてみよう。

class RefType {
  internal int value;
}
...
RefType X = new RefType();
X.value = 1;

 Xという名前の変数に、新しく作ったRefType型のインスタンスを代入している。C#のclassは参照型なので、classであるRefType型の変数Xは参照を保持する。

 この違いは、次のコードを見てみると顕著に表れる。

1: int y = x;
2: y = 327;
// xの値は?

 このコードは、int型をもう1つ宣言して、そこにxが保持している値を代入している。xは1という値を保持しているから、1行目ではyも1になる。2行目で、yの値を327に変えている。ここで、もちろんyは327だが、ではxの値は何だろうか? 答えは1のままだ。yはxが持っていた値をコピーして別の場所に持っただけなので、yの値が変わっても、オリジナルのxには何の影響もないわけだ。

 一方、次のコードはどうだろう。

1: RefType Y = X;
2: Y.value = 327;
// X.valueの値は?

 intの例と同様に、同じ型の別の変数を宣言して、そこにXが保持している値を代入している。Xは前に作ったインスタンスへの参照を保持しているので、1行目ではYにはXが持っている参照のコピーが渡されることになる。結果、YもXと同じ参照を持つようになった。2行目でYが指すインスタンスのvalueの値を327に変えている。ここで、Y.valueはもちろん327だが、ではX.valueの値は何だろうか? 答えは327だ。

 参照を別の変数に渡しても、参照がコピーされるだけで、参照先はそのまま何も変わらずに残っている。そのため、コピー先とオリジナルのどちらから参照しても同じものを見ていることになるのだから、どちらかが加えた変更はもう一方でも有効になるのだ。これが参照型と値型の違いだ。

 この解説は何も間違ってはいない。だが、この解説からパダワン1号(とマスター)の間違いを理解できる人は少ないだろう。上記のパダワン1号のコードがコンパイル・エラーになるのは、まさにこの「値型を他者に渡すと、値型のクローンが作成される」という仕様が理由なのだ。

 実際にコンパイル・エラーになっているのは、最後から4行目の次のコードだ。

t.Value.x = 200;

 このコードが行おうとしているのは、次の図のような作業であろう。

「t.Value.x = 200;」の意図

 しかしこのコードをコンパイルすると、C#コンパイラは次のようなCIL(Common Intermediate Language)コードを出力する(実際にはエラーになるので出力されない)。

.locals init (class Target t, valuetype ValueType BUF)
// (省略)
IL01: callvirt instance valuetype ValueType Target::get_Value()
IL02: stloc.1
IL03: ldloca.s   BUF
IL04: ldc.i4.s   200
IL05: stfld      int32 ValueType::x

 1行目は、C#のコードでは「t.Value」の部分が展開されたコードだ。プロパティはメソッドの1種であり、C#の構文でプロパティを実装すると、実際にはget_プロパティ名とset_プロパティ名の対が作成される。2行目では1行目の実行結果として返されたValueType型の値を、1番のローカル変数(BUF)に格納している。3行目では、いま格納したモノへの参照を再び取り出し、4行目では200という定数をロードして、5行目で、3行目でロードした場所に4行目の定数を格納している。

 さて、ここで問題になるのが1行目で取得されるValueType型の値だ。上記の値型の説明を読むと、値型の値を別のものに代入すると値のクローンが作成されることになっている。つまり、1行目のコード、あるいはC#の「t.Value」を実行した結果出力されるものは、Target型(クラス)の実体がprivateに保持しているvalueフィールドのクローンなのだ。このクローンは、特別な変数に代入されているわけではなく、あくまでも「t.Value.x」という1つの式の途中の状態としてメモリに保持される(下図)。

「t.Value.x = 200;」の実際の動作

 そして、後半の「.x = 200」の部分が実行されるわけだが、「.x」によって参照されるのは、「t.Value」を実行して作成されたクローンの値である。つまり、「t.Value.x」が指している値は、Target型(クラス)の実体がprivateに保持しているvalueフィールド(ValueType型)のフィールド「x」ではなく、あくまでも「t.Value」の段階で作られたクローンが持っている「x」フィールドなのである。「 = 200」を実行して200が代入されるのは、メモリ上に一時的に保持されたクローンの領域なのだ(上図)。

 このような状態になってしまうと、xに200を代入しようが327を代入しようが、その値には何の意味もないことになる。メモリ上に一時的に確保された領域の値を変更しても、それを変数などで確保していない以上、そこにアクセスする手段がないからだ。従って、「t.Value.x = 200」という処理は、後にも先にも何も残らない、単なるゴミの操作になってしまうのだ。パダワン1号が意図していた内容は、実はこのコードでは実現できないのである。これはバグではない。値型の特徴なのだ。これらのコードは、別にエラーにしなくても何も発生しないだけで害はないが、C#コンパイラはこのような無駄な処理による混乱を防ぐために、親切心でコンパイル・エラーを出力してくれているのだ。

暗黒面はどこにでも存在する

 この問題は上記の場面以外でも見られる。例えば次のコードは上記と同じ理由でコンパイルできない。

struct ValueType {
  internal int x;
}

class App {
  static void Main() {
    System.Collections.ArrayList ar
                          = new System.Collections.ArrayList();
    ValueType value0; value0.x = 0; ar.Add(value0);
    ValueType value1; value1.x = 1; ar.Add(value1);
    ValueType value2; value2.x = 2; ar.Add(value2);
    ValueType value3; value3.x = 3; ar.Add(value3);
    ValueType value4; value4.x = 4; ar.Add(value4);
    ValueType value5; value5.x = 5; ar.Add(value5);

    foreach (ValueType value in ar) {
      value.x += 100;
    }
  }
}

 foreachループの中で次のようなコンパイル・エラーになるのだ。

C:\Padawan>csc clone2.cs
Microsoft (R) Visual C# .NET Compiler version 7.00.9466
for Microsoft (R) .NET Framework version 1.0.3705
Copyright (C) Microsoft Corporation 2001. All rights reserved.

clone2.cs(16,4): error CS0131: 代入式の左辺には変数、プロパティ、またはインデクサを指定しください。

 理由は上記とまったく同じである。foreachループの中で取得されるvalue変数は、実際にArrayListに格納されている値型の値の「クローン」なのだ。クローンに何がしかの変更を加えても、元の値型には何の変化も起こらず、「value.x += 100;」は、意味のない無駄なコードになってしまうのである。

まとめ

 このようなトピックは非常に高度で、与田さんのようなマスター中のマスターでもない限り知っておく必要がないと思われるかもしれない。だが、Windowsフォームに含まれる次のような定義を見ればその考えも改まるに違いない。

namespace System.Windows.Forms {
  public class Form : ContainerControl {
    .. (省略)
    public Point Location {get; set;}
    public new Size Size {get; set;}
  }
}

 PointとSizeはSystem.Drawing名前空間に定義されている構造体であり、しかもLocationとSizeはFormのフィールドではなくプロパティであることに注意して欲しい。つまり、これまで説明してきたとおり、次のようなコードは書けないということだ。

Form form1;
.... (省略)
form1.Location.X = 100;
form1.Size.Height = 200;

 上記のコードがエラーになる理由はもう分かっただろう。「プロパティはメソッドなので、値型のプロパティはメソッドの戻り値として値型を指定したことになり、結果としてクローンが戻されてしまう。そのクローンに対して意味のない値を設定しようとしているため、無駄なコードがエラーとして認識される」というのが理由だ。Windowsアプリケーションを開発していて、コードからフォームの位置を動かしたり、サイズを変更したりしたくなったときに、このようなコードを書くとエラーになって、行き詰まってしまいかねない。このような問題は開発者の身近にいくつも存在する。このような問題に突き当たったときにそれを解決する原動力となるのは、小手先のテクニックではなく実行環境の本質に対する深い理解なのだ。End of Article

 
     
 
「連載 One Point .NET」


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 記事ランキング

本日 月間