這個做法在討論 Value Types 和 Reference Types 兩種型別中複製與傳遞的差異性與它們的使用場合。
當我們要建立新的型別時,第一件要做的事就是挑選 class
或是 struct
,如果選錯的話之後要修改會大大影響使用到這個型別的使用者。
首先看看這個例子,這裡 MyData 型別使用 struct
建立,所以它是個 Value Types,並且透過 Foo 方法將物件進行回傳,最終會將回傳的物件完整複製一份到 v
區域變數內,
這也是 Value Types 的特性。
void Main()
{
var a = new MyClass();
MyData v = a.Foo();
v.Value = 2;
Console.WriteLine(v.Value);
Console.WriteLine(a.FooValue());
}
public class MyClass
{
private MyData myData;
public MyData Foo() => myData;
public int FooValue() => myData.Value;
public MyClass()
{
myData = new MyData(1);
}
}
public struct MyData
{
public int Value { get; set; } = 1;
public MyData(int value)
{
Value = value;
}
}
但如果只要將 struct
改成 class
,那麼就會變成 Reference Types,Foo 方法回傳的就是物件的參考,最終複製到 v
區域變數內的就只是一份參考,
可以理解成匯出一份參考到 v
中,這也代表你能透過 v
修改 myData
實例內部的狀態這個做法違反了封裝的原則。
public class MyData
{
public int Value { get; set; } = 1;
public MyData(int value)
{
Value = value;
}
}
接下來看看這個例子,內部使用了 MemberwiseClone
方法將 myData
實例複製一份到新的到 myDataClone
上,
這樣區域變數 v
獲得的就是 myDataClone
的參考,所以修改 v
變數時不會修改到原始的 myData
實例,
最後修改沒問題在把值更新回去 myData
實例,但這種做法會在 heap 上額外建立一個物件。
void Main()
{
var c = new MyClass();
MyData v = c.Foo();
v.Value = 2;
c.Update(v);
Console.WriteLine(c.FooValue());
Console.WriteLine(c.BarValue());
}
public class MyClass
{
private MyData myData;
private MyData myDataClone;
public MyData Foo() => myDataClone;
public int FooValue() => myData.Value;
public int BarValue() => myDataClone.Value;
public MyClass()
{
myData = new MyData(1);
myDataClone = myData.Clone();
}
public void Update(MyData m)
{
myData = m;
}
}
public class MyData
{
public int Value { get; set; } = 1;
public MyData(int value)
{
Value = value;
}
public MyData Clone()
{
return (MyData)this.MemberwiseClone();
}
}
現在來看一下兩種型別是如何在記憶體中儲存的,假設 MyType
是一個 Value Types 那麼這段程式碼就會執行一次記憶體分配(Allocating memory)
並分配兩倍 MyType
大小的記憶體。
但如果是 Reference Types 則會執行三次記憶體分配,分別是一次 C 物件與兩次內部的 MyType 物件。
這兩者之間的差異是保存 Value Types 的變數是真正保存實際的值,而 Reference Types 則是保存參考。
public class C
{
private MyType a = new MyType();
private MyType b = new MyType();
}
C cThing = new C();
更詳細的說明可以參考這段程式碼,假設 MyType
是一個 Value Types 那麼這段程式碼就會分配一次大小為 100 倍 MyType
大小的記憶體,
如果是 Reference Types 則會執行一次記憶體分配,並且陣列中的所有元素都是 null,但如果你把所有 100 個元素都初始化,則會執行 101 次記憶體分配,
跟 Value Types 的一次相比之下,Reference Types 需要更多時間並且造成 heap fragments。
MyType[] arrayOfTypes = new MyType[100];
也就是說如果你建立的型別確定是用來保存資料的,那就應該使用 Value Types;
Summary
資料儲存的場合將物件定義成 Value Types,與行為相關的場合定義成 Reference Types,這樣在複製資料物件的時候就能更加安全,同時 Value Types 又能將資料保存在更快的 stack 記憶體,使用 Value Types 的場合目的要很明確並且要知道它的優缺點,如果有模糊不知道該怎麼選擇的場合, 那應該選擇 Reference Types 比較不會出錯。