不得不先說明一下,這又是一篇跟實值型別的裝箱拆箱有關的文章,儘管我之前已近寫了兩篇隨筆來闡述這個很基礎的問題了。它們分別在:這裡和這裡。本文中的程式碼範例出自後者,稍作了修改。
我們知道C#是一門“安全”的的語言,以至於它不讓我們修改已裝箱實值型別執行個體中的欄位。因為這種嘗試會帶來出乎意料的效果。下面就來解釋一下為什麼會有這種讓很多程式員“意外”的情況發生以及如何“欺騙”C#來實現程式員真正的意圖,儘管那樣做不是合理的方式。
首先還是把我以前在這裡提到的那段老代碼翻出來:
1 /**//// <summary>
2 /// 重新訂票的介面
3 /// </summary>
4 internal interface IReBook
5 {
6 Ticket ReBook(String newTerminal, Int32 newDistance);
7 }
8
9 internal struct Ticket:IReBook
10 {
11 private String _start, _terminal;//起點和終點
12 private Int32 _distance;//距離
13
14 public Ticket(string start, string terminal, Int32 distance)
15 {
16 _start = start;
17 _terminal = terminal;
18 _distance = distance;
19 }
20
21
22 /**//// <summary>
23 /// 重寫System.ValueType的ToString方法
24 /// </summary>
25 public override String ToString()
26 {
27 return String.Format("From {0} To {1} , {2} km",
28 _start,
29 _terminal,
30 _distance);//在方法的內部,_distance被裝箱
31 }
32
33 IReBook Members#region IReBook Members
34 /**//// <summary>
35 /// 重新訂票
36 /// </summary>
37 /// <param name="newTerminal">新的終點站</param>
38 /// <param name="newDistance">到終點站的距離</param>
39 public Ticket ReBook(string newTerminal, int newDistance)
40 {
41 _terminal = newTerminal;
42 _distance = newDistance;
43 return this;
44 }
45
46 #endregion
47 }
48
49 public sealed class Program
50 {
51 public static void Main()
52 {
53 Ticket t = new Ticket("北京", "漢口", 1225);
54 //實值型別執行個體t在這裡第一次被裝箱:Ticket-->Object-->override ToString
55 Console.WriteLine(t);
56
57 //顯示的裝箱
58 // Console.WriteLine(((Object)t).ToString());
59
60 t.ReBook("上海", 1400);
61 Console.WriteLine(t);
62
63 Object o = t;
64 Console.WriteLine(o);
65
66 Ticket t2 = ((Ticket)o).ReBook("廣州", 2000);
67 Console.WriteLine(o);
68 Console.WriteLine(t2);
69
70 //t-->IReBook,被裝箱
71 ((IReBook)t).ReBook("廣州", 2000);
72 Console.WriteLine(t);
73
74 //o-->IReBook,無須裝箱
75 ((IReBook)o).ReBook("廣州", 2000);
76 Console.WriteLine(o);
77 }
78 }
跟之前的代碼相比,僅僅多了一個介面IReBook,然後在Ticket中實現了這個介面。輸出方面,前5個輸出都跟原來的代碼一樣,顯示的結果也一樣。我在後面增加了兩個輸出,您可以先猜猜第72行的輸出結果會是怎樣?
似乎我們原本的意圖是修改車票為到廣州,2000km。但是這裡的輸出卻仍然是"From 北京 To 上海,1400 km"。這是違背了我們的初衷的(其實原本定義這樣的方法就是不合理的)。
是什麼原因會讓我們的修改“失敗”了呢?看了這篇文章的朋友應該能看出來,因為實值型別執行個體t裝箱成為參考型別IReBook,我們調用ReBook時,只是在CLR產生的已裝箱的實值型別執行個體(姑且稱做tII)上進行了修改,由於沒有任何引用指向tII,tII會被GC探知並回收。
那麼如何強行讓這種對實值型別執行個體欄位的改變變得合理呢?這就是引入介面的原因,來看第75行,我們把參考型別o(其指向的是已裝箱的實值型別tII)轉型為IReBook,這是兩個參考型別之間的轉換,不存在裝箱拆箱,不建立額外的副本,所以當我們在IReBook上調用ReBook方法時,會理所當然的顯示改變後的結果"From 北京 To 廣州,2000 km"。
這就是所謂的介面欺詐,間接地修改已裝箱實值型別的執行個體欄位。
程式完整的輸出結果如下:
顯然,一個可變(mutable)的實值型別,如這裡的Ticket ,一般來講都是不合理的設計,因為這會給我們帶來像上述的出乎預料的結果,而且會產生額外的“垃圾”,這在我們定義任何一個實值型別的時候都是應當注意的。就這個例子來說,明顯定義Ticket為一個class是較好的設計,因為這樣避免了產生"失控的對象"以及對它的操作。
總之,"一個實值型別成員不應該修改任何執行個體欄位" --《CLR via C#》。
順便再多說一句關於介面的,一般來說不要嘗試把未裝箱的實值型別轉化為介面類型,因為那樣做實際上是讓CLR在背後為你“悄悄地”建立一個已裝箱的實值型別,而你卻無法控制。
就寫到這吧,我一直希望用最簡單的話把問題說清楚,同時歡迎大家批評指正