[C#.Tips]也來談談介面欺詐

來源:互聯網
上載者:User
不得不先說明一下,這又是一篇跟實值型別的裝箱拆箱有關的文章,儘管我之前已近寫了兩篇隨筆來闡述這個很基礎的問題了。它們分別在:這裡和這裡。本文中的程式碼範例出自後者,稍作了修改。
我們知道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在背後為你“悄悄地”建立一個已裝箱的實值型別,而你卻無法控制。

就寫到這吧,我一直希望用最簡單的話把問題說清楚,同時歡迎大家批評指正

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.