[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

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

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在背后为你“悄悄地”创建一个已装箱的值类型,而你却无法控制。
就写到这吧,我一直希望用最简单的话把问题说清楚,同时欢迎大家批评指正 :-)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述