探索 C# 9 的源代碼生成器
源代碼生成器(Source Generator)是一種機(jī)制,它允許我們使用 C# 代碼在編譯前產(chǎn)生別的、自定義的代碼,以達(dá)到代碼注入的效果。
何為代碼注入?
那什么是代碼注入(Code Injection)呢?我舉一個(gè)稍微偏一點(diǎn)但是比較形象的例子。在多個(gè)人比賽之前,裁判就已經(jīng)定下了一些比賽期間的“黑幕”:比如我給誰(shuí)誰(shuí)誰(shuí)比賽選手做一個(gè)標(biāo)記,裁判在比賽之前要是看到了這個(gè)標(biāo)記,裁判就知道我給他說(shuō)過(guò),讓裁判故意判他贏。
這個(gè)例子里,標(biāo)記好比是編譯前用戶(hù)寫(xiě)代碼時(shí)給的特性之類(lèi)的類(lèi)型標(biāo)記,而裁判就是這個(gè)編譯器。在編譯程序的時(shí)候,一旦發(fā)現(xiàn)這個(gè)東西,就立刻作出一些額外的補(bǔ)充。我再舉個(gè)代碼層面的例子:
假如我有這么一個(gè)類(lèi)型 ,這個(gè)類(lèi)型我不給出任何的實(shí)現(xiàn),但給了一個(gè)特性標(biāo)記:
。是的,這個(gè)特性是我隨手寫(xiě)的,庫(kù)文件里不存在。假定這個(gè)特性用來(lái)表示,讓編譯器知道,我一會(huì)兒要自動(dòng)追加一個(gè)?
?一個(gè)
?這兩個(gè)屬性到項(xiàng)目里,并自動(dòng)實(shí)現(xiàn)諸如
、
、
、
?之類(lèi)的方法,而我不從這里直接實(shí)現(xiàn)(因?yàn)閷?xiě)起來(lái)比較復(fù)雜,我懶得寫(xiě),于是就想使用代碼注入的方式,把工作交給編譯器)。
再舉個(gè)例子。假如,我實(shí)現(xiàn)了一個(gè)類(lèi)型,這個(gè)類(lèi)型底層用了一個(gè)數(shù)組來(lái)封裝,比如這樣:
可以發(fā)現(xiàn)一個(gè)顯著的問(wèn)題。索引器 ?的參數(shù)可能隨用戶(hù)傳入負(fù)數(shù)進(jìn)去。但我在實(shí)現(xiàn)的時(shí)候并沒(méi)有寫(xiě)這句話。我可以考慮使用代碼注入的方式,給 getter 和 setter 標(biāo)記特性(或者直接給屬性標(biāo)記諸如
),以表示參數(shù)必須在合理的范圍內(nèi),否則拋出異常。這樣我也懶得寫(xiě)這些代碼了,也可以防止我有些時(shí)候代碼寫(xiě)錯(cuò)了或者漏寫(xiě)了:
然后寫(xiě)上合適的生成代碼,最后編譯器可以自動(dòng)加上注入代碼:
當(dāng)然,這我們壓根沒(méi)給出實(shí)現(xiàn)這樣的行為的代碼,我也就這么說(shuō)說(shuō),讓你明白這個(gè)代碼注入到底有多強(qiáng)。
源代碼生成器和 T4 模板
下面我們回到話題,來(lái)說(shuō)一下,如何構(gòu)造一個(gè)源代碼生成器。
前文說(shuō)到,源代碼生成器的作用之一,就是用來(lái)代碼注入。接下來(lái)我們?yōu)榱俗尨蠹胰腴T(mén),我們來(lái)說(shuō)另外一種用法:代替 T4 模板。
前文的內(nèi)容還是對(duì)入門(mén)的朋友們來(lái)說(shuō)太難了,所以這里我們講簡(jiǎn)單一點(diǎn)。如果你想學(xué)習(xí)前文的代碼注入的過(guò)程,你可以參考 Source Generator Cookbook 一節(jié)的內(nèi)容,但有點(diǎn)多,而且全英文,所以需要你多多努力。
地址:https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md
T4 模板是一種控制復(fù)雜的、生成類(lèi)似變長(zhǎng)泛型參數(shù)這種無(wú)法從 C# 語(yǔ)言本身做到,但又有規(guī)律性的代碼。當(dāng)然,我知道 C++ 里已經(jīng)有了變長(zhǎng)泛型參數(shù)這個(gè)概念和特性了,但 C# 官方不出意外是不會(huì)添加這個(gè)特性了,因?yàn)闆](méi)有必要。T4 模板可以生成這樣的代碼。那么做法很簡(jiǎn)單,按照 T4 模板的語(yǔ)法規(guī)則,將基本的代碼的文本寫(xiě)出來(lái),把變化的部分用 、
?這樣的標(biāo)記來(lái)替換掉。但是,這樣的代碼控制縮進(jìn)非常困難。稍不注意,標(biāo)記位置寫(xiě)得不對(duì),就會(huì)導(dǎo)致代碼生成的縮進(jìn)有問(wèn)題,而且就算是沒(méi)問(wèn)題了,T4 模板代碼本身也會(huì)很丑。
那么,源代碼生成器就這么誕生了。
使用源代碼生成器
下面我們來(lái)介紹一下源代碼生成器是怎么用的。
第一步:創(chuàng)建動(dòng)態(tài)鏈接庫(kù)項(xiàng)目
怎么用呢?我們先創(chuàng)建一個(gè)生成動(dòng)態(tài)鏈接庫(kù)的項(xiàng)目。
在此之前,請(qǐng)確保你的 Visual Studio 是 2019 v16.9 Preview 3 及其以上版本的,因?yàn)閺倪@個(gè)版本開(kāi)始,Visual Studio 才開(kāi)始支持源代碼生成器的使用和顯示。
如果不使用 16.9 Preview 3 及其以上的版本運(yùn)行和編譯程序的話,你將 100% 獲得編譯器警告 CS8032 一份。
老實(shí)說(shuō),這個(gè)功能才出來(lái)不久,所以很多 bug 需要修復(fù)。比如第一次運(yùn)行的時(shí)候,可能報(bào)的 CS8032 警告不論編譯多少次程序,不論編譯是不是已經(jīng)成功過(guò),這個(gè)編譯器警告一直都存在(說(shuō)白了就是不會(huì)刷新),只有重啟了才沒(méi)有;但是隔一會(huì)兒又來(lái)了,但是運(yùn)行編譯程序則又是成功的。

