C# 9.0 終於來了, Top-level programs 和 Partial Methods 兩大新特性探究

一:背景

1. 講故事

.NET 5 終於在 6月25日 發佈了第六個預覽版,隨之而來的是更多的新特性加入到了 C# 9 Preview 中,這個系列也能夠繼續往下寫了,廢話很少說,今天來看一下 Top-level programsExtending Partial Methods 兩大新特性。python

2. 安裝必備

下載最新的 .net 5 preview 6ide

下載最新的 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 反編譯看一下就好啦!spa

.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 看一些託管和非託管堆棧,看看有什麼新發現。.net

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就能認的出來~~~code

2. Partial Methods

咱們知道 部分方法 是一個很好的樁函數,並且在 C# 3.0 中就已經實現了,那時候給咱們增長了不少限制,以下圖:blog

翻譯過來就是:ci

  • 部分方法的簽名必須一致
  • 方法必須返回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 特性留給你們補充吧,我基本上算是沒用過 (┬_┬)。

相關文章
相關標籤/搜索