.NET,你忘记了么?(七)—— 从a?b:c谈起
1. 摘要
在这篇文章中,我会通过IL去分析一个简单的语句。
如果觉得实在简单,可以略过。
2. 引子
事情是这样的,同事写了一段类似这样的代码:
class Program { static void Main(string[] args) { object o = new object(); int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : 3.5; } }当然不是在控制台程序中,我在这里只是写出个模拟。
然后系统报出了一个这样的错误。
3. 错误分析
同事很诧异地问我,这是为什么啊?
他给出的理由是object是一切类的父类,那么我把3.5或者null赋给他都没有问题啊,那这个问题是怎么回事呢?
我意识到自己的语言表达能力远不如代码有说服力,于是,写段代码,然后请出IL。
4. 请出IL
让我们先写段正确的代码,保证他的编译通过。
class Program { static void Main(string[] args) { object o = new object(); int i = 1; int j = 2; o = i+j > 3 ? 3 : 3.5; } }然后去查看IL代码:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 2 .locals init ( [0] object o, [1] int32 i, [2] int32 j) L_0000: nop L_0001: newobj instance void [mscorlib]System.Object::.ctor() L_0006: stloc.0 L_0007: ldc.i4.1 L_0008: stloc.1 L_0009: ldc.i4.2 L_000a: stloc.2 L_000b: ldloc.1 L_000c: ldloc.2 L_000d: add L_000e: ldc.i4.3 L_000f: bgt.s L_001c L_0011: ldc.r8 3.5 L_001a: br.s L_0025 L_001c: ldc.r8 3 L_0025: box float64 L_002a: stloc.0 L_002b: ret }在这里,我们只关注从L_00d开始的代码,首先我们将两个数i和j相加,然后去与3比较大小,如果大于3,那么便跳转到L_001c,将3作为Float类型压栈,否则顺序向下执行,将3.5作为float类型压栈。最后将栈顶元素装箱。
看过了这个解释,我们再回去看原有的那段代码,原因再清楚不过了,?:这个三元运算符在编译成IL代码时,把:两端的值压栈,然后把这两个值存储在一个临时变量里,而这个变量要取两者之前类型转换后级数最高的类型。举个例子:int 和 float 就需要转换成float,float 和 double 就需要转换成double 等等。而在同事的程序中, double 类型和 null 类型无法相互转换,所以就报了这样的一个错误。
5. 改造代码
继续仔细思考,究竟什么样的两个类型可以写在:的两端。上面的错误再清楚不过。两个可以隐式转换的类型可以。
那下面继续解决这个问题。上面的代码我们要怎么写:
static void Main(string[] args) { object o = new object(); int i; Int32.TryParse(Console.ReadLine(), out i); if (i > 3) { o = null; } else { o = 3.5; } }怎么看都没有上面的代码漂亮。这个可以说除了能运行外真的没什么优点了。
还记得那个泛型类吧:Nullable<T>。
那就让我们用这个泛型类来改造吧:
static void Main(string[] args) { object o = new object(); Nullable<double> n = 3.5; int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : n; }如果觉得Nullable<T>还不够美观。
static void Main(string[] args) { object o = new object(); double? n = 3.5; int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : n; }这样改造是不是优秀了一些呢?
这个时候如果有人提出,那么我为什么不o=i>3?null:(object)3.5呢?那我们想一下如果有一天我们不再用object o;
我们是不是可以把代码写成这样:
static void Main(string[] args) { //object o = new object(); double? o; double? n = 3.5; int i; Int32.TryParse(Console.ReadLine(), out i); o = i > 3 ? null : n; }ShadowK 给出了这样的做法,是个好办法:
o = i > 3 ? (double?)null : n ;
这个时候再看IL,是不是已经没有了可恶的box呢?
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 2 .locals init ( [0] valuetype [mscorlib]System.Nullable`1<float64> o, [1] valuetype [mscorlib]System.Nullable`1<float64> n, [2] int32 i, [3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0000) L_0000: nop L_0001: ldloca.s n L_0003: ldc.r8 3.5 L_000c: call instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0) L_0011: nop L_0012: call string [mscorlib]System.Console::ReadLine() L_0017: ldloca.s i L_0019: call bool [mscorlib]System.Int32::TryParse(string, int32&) L_001e: pop L_001f: ldloc.2 L_0020: ldc.i4.3 L_0021: bgt.s L_0026 L_0023: ldloc.1 L_0024: br.s L_002f L_0026: ldloca.s CS$0$0000 L_0028: initobj [mscorlib]System.Nullable`1<float64> L_002e: ldloc.3 L_002f: stloc.0 L_0030: ret }6. 总结
写上面的文章我并不是单纯地想阐明这个具体的语法情况。而是希望大家掌握一种思路。
语法怎么回事?为什么不是像我想的那样?
请出IL。
【推荐】国内首个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 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· [AI/GPT/综述] AI Agent的设计模式综述
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!