Roslyn 是微軟爲 C# 設計的一套分析器,它具有很強的擴展性。以至於我們只需要編寫很少量的代碼便能夠編譯並執行我們的代碼。
作爲 Roslyn 入門篇文章之一,你將可以通過本文學習如何開始編寫一個 Roslyn 擴展項目 —— 編譯一個類,然後執行其中的一段代碼。
本文是 Roslyn 入門系列之一:
- Roslyn 入門:使用 Visual Studio 的語法可視化(Syntax Visualizer)窗格查看和了解代碼的語法樹
- Roslyn 入門:使用 .NET Core 版本的 Roslyn 編譯並執行跨平臺的靜態的源碼(本文)
- Roslyn 入門:使用 Roslyn 靜態分析現有項目中的代碼
我們希望做什麼?
是否有過在編譯期間修改一段代碼的想法呢?
我曾經在 生成代碼,從 T 到 T1, T2, Tn —— 自動生成多個類型的泛型 一文中提到過這樣的想法,在這篇文章中,我希望只編寫泛型的一個參數的版本 Demo<T>
,然後自動生成 2~16 個參數的版本 Demo<T1, T2>
, Demo<T1, T2, T3>
… Demo<T1, T2, ... T16>
。不過,在那篇文章中,我寫了一個應用程序來完成這樣的事情。我在另一篇文章 如何創建一個基於命令行工具的跨平臺的 NuGet 工具包 中說到我們可以將這樣的應用程序打包成一個 NuGet 工具包。也就是說,利用這兩種不同的技術,我們可以製作一個在編譯期間生成多個泛型的 NuGet 工具包。
不過,這樣的生成方式不夠通用。今天我們想生成泛型,明天我們想生成多語言類,後天我們又想生成代理類。能否做一種通用的方式來完成這樣的任務呢?
於是,我想到可以使用 Roslyn。在項目中編寫一段轉換代碼,我們使用通用的方式去編譯和執行這段代碼,以便完成各種各樣日益增加的類型轉換需求。具體來說,就是 使用 Roslyn 編譯一段代碼,然後執行它。
準備工作
與之前在 Roslyn 入門:使用 Roslyn 靜態分析現有項目中的代碼 中的不同,我們這次無需打開解決方案或者項目,而是直接尋找並編譯源代碼文件。所以(利好消息),我們這回可以使用 .NET Core 跨平臺版本的 Roslyn 了。所以爲了充分有跨平臺特性,我們創建控制檯應用 (.NET Core)
。
▲ 千萬不要吐槽相比於上一個入門教程來說,這次的界面變成了英文
安裝必要的 NuGet 包
這次不需要完整的 .NET Framework 環境,也不需要打開解決方案和項目這種重型 API,所以一個簡單的 NuGet 包足矣:
準備一份用於編譯和執行代碼文件
我直接使用 生成代碼,從 T 到 T1, T2, Tn —— 自動生成多個類型的泛型 這篇文章中的例子。把其中最關鍵的文件拿來用於編譯和生成試驗。
using System.Linq;
using static System.Environment;
namespace Walterlv.Demo.Roslyn
{
public class GenericGenerator
{
private static readonly string GeneratedAttribute =
@"[System.CodeDom.Compiler.GeneratedCode(""walterlv"", ""1.0"")]";
public string Transform(string originalCode, int genericCount)
{
if (genericCount == 1)
{
return originalCode;
}
var content = originalCode
// 替換泛型。
.Replace("<out T>", FromTemplate("<{0}>", "out T{n}", ", ", genericCount))
.Replace("Task<T>", FromTemplate("Task<({0})>", "T{n}", ", ", genericCount))
.Replace("Func<T, Task>", FromTemplate("Func<{0}, Task>", "T{n}", ", ", genericCount))
.Replace(" T, Task>", FromTemplate(" {0}, Task>", "T{n}", ", ", genericCount))
.Replace("(T, bool", FromTemplate("({0}, bool", "T{n}", ", ", genericCount))
.Replace("var (t, ", FromTemplate("var ({0}, ", "t{n}", ", ", genericCount))
.Replace(", t)", FromTemplate(", {0})", "t{n}", ", ", genericCount))
.Replace("return (t, ", FromTemplate("return ({0}, ", "t{n}", ", ", genericCount))
.Replace("<T>", FromTemplate("<{0}>", "T{n}", ", ", genericCount))
.Replace("(T value)", FromTemplate("(({0}) value)", "T{n}", ", ", genericCount))
.Replace("(T t)", FromTemplate("({0})", "T{n} t{n}", ", ", genericCount))
.Replace("(t)", FromTemplate("({0})", "t{n}", ", ", genericCount))
.Replace("var t =", FromTemplate("var ({0}) =", "t{n}", ", ", genericCount))
.Replace(" T ", FromTemplate(" ({0}) ", "T{n}", ", ", genericCount))
.Replace(" t;", FromTemplate(" ({0});", "t{n}", ", ", genericCount))
// 生成 [GeneratedCode]。
.Replace(" public interface ", $" {GeneratedAttribute}{NewLine} public interface ")
.Replace(" public class ", $" {GeneratedAttribute}{NewLine} public class ")
.Replace(" public sealed class ", $" {GeneratedAttribute}{NewLine} public sealed class ");
return content.Trim();
}
private static string FromTemplate(string template, string part, string seperator, int count)
{
return string.Format(template,
string.Join(separator, Enumerable.Range(1, count).Select(x => part.Replace("{n}", x.ToString()))));
}
}
}
這份代碼你甚至可以直接複製到你的項目中,一定是可以編譯通過的。
編譯這份代碼
使用 Roslyn 編譯一份代碼是非常輕鬆愉快的。寫出以下這三行就夠了:
var syntaxTree = CSharpSyntaxTree.ParseText("那份代碼的全文內容");
var compilation = CSharpCompilation.Create("assemblyname", new[] { syntaxTree },
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var result = compilation.Emit(ms);
好吧,其實我是開玩笑的,這三行代碼確實能夠跑通過,不過得到的 result
是編譯不通過的結局。爲了能夠在多數情況下編譯通過,我寫了更多的代碼:
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Walterlv.Demo.Roslyn
{
class Program
{
static void Main(string[] args)
{
// 大家都知道在代碼中寫死文件路徑是不對的,不過,我們這裏是試驗。放心,我會改的!
var file = @"D:\Development\Demo\Walterlv.Demo.Roslyn\Walterlv.Demo.Roslyn.Tests\GenericGenerator.cs";
var originalText = File.ReadAllText(file);
var syntaxTree = CSharpSyntaxTree.ParseText(originalText);
var type = CompileType("GenericGenerator", syntaxTree);
// 於是我們得到了編譯後的類型,但是還不知道怎麼辦。
}
private static Type CompileType(string originalClassName, SyntaxTree syntaxTree)
{
// 指定編譯選項。
var assemblyName = $"{originalClassName}.g";
var compilation = CSharpCompilation.Create(assemblyName, new[] { syntaxTree },
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(
// 這算是偷懶了嗎?我把 .NET Core 運行時用到的那些引用都加入到引用了。
// 加入引用是必要的,不然連 object 類型都是沒有的,肯定編譯不通過。
AppDomain.CurrentDomain.GetAssemblies().Select(x => MetadataReference.CreateFromFile(x.Location)));
// 編譯到內存流中。
using (var ms = new MemoryStream())
{
var result = compilation.Emit(ms);
if (result.Success)
{
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
return assembly.GetTypes().First(x => x.Name == originalClassName);
}
throw new CompilingException(result.Diagnostics);
}
}
}
}
執行編譯後的代碼
既然得到了類型,那麼執行這份代碼其實毫無壓力,因爲我們都懂得反射(好吧,我假裝你懂反射)。
var transformer = Activator.CreateInstance(type);
var newContent = (string) type.GetMethod("Transform").Invoke(transformer,
new object[] { "某個泛型類的全文,假裝我是泛型類 Walterlv<T> is a sb.", 2 });
執行完之後,裏面的 Walterlv<T>
真的變成了 Walterlv<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
啊。說明成功執行。
下面進入高階模式
作爲入門篇,我纔不會進入高階模式呢!如果你想實現如本文開頭所說的更通用的效果,歡迎發動你的大腦讓想象力迸發。當然,如果你確實想不出來,歡迎在下方評論,我將盡快回復。