C# 9 新特性:代碼生成器、編譯時反射

来源:https://www.cnblogs.com/hez2010/archive/2020/04/30/12810993.html
-Advertisement-
Play Games

前言 今天 .NET 官方博客宣佈 C 9 Source Generators 第一個預覽版發佈,這是一個用戶已經喊了快 5 年特性,今天終於發佈了。 簡介 Source Generators 顧名思義代碼生成器,它允許開發者在代碼編譯過程中獲取查看用戶代碼並且生成新的 C 代碼參與編譯過程,並且可 ...


前言

今天 .NET 官方博客宣佈 C# 9 Source Generators 第一個預覽版發佈,這是一個用戶已經喊了快 5 年特性,今天終於發佈了。

簡介

Source Generators 顧名思義代碼生成器,它允許開發者在代碼編譯過程中獲取查看用戶代碼並且生成新的 C# 代碼參與編譯過程,並且可以很好的與代碼分析器集成提供 Intellisense、調試信息和報錯信息,可以用它來做代碼生成,因此也相當於是一個加強版本的編譯時反射。

使用 Source Generators,可以做到這些事情:

  • 獲取一個 Compilation 對象,這個對象表示了所有正在編譯的用戶代碼,你可以從中獲取 AST 和語義模型等信息
  • 可以向 Compilation 對象中插入新的代碼,讓編譯器連同已有的用戶代碼一起編譯

Source Generators 作為編譯過程中的一個階段執行:

編譯運行 -> [分析源代碼 -> 生成新代碼] -> 將生成的新代碼添加入編譯過程 -> 編譯繼續。

上述流程中,中括弧包括的內容即為 Source Generators 所參與的階段和能做到的事情。

作用

.NET 明明具備運行時反射和動態 IL 織入功能,那這個 Source Generators 有什麼用呢?

編譯時反射 - 0 運行時開銷

拿 ASP.NET Core 舉例,啟動一個 ASP.NET Core 應用時,首先會通過運行時反射來發現 Controllers、Services 等的類型定義,然後在請求管道中需要通過運行時反射獲取其構造函數信息以便於進行依賴註入。然而運行時反射開銷很大,即使緩存了類型簽名,對於剛剛啟動後的應用也無任何幫助作用,而且不利於做 AOT 編譯。

Source Generators 將可以讓 ASP.NET Core 所有的類型發現、依賴註入等在編譯時就全部完成並編譯到最終的程式集當中,最終做到 0 運行時反射使用,不僅利於 AOT 編譯,而且運行時 0 開銷。

除了上述作用之外,gRPC 等也可以利用此功能在編譯時織入代碼參與編譯,不需要再利用任何的 MSBuild Task 做代碼生成啦!

另外,甚至還可以讀取 XML、JSON 直接生成 C# 代碼參與編譯,DTO 編寫全自動化都是沒問題的。

AOT 編譯

Source Generators 的另一個作用是可以幫助消除 AOT 編譯優化的主要障礙。

許多框架和庫都大量使用反射,例如System.Text.Json、System.Text.RegularExpressions、ASP.NET Core 和 WPF 等等,它們在運行時從用戶代碼中發現類型。這些非常不利於 AOT 編譯優化,因為為了使反射能夠正常工作,必須將大量額外甚至可能不需要的類型元數據編譯到最終的原生映像當中。

有了 Source Generators 之後,只需要做編譯時代碼生成便可以避免大部分的運行時反射的使用,讓 AOT 編譯優化工具能夠更好的運行。

例子

INotifyPropertyChanged

寫過 WPF 或 UWP 的都知道,在 ViewModel 中為了使屬性變更可被髮現,需要實現 INotifyPropertyChanged 介面,並且在每一個需要的屬性的 setter 處除法屬性更改事件:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
        }
    }
}

當屬性多了之後將會非常繁瑣,先前 C# 引入了 CallerMemberName 用於簡化屬性較多時候的情況:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            OnPropertyChanged();
        }
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

即,用 CallerMemberName 指示參數,在編譯時自動填充調用方的成員名稱。

但是還是不方便。

如今有了 Source Generators,我們可以在編譯時生成代碼做到這一點了。

為了實現 Source Generators,我們需要寫個實現了 ISourceGenerator 並且標註了 Generator 的類型。

完整的 Source Generators 代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace MySourceGenerator
{
    [Generator]
    public class AutoNotifyGenerator : ISourceGenerator
    {
        private const string attributeText = @"
using System;
namespace AutoNotify
{
    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    sealed class AutoNotifyAttribute : Attribute
    {
        public AutoNotifyAttribute()
        {
        }
        public string PropertyName { get; set; }
    }
}
";

        public void Initialize(InitializationContext context)
        {
            // 註冊一個語法接收器,會在每次生成時被創建
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }

        public void Execute(SourceGeneratorContext context)
        {
            // 添加 Attrbite 文本
            context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8));

            // 獲取先前的語法接收器 
            if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
                return;

            // 創建處目標名稱的屬性
            CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
            Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));

            // 獲取新綁定的 Attribute,並獲取INotifyPropertyChanged
            INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
            INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

            // 遍歷欄位,只保留有 AutoNotify 標註的欄位
            List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
            foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
            {
                SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
                foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
                {
                    // 獲取欄位符號信息,如果有 AutoNotify 標註則保存
                    IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                    if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
                    {
                        fieldSymbols.Add(fieldSymbol);
                    }
                }
            }

            // 按 class 對欄位進行分組,並生成代碼
            foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
            {
                string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
               context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
            }
        }

        private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context)
        {
            if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
            {
                // TODO: 必須在頂層,產生診斷信息
                return null;
            }

            string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();

            // 開始構建要生成的代碼
            StringBuilder source = new StringBuilder($@"
namespace {namespaceName}
{{
    public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
    {{
");

            // 如果類型還沒有實現 INotifyPropertyChanged 則添加實現
            if (!classSymbol.Interfaces.Contains(notifySymbol))
            {
                source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
            }

            // 生成屬性
            foreach (IFieldSymbol fieldSymbol in fields)
            {
                ProcessField(source, fieldSymbol, attributeSymbol);
            }

            source.Append("} }");
            return source.ToString();
        }

        private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
        {
            // 獲取欄位名稱
            string fieldName = fieldSymbol.Name;
            ITypeSymbol fieldType = fieldSymbol.Type;

            // 獲取 AutoNotify Attribute 和相關的數據
            AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
            TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;

            string propertyName = chooseName(fieldName, overridenNameOpt);
            if (propertyName.Length == 0 || propertyName == fieldName)
            {
                //TODO: 無法處理,產生診斷信息
                return;
            }

            source.Append($@"
public {fieldType} {propertyName} 
{{
    get 
    {{
        return this.{fieldName};
    }}
    set
    {{
        this.{fieldName} = value;
        this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
    }}
}}
");

            string chooseName(string fieldName, TypedConstant overridenNameOpt)
            {
                if (!overridenNameOpt.IsNull)
                {
                    return overridenNameOpt.Value.ToString();
                }

                fieldName = fieldName.TrimStart('_');
                if (fieldName.Length == 0)
                    return string.Empty;

                if (fieldName.Length == 1)
                    return fieldName.ToUpper();

                return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
            }

        }

        // 語法接收器,將在每次生成代碼時被按需創建
        class SyntaxReceiver : ISyntaxReceiver
        {
            public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();

            // 編譯中在訪問每個語法節點時被調用,我們可以檢查節點並保存任何對生成有用的信息
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                // 將具有至少一個 Attribute 的任何欄位作為候選
                if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                    && fieldDeclarationSyntax.AttributeLists.Count > 0)
                {
                    CandidateFields.Add(fieldDeclarationSyntax);
                }
            }
        }
    }
}

有了上述代碼生成器之後,以後我們只需要這樣寫 ViewModel 就會自動生成通知介面的事件觸發調用:

public partial class MyViewModel
{
    [AutoNotify]
    private string _text = "private field text";

    [AutoNotify(PropertyName = "Count")]
    private int _amount = 5;
}

上述代碼將會在編譯時自動生成以下代碼參與編譯:

public partial class MyViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    public string Text
    {
        get 
        {
            return this._text;
        }
        set
        {
            this._text = value;
            this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Text)));
        }
    }

    public int Count
    {
        get 
        {
            return this._amount;
        }
        set
        {
            this._amount = value;
            this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Count)));
        }
    }
}

