しばしば値型と参照型の動作の違いをイミュータブルなオブジェクトの挙動とごっちゃにしている例があるようなので整理。
ついでに派生してイミュータブルとか破壊的とか非破壊的とか。うまく文章まとまりませんでした。
値型と参照型の動作の違い(結論)
次のような例で動作の違いが出る。
[TestClass]
public class ReferenceTypeTest
{
[TestMethod]
public void MainMethod()
{
// あるクラスのあるプロパティを 1 にしてインスタンスを生成
var someInstance = new SomeClass
{
SomeProperty = 1
};
// メソッドに渡す
SomeMethod(someInstance);
// 値は 2 に書き換えられている
Assert.AreEqual(2, someInstance.SomeProperty);
}
public void SomeMethod(SomeClass obj)
{
// 引数で受け取ったオブジェクトのプロパティを 2 に書き換える
obj.SomeProperty = 2;
}
}
public class SomeClass
{
public int SomeProperty { get; set; }
}
[TestClass]
public class ValueTypeTest
{
[TestMethod]
public void MainMethod()
{
// あるクラスのあるプロパティを 1 にしてインスタンスを生成
var someInstance = new SomeStruct
{
SomeProperty = 1
};
// メソッドに渡す
SomeMethod(someInstance);
// 値は 2 に書き換えられて*いない*
Assert.AreEqual(1, someInstance.SomeProperty);
}
public void SomeMethod(SomeStruct obj)
{
// 引数で受け取ったオブジェクトのプロパティを 2 に書き換える
obj.SomeProperty = 2;
}
}
// ** 違いは↓ここだけ **
public struct SomeStruct
{
public int SomeProperty { get; set; }
}
メソッドの中で渡されたオブジェクトの「プロパティを」書き換えているのがミソ。
これで動作に違いが出る理由は参照型(今回はクラス)の場合 SomeMethod
に「参照のコピー」が渡るのに対して値型(今回はユーザー定義構造体)の場合「値のコピー」が渡るため。
これを絵にしてみるとこうなる。
http://www.slideshare.net/chocolamint/ss-29404257
で、言ってしまえば実はこれだけの話なんだけど、おそらくきっとたぶん、参照型の代表例として string
を持ちだして以下の様な説明をしていたりする解説があるものだから混乱する人が出てくる。
変数の書き換え
というわけで、全然違うよね、の例。
[TestMethod]
public void MainMethod()
{
// とある文字列
var str = "あいうえお";
// メソッドに渡す
SomeMethod(str);
// 値は "かきくけこ" に書き換えられている…わけない!
Assert.AreEqual("あいうえお", str);
}
public void SomeMethod(string str)
{
// 引数で受け取った文字列を書き換える(?)
str = "かきくけこ";
}
引数の str
自体に別の値を代入しているのがミソ。
これでもって「string
は参照型の中でも(組み込み型であるがゆえに)特殊だ」と理解する人がいる模様。
でも全然違う。
だって絵にしてみるとこう。
書き換えているものが全然違うのがわかりますか?
この例でもって「string
は参照型ですが値型の動きをするような特別なクラスです」なんて説明も聴いたことがあるけども、それは違うよねと。
で、それじゃあどういう例なら string
がクラスの動きをしていることがわかるんだということなんだけども、最初に出した「プロパティを書き換える」という例が使えないんですよね、string
型は。
それなのに string
型で参照型の動作例を説明しようとするからおかしくなっちゃう。
string
は確かに特別です。でもそれは「値型みたいな動きをする」わけではなくて「イミュータブルである」という点でです。
そしてそれは「組み込み型だから」ではありません。
※ extern が云々とかの話は今回は置いておきます。
…イミュータブルって?
イミュータブルとミュータブル
イミュータブルを日本語でいうと「不変」。つまり値が変わらないということ。
もっと砕いた言い方をすると「全てのプロパティが読み取り専用」ってことです。
System.String
型をMSDNで調べると確認できます。
http://msdn.microsoft.com/ja-jp/library/system.string%28v=vs.110%29.aspx
string
型には Chars
(C#ではインデクサで表現されています)と Length
の 2 つのプロパティがありますが、いずれも get
アクセサのみで読み取り専用であることが確認できると思います。
イミュータブル(不変)なオブジェクトの場合、最初に挙げた「メソッドの中でプロパティを書き換える」という動作自体ができないため、「値型と参照型の違いの不理解による想定外の動作」という現象には通常遭遇しません。
ということで、構造体(つまり値型)を定義する際は通常はイミュータブルに設計したほうが良いです。
(2016/05/19追記 ここから)
(推奨されてた気がしたけど根拠を見つけられませんでした…)
何のことはない、クラスまたは構造体の選択の章に書いてありました。
型が次に挙げるすべての特性を持たない場合、構造体は定義しません。
- プリミティブ型 (整数、倍精度浮動小数点数など) に似た単一の値を論理的に表す。
- インスタンスのサイズが 16 バイト未満である。
- 変更できない。
- 頻繁にボックス化する必要がない。
(2016/05/19追記 ここまで)
例えば System.DateTime
構造体はイミュータブルに設計されており、全てのプロパティが読み取り専用です。
また、全てのメソッドは新しい DateTime
オブジェクトを返すように設計されています。
var tomorrow = DateTime.Today.AddDays(1);
この AddDays
メソッドが自分自身の日付を書き換える作りだとイミュータブルを保てませんからね。
このようなメソッドを「非破壊的」なメソッドといいます。
「破壊的」なメソッドと「非破壊的」なメソッドは普段から意識して区別しておく必要があります。
しばしば次のようなコードを見かけるからです。
List<Hoge> list = GetList();
// OrderBy は非破壊的なメソッドなので list 自体は書き換わらない
list.OrderBy(x => x.Foo);
正しくは以下のいずれかですね。
List<Hoge> list = GetList();
// List<T>.Sort は破壊的なメソッドなので list 自体が書き換わる
list.Sort((a, b) => a.Foo - b.Foo);
// OrderBy は非破壊的なメソッドなので戻り値を受け取る必要がある
list = list.OrderBy(x => x.Foo).ToList();
特に JavaScript の配列メソッドでは破壊的なものと非破壊的なものが混在しているので注意が必要です。
(あと jQuery.fn.add が非破壊的メソッドなのは過去になんどもハマりました…)
また、自身がメソッドを設計する場合にも破壊的にすべきか非破壊的にすべきかをきちんと検討すべきです。
構造体をミュータブルに設計した場合の問題点
以下の様な場合に問題が生じます。
public struct SomeStruct
{
public int SomeProperty { get; set; }
}
public class Hoge
{
public SomeStruct StructProperty { get; set; }
public Hoge()
{
StructProperty.SomeProperty = 2; // 変数ではないため、'Hoge.StructProperty' の戻り値を変更できません。
}
}
これはプロパティが「一見変数(フィールド)に見えるけど実はメソッドで実装されている」から起こる現象です。
StructProperty
にアクセスした時点で値のコピーが返ってきちゃうのでそれを書き換えても意味がないということです。
想像しづらい人は GetStructProperty
メソッドを定義して考えてみるといいと思います。
これに相当する例が System.Drawing.Point
構造体です。これの X
プロパティや Y
プロパティは set
可能なプロパティのため、コントロールの Location.X
などを書き換えようとすると怒られます。
じゃあどうすればいいかというと、構造体ごと書き換える必要があります。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
// Location.X = 100; // これは先ほどと同じエラー
Location = new Point(100, Location.Y); // Point 構造体自体を作りなおして設定する
}
}
面倒くさいですね。そのため、この動作をラップした Top
プロパティや Left
プロパティが公開されています。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Left = 100;
}
}
ということで、ミュータブルな構造体の注意点と、やむ得ずそのように実装する場合の考慮点でした。
記事を綺麗に簡潔にまとめるのって難しいですね。今日はこの辺で…