[C#.Tips]也来谈谈接口欺诈
不得不先说明一下,这又是一篇跟值类型的装箱拆箱有关的文章,尽管我之前已近写了两篇随笔来阐述这个很基础的问题了。它们分别在:这里和这里。本文中的代码示例出自后者,稍作了修改。
我们知道C#是一门“安全”的的语言,以至于它不让我们修改已装箱值类型实例中的字段。因为这种尝试会带来出乎意料的效果。下面就来解释一下为什么会有这种让很多程序员“意外”的情况发生以及如何“欺骗”C#来实现程序员真正的意图,尽管那样做不是合理的方式。
首先还是把我以前在这里提到的那段老代码翻出来:
跟之前的代码相比,仅仅多了一个接口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在背后为你“悄悄地”创建一个已装箱的值类型,而你却无法控制。
就写到这吧,我一直希望用最简单的话把问题说清楚,同时欢迎大家批评指正 :-)
我们知道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
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 }
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
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在背后为你“悄悄地”创建一个已装箱的值类型,而你却无法控制。
就写到这吧,我一直希望用最简单的话把问题说清楚,同时欢迎大家批评指正 :-)