非常方便!

使用時,將 Source Generators 部分作為一個獨立的 .NET Standard 2.0 程式集(暫時不支持 2.1),用以下方式引入到你的項目即可:

<ItemGroup>
  <Analyzer Include="..\MySourceGenerator\bin\$(Configuration)\netstandard2.0\MySourceGenerator.dll" />
</ItemGroup>

<ItemGroup>
  <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj" />
</ItemGroup>

註意需要最新的 .NET 5 preview(寫文章時還在 artifacts 里沒正式 release),並指定語言版本為 preview

<PropertyGroup>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

另外,Source Generators 需要引入兩個 nuget 包:

<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.final" PrivateAssets="all" />
  <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
</ItemGroup>

限制

Source Generators 僅能用於訪問和生成代碼,但是不能修改已有代碼,這有一定原因是出於安全考量。

文檔

Source Generators 處於早期預覽階段,docs.microsoft.com 上暫時沒有相關文檔,關於它的文檔請訪問在 roslyn 倉庫中的文檔:

設計文檔

使用文檔

後記

目前 Source Generators 仍處於非常早期的預覽階段,API 後期還可能會有很大的改動,因此現階段不要用於生產。

另外,關於與 IDE 的集成、診斷信息、斷點調試信息等的開發也在進行中,請期待後續的 preview 版本吧。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 0. 前言 這段時間完成了第二階段的Java作業練習,第一階段是入手,那麼這個階段則是這之後的學習打基礎;這幾次作業主要是加強我們對面向對象的封裝性、繼承性、多態性特征的理解,下麵是我對此次作業的總結分析。 1.作業過程總結 (1) 三次作業之間的知識迭代關係 第4次作業涉及數據檢驗及處理,類的繼承 ...
  • StringBuilder、StringBuffer源碼分析 StringBuilder源碼分析 類結構 StringBuilder使用final關鍵字修飾,和String一樣不可以被繼承 StringBuilder繼承AbstractStringBuilder並實現了Serializable和Ch ...
  • 首先看下ThinkPHP6官方手冊關於多應用的目錄結構: ├─app 應用目錄 │ ├─index 主應用 │ │ ├─controller 控制器目錄 │ │ ├─model 模型目錄 │ │ ├─view 視圖目錄 │ │ ├─config 配置目錄(優先) │ │ └─ ... 更多類庫目錄 ...
  • Gevent 是一個第三方庫,可以輕鬆通過gevent實現併發同步或非同步編程 ...
  • 程式目的:輸入年份和月份,查詢當月的日曆。弄著玩。程式界面:代碼如下:# coding:utf8from tkinter import *from calendar import *from time import *class APP: def __init__(self, master): fr... ...
  • 環境: 介紹 Quickuse.Caching 快速應用緩存組件,提供常用緩存使用方式,目前支持常用的 、`Redis Memcache` 運行時緩存 有時候也本稱作為伺服器緩存、進程緩存、站點緩存、程式緩存、本地緩存......各式各樣,我理解的其實他們都一個東西,都是在程式運行的時候才可以使用的 ...
  • 在之前的文章abp(net core)+easyui+efcore實現倉儲管理系統——入庫管理之九(四十五) 中我們已經實現了修改與刪除入庫單,今天來測試一下入庫單的修改與刪除功能。 ...
  • 如果你經常看開源項目的源碼,你會發現很多Dispose方法中都有這麼一句代碼: ,看過一兩次可能無所謂,看多了就來了興趣,這篇就跟大家聊一聊。 一:背景 1. 在哪發現的 相信現在Mysql在.Net領域中鋪的面越來越廣了,C 對接MySql的MySql.Data類庫的代碼大家可以研究研究,幾乎所有 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...