然后選擇“Class Library”,點(diǎn)“Next"。

這里是讓你創(chuàng)建一個(gè)項(xiàng)目,給項(xiàng)目取名。項(xiàng)目隨便取名,比如 ?就好。也繼續(xù)點(diǎn)擊“Next”。

最后,注意這一步。請(qǐng)選擇 .NET Standard 2.0。請(qǐng)不要選擇 .NET 或 .NET Core、.NET Framework 或其它內(nèi)容(也不要選擇 .NET Standard 2.1),這一點(diǎn)很重要。
第二步:創(chuàng)建一個(gè)生成器的類(lèi),并且實(shí)現(xiàn) `ISourceGenerator` 接口和標(biāo)記 `GeneratorAttribute` 特性
當(dāng)我們創(chuàng)建完成了基本的項(xiàng)目后,項(xiàng)目會(huì)默認(rèn)帶一個(gè)空的類(lèi) 。這個(gè)時(shí)候隨便改個(gè)名字就行,比如叫
。
這個(gè)時(shí)候,請(qǐng)寫(xiě)這樣的代碼:

代碼上會(huì)報(bào)錯(cuò)。很明顯的原因:?和
?并不存在。這個(gè)時(shí)候我們點(diǎn)擊彈出的黃色燈泡圖標(biāo)(或者按
),可以彈出如下的內(nèi)容:

此時(shí),請(qǐng)選擇“Install package 'Microsoft.CodeAnalysis.Common'”就可以了。然后等待 VS 自動(dòng)安裝。
安裝完成后,接口和特性就都會(huì)成功導(dǎo)入。但是,此時(shí)會(huì)有這樣的錯(cuò)誤信息:

當(dāng)然,因?yàn)槲覀儧](méi)實(shí)現(xiàn)接口呢。然后我們照著這個(gè)實(shí)現(xiàn)接口。
這樣,接口就不報(bào)錯(cuò)了。請(qǐng)注意代碼里寫(xiě)的這個(gè)注釋。這是我加的,因?yàn)槲覀円粫?huì)兒只在 ?方法里給實(shí)現(xiàn),而不是在
?方法里。下面這個(gè)方法我們這一次用不到,留空就行。
留空不是拋
。留空就是保持空代碼塊就行,不寫(xiě)代碼,而不是拋異常。
第三步:照著我抄就行了,看我怎么寫(xiě)代碼的
是的,看標(biāo)題就明白我的意思了:
行了。抄上去就行。
第四步:創(chuàng)建一個(gè)控制臺(tái)程序項(xiàng)目
接著,創(chuàng)建一個(gè)測(cè)試項(xiàng)目。當(dāng)然了,這個(gè)測(cè)試項(xiàng)目那肯定得是可以運(yùn)行的,對(duì)吧。

這里隨便取名。

然后是這里??刂婆_(tái)程序可以是任何版本的,只要不低于 .NET Standard 2.1(就是剛才第一個(gè)項(xiàng)目里,你選擇的那一項(xiàng))。
點(diǎn)擊“Create”就行了。
第五步:修改項(xiàng)目配置文件
這里是整個(gè)創(chuàng)建過(guò)程的難點(diǎn)。請(qǐng)打開(kāi)這個(gè)新建項(xiàng)目的配置文件(?的那個(gè)文件)。然后添加這樣的內(nèi)容:

現(xiàn)在,配置文件長(zhǎng)這樣。然后關(guān)閉這個(gè)文件。記得保存,而且別寫(xiě)錯(cuò)了。
然后隔一會(huì)兒,你就可以發(fā)現(xiàn),你剛添加的這個(gè)源代碼生成器項(xiàng)目已經(jīng)添加到了測(cè)試項(xiàng)目里了,而且是以一個(gè)分析器的形式添加進(jìn)來(lái)的(看顯示的位置,是在 Dependencies 的 Analyzers 下面的。這倆單詞啥意思呢?依賴(lài)和分析器的意思)。

就這個(gè)顯示分析器的功能,只有 16.9 Preview 3 才開(kāi)始有的。這也就是為什么我告訴你必須要這個(gè)版本及其以上的才可以的真正原因:分析器都不顯示,你從哪里查看生成器和源代碼呢?
第六步:更替代碼
這里就需要我們更新源代碼了。我們把剛創(chuàng)建的控制臺(tái)的程序的文件改成這樣:
是的,就這兩句話。這是 C# 9 提供的新特性:全局 ?方法。當(dāng)你只寫(xiě)了執(zhí)行邏輯的時(shí)候,編譯器會(huì)自動(dòng)創(chuàng)建一個(gè)
?類(lèi),并帶一個(gè)
?方法;然后這里的執(zhí)行代碼就是被一起塞進(jìn)
?方法里的代碼了。這個(gè)方法自帶一個(gè)
?參數(shù)。所以你壓根不用擔(dān)心你寫(xiě)的代碼會(huì)出問(wèn)題,或者沒(méi)辦法用命令行參數(shù)。
當(dāng)然,這里不是講 C# 新語(yǔ)法的。寫(xiě)完這段代碼的時(shí)候,可能你會(huì)看到報(bào)錯(cuò)信息:提示你 ?命名空間不存在啊,或者
?這個(gè)類(lèi)也不存在啊,這樣類(lèi)似的錯(cuò)誤。

別擔(dān)心,你的源代碼生成器已經(jīng)完成了,所以代碼一會(huì)兒會(huì)自動(dòng)在編譯的時(shí)候生成,因此它一會(huì)兒就是存在的了。
第七步:然后重啟 Visual Studio
這一步應(yīng)該是目前 Source Generator 和 Visual Studio 2019 交互的 bug。我不知道以后 VS 會(huì)不會(huì)修復(fù)這一個(gè)問(wèn)題,但目前必須要這么做才能運(yùn)行成功。
請(qǐng)重啟 VS。
最后一步:運(yùn)行和調(diào)試代碼
在前文描述的過(guò)程里,我們已經(jīng)大概表達(dá)了所有需要編譯和運(yùn)行一個(gè)帶有源代碼生成器的程序,到底應(yīng)該怎么寫(xiě)東西。現(xiàn)在是最后一步了。那自然是調(diào)試代碼。
前文如果嘗試完成后,發(fā)現(xiàn)依舊失敗的話,請(qǐng)嘗試把代碼生成器項(xiàng)目的標(biāo)準(zhǔn)改成 .NET Standard 2.0。.NET Standard 2.1 可以用,但可能第一次需要 2.0 版本的標(biāo)準(zhǔn)進(jìn)行編譯才可以成功。

這個(gè)時(shí)候,你就會(huì)發(fā)現(xiàn),類(lèi)名已經(jīng)不再帶有紅色波浪線了,而是正確的類(lèi)的語(yǔ)義著色,這就說(shuō)明,這個(gè)類(lèi)成功編譯出來(lái)了。
我們可以點(diǎn)進(jìn)去看看。

你將獲得原本我們剛才寫(xiě)的那串代碼,最后生成的東西。
上面的文字說(shuō):“這個(gè)文件是被
?這個(gè)生成器自動(dòng)生成的,且你無(wú)法對(duì)其進(jìn)行修改”。
下面我們來(lái)運(yùn)行一下。
很高興,我們得到了想要的結(jié)果。

那么,整個(gè)項(xiàng)目我們就完成了。
總結(jié)
源代碼生成器的主要作用是用來(lái)在編譯前注入別的代碼(產(chǎn)生代碼)來(lái)達(dá)到額外的、必須在編譯前要完成的功能。源代碼生成器有些時(shí)候格外重要,但不能亂用。請(qǐng)勿濫用代碼注入。
另外,我查了很多的資料,至于我們能否改成現(xiàn)在出的 .NET 5,很抱歉,不能。

可以參考這個(gè)回答:https://stackoverflow.com/a/65480017/13613782(原問(wèn)題是說(shuō),我想把 .NET Standard 2.0 這個(gè)設(shè)定改成 .NET 5,可以嗎)。