C# 9.0 终于来了, Top-level programs 和 Partial Methods 两大新特性探究
source link: https://segmentfault.com/a/1190000023028659
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
一:背景
1. 讲故事
.NET 5 终于在 6月25日 发布了第六个预览版,随之而来的是更多的新特性加入到了 C# 9 Preview 中,这个系列也可以继续往下写了,废话不多说,今天来看一下 Top-level programs
和 Extending Partial Methods
两大新特性。
2. 安装必备
下载最新的 .net 5 preview 6
。
下载最新的 Visual Studio 2019 version 16.7 Preview 3.1
二:新特性研究
1. Top-level programs
如果大家玩过 python,应该知道在 xxx.py 中写一句 print,这程序就能跑起来了,简单高效又粗暴,很开心的是这特性被带到了C# 9.0 中。
- 修改前
using System; namespace ConsoleApp2 { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } }
- 修改后
System.Console.WriteLine("Hello World!");
这就有意思了,Main入口函数去哪了? 没它的话,JIT还怎么编译代码呢? 想知道答案的话用 ILSpy 反编译看一下就好啦!
.class private auto ansi abstract sealed beforefieldinit $Program extends [System.Runtime]System.Object { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Methods .method private hidebysig static void $Main ( string[] args ) cil managed { // Method begins at RVA 0x2050 // Code size 18 (0x12) .maxstack 8 .entrypoint IL_0000: ldstr "Hello World!" IL_0005: call void [System.Console]System.Console::WriteLine(string) IL_000a: nop IL_000b: call string [System.Console]System.Console::ReadLine() IL_0010: pop IL_0011: ret } // end of method $Program::$Main } // end of class $Program
从 IL 上看,类变成了 $Program
, 入口方法变成了 $Main
, 这就好玩了,在我们的印象中入口函数必须是 Main
,否则编译器会给你一个大大的错误,你加了一个 $ 符号,那CLR还能认识吗? 能不能认识我们用 windbg 看一些托管和非托管堆栈,看看有什么新发现。
0:010> ~0s ntdll!NtReadFile+0x14: 00007ffe`f8f8aa64 c3 ret 0:000> !dumpstack OS Thread Id: 0x7278 (0) Current frame: ntdll!NtReadFile + 0x14 Child-SP RetAddr Caller, Callee 0000008551F7E810 00007ffed1e841dc (MethodDesc 00007ffe4020d500 + 0x1c System.Console.ReadLine()), calling 00007ffe400ab090 0000008551F7E840 00007ffe4014244a (MethodDesc 00007ffe401e58f0 + 0x3a $Program.$Main(System.String[])), calling 00007ffe40240f58 0000008551F7E880 00007ffe9fcc8b43 coreclr!CallDescrWorkerInternal + 0x83 [F:\workspace\_work\1\s\src\coreclr\src\vm\amd64\CallDescrWorkerAMD64.asm:101] 0000008551F7E8C0 00007ffe9fbd1e03 coreclr!MethodDescCallSite::CallTargetWorker + 0x263 [F:\workspace\_work\1\s\src\coreclr\src\vm\callhelpers.cpp:554], calling coreclr!CallDescrWorkerWithHandler [F:\workspace\_work\1\s\src\coreclr\src\vm\callhelpers.cpp:56] 0000008551F7E950 00007ffe9fb8c4e5 coreclr!MethodDesc::IsVoid + 0x21 [F:\workspace\_work\1\s\src\coreclr\src\vm\method.cpp:1098], calling coreclr!MetaSig::IsReturnTypeVoid [F:\workspace\_work\1\s\src\coreclr\src\vm\siginfo.cpp:5189] 0000008551F7EA00 00007ffe9fb8c4bf coreclr!RunMainInternal + 0x11f [F:\workspace\_work\1\s\src\coreclr\src\vm\assembly.cpp:1488], calling coreclr!MethodDescCallSite::CallTargetWorker [F:\workspace\_work\1\s\src\coreclr\src\vm\callhelpers.cpp:266] 0000008551F7EB30 00007ffe9fb8c30a coreclr!RunMain + 0xd2 [F:\workspace\_work\1\s\src\coreclr\src\vm\assembly.cpp:1559], calling coreclr!RunMainInternal [F:\workspace\_work\1\s\src\coreclr\src\vm\assembly.cpp:1459]
从上面堆栈的流程图看: coreclr!RunMain
-> coreclr!MethodDesc
-> coreclr!CallDescrWorkerInternal
-> $Program.$Main
, 确实被调用了,不过有一个重大发现,在 $Program.$Main
调用之前底层的 CLR 读取了 方法描述符,这就是一个重大突破点,方法描述符在哪里呢? 可以用 ildasm 去看一下元数据列表。
可以看到,入口函数那里打上了一个 ENTRYPOINT
标记,这就说明入口函数名其实是可以随便更改的,只要被 ENTRYPOINT
打上标记即可,CoreCLR就能认的出来~~~
2. Partial Methods
我们知道 部分方法 是一个很好的桩函数,而且在 C# 3.0 中就已经实现了,那时候给我们增加了很多限制,如下图:
翻译过来就是:
- 部分方法的签名必须一致
- 方法必须返回void
- 不允许使用访问修饰符,而且还是隐式私有的。
在 C# 9.0 中放开了对 方法签名 的所有限制,正如 issue 总结:
这是一个非常好的消息,现在你的部分方法上可以加上各种类型的返回值啦,这里我举一个例子:
class Program { static void Main(string[] args) { var person = new Person(); Console.WriteLine(person.Run("jack")); } } public partial class Person { public partial string Run(string name); } public partial class Person { public partial string Run(string name) => $"{name}:开溜了~"; }
然后我们用 ILSpy 简单看看底层怎么玩的,如下图可以看到其实就是一个简单的合成,对吧。
现在我有想法了,如果我不给 Run 方法实现会怎么样? 把下面的 partial 类注释掉看一下。
从报错信息看,可访问的修饰符必须要有方法实现,<font color="red">还以为直接编译的时候抹掉呢。</font> 这就起不到桩函数的作用:-D,不过这个特性还是给了我们更多的可能用的到的应用场景吧。
三:总结
本篇两个特性还是非常实用的,Top-level programs 让我们可以写更少的代码,甚至拿起 记事本 都可以快捷的编写类似一次性使用的测试代码, Partial Methods 特性留给大家补充吧,我基本上算是没用过 (┬_┬)。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK