前言
當你想寫一個泛型 <T> 的類型的時候,是否想過兩個泛型參數、三個泛型參數、四個泛型參數或更多泛型參數的版本如何編寫呢?是一個個編寫?類小還好,類大了就杯具!
事實上,在 Visual Studio 中生成代碼的手段很多,本文采用最笨的方式生成,但效果也很明顯——代碼寫得輕松寫得爽!
本文主要給大家介紹了關于從T到T1、T2、Tn自動生成多個類型的泛型的方法,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧
我們想要的效果
我們現在有一個泛型的版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class Demo<T> { public Demo(Action<T> demo) { _demo = demo ?? throw new ArgumentNullException(nameof(action)); } private Action<T> _demo; public async Task<T> DoAsync(T t) { // 做某些事情。 } // 做其他事情。 } |
希望生成多個泛型的版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class Demo<T1, T2> { public Demo(Action<T1, T2> demo) { _demo = demo ?? throw new ArgumentNullException(nameof(action)); } private Action<T1, T2> _demo; public async Task<(T1, T2)> DoAsync(T1 t1, T2 t2) { // 做某些事情。 } // 做其他事情。 } |
注意到類型的泛型變成了多個,參數從一個變成了多個,返回值從單個值變成了元組。
于是,怎么生成呢?
回顧 Visual Studio 那些生成代碼的方式
Visual Studio 原生自帶兩種代碼生成方式。
第一種:T4 文本模板
事實上 T4 模板算是 Visual Studio 最推薦的方式了,因為你只需要編寫一個包含占位符的模板文件,Visual Studio 就會自動為你填充那些占位符。
那么 Visual Studio 用什么填充?是的,可以在模板文件中寫 C# 代碼!比如官方 DEMO:
1
2
3
4
5
6
7
8
9
|
<#@ output extension= ".txt" #> <#@ assembly name= "System.Xml" #> <# System.Xml.XmlDocument configurationData = ...; // Read a data file here. #> namespace Fabrikam.<#= configurationData.SelectSingleNode( "jobName" ).Value #> { ... // More code here. } |
這代碼寫哪兒呢?在項目上右鍵新建項,然后選擇“運行時文本模板”。
T4 模板編輯后一旦保存(Ctrl+S),代碼立刻生成。
有沒有覺得這代碼著色很恐怖?呃……根本就沒有代碼著色好嗎!即便如此,T4 本身也是非常強悍的代碼生成方式。
這不是本文的重點,于是感興趣請閱讀官方文檔 Code Generation and T4 Text Templates - Microsoft Docs 學習。
第二種:文件屬性中的自定義工具
右鍵選擇項目中的一個代碼文件,然后選擇“屬性”,你將看到以下內容:
就是這里的自定義工具。在這里填寫工具的 Key,那么一旦這個文件保存,就會運行自定義工具生成代碼。
那么 Key 從哪里來?這貨居然是從注冊表拿的!也就是說,如果要在團隊使用,還需要寫一個注冊表項!即便如此,自定義工具本身也是非常強悍的代碼生成方式。
這也不是本文的重點,于是感興趣請閱讀官方文檔 Custom Tools - Microsoft Docs 學習。
第三種:笨笨的編譯生成事件
這算是通常項目用得最多的方式了,因為它可以在不修改用戶開發環境的情況下執行幾乎任何任務。
右鍵項目,選擇屬性,進入“生成事件”標簽:
在“預先生成事件命令行”中填入工具的名字和參數,便可以生成代碼。
制作生成泛型代碼的工具
我們新建一個控制臺項目,取名為 CodeGenerator,然后把我寫好的生成代碼粘貼到新的類文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
using System; using System.Linq; using static System.Environment; namespace Walterlv.BuildTools { public class GenericTypeGenerator { private static readonly string GeneratedHeader = $ @"//------------------------------------------------------------------------------ // <auto-generated> // 此代碼由工具生成。 // 運行時版本:{Environment.Version.ToString(4)} // // 對此文件的更改可能會導致不正確的行為,并且如果 // 重新生成代碼,這些更改將會丟失。 // </auto-generated> //------------------------------------------------------------------------------ #define GENERATED_CODE " ; private static readonly string GeneratedFooter = $ @"" ; private readonly string _genericTemplate; private readonly string _toolName; public GenericTypeGenerator( string toolName, string genericTemplate) { _toolName = toolName ?? throw new ArgumentNullException(nameof(toolName)); _genericTemplate = genericTemplate ?? throw new ArgumentNullException(nameof(toolName)); } public string Generate( int genericCount) { var toolName = _toolName; var toolVersion = "1.0" ; var GeneratedAttribute = $ "[System.CodeDom.Compiler.GeneratedCode(\"{toolName}\", \"{toolVersion}\")]" ; var content = _genericTemplate // 替換泛型。 .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 GeneratedHeader + NewLine + content.Trim() + NewLine + GeneratedFooter; } private static string FromTemplate( string template, string part, string separator, int count) { return string .Format(template, string .Join(separator, Enumerable.Range(1, count).Select(x => part.Replace( "{n}" , x.ToString())))); } } } |
這個類中加入了非常多種常見的泛型字符串特征,當然是采用最笨的字符串替換方法。如果感興趣優化優化,可以用正則表達式,或者使用 Roslyn 擴展直接拿語法樹。
于是,在 Program.cs 中調用以上代碼即可完成泛型生成。我寫了一個簡單的版本,可以將每一個命令行參數解析為一個需要進行轉換的泛型類文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
using System.IO; using System.Linq; using System.Text; using Walterlv.BuildTools; class Program { static void Main( string [] args) { foreach (var argument in args) { GenerateGenericTypes(argument, 4); } } private static void GenerateGenericTypes( string file, int count) { // 讀取原始文件并創建泛型代碼生成器。 var template = File.ReadAllText(file, Encoding.UTF8); var generator = new GenericTypeGenerator(template); // 根據泛型個數生成目標文件路徑和文件內容。 var format = GetIndexedFileNameFormat(file); ( string targetFileName, string targetFileContent)[] contents = Enumerable.Range(2, count - 1).Select(i => ( string .Format(format, i), generator.Generate(i)) ).ToArray(); // 寫入目標文件。 foreach (var writer in contents) { File.WriteAllText(writer.targetFileName, writer.targetFileContent); } } private static string GetIndexedFileNameFormat( string fileName) { var directory = Path.GetDirectoryName(fileName); var name = Path.GetFileNameWithoutExtension(fileName); if (name.EndsWith( "1" )) { name = name.Substring(0, name.Length - 1); } return Path.Combine(directory, name + "{0}.cs" ); } } |
考慮到這是 Demo 級別的代碼,我將生成的泛型個數直接寫到了代碼當中。這段代碼的意思是按文件名遞增生成多個泛型類。
例如,有一個泛型類文件 Demo.cs,則會在同目錄生成 Demo2.cs,Demo3.cs,Demo4.cs。當然,Demo.cs 命名為 Demo1.cs 結果也是一樣的。
在要生成代碼的項目中添加“預先生成事件命令行”:
1
|
"$(ProjectDir)..\CodeGenerator\$(OutDir)net47\CodeGenerator.exe" "$(ProjectDir)..\Walterlv.Demo\Generic\IDemoFile.cs" "$(ProjectDir)..\..\Walterlv.Demo\Generic\DemoFile.cs" |
現在,編譯此項目,即可生成多個泛型類了。
彩蛋
如果你仔細閱讀了 GenericTypeGenerator 類的代碼,你將注意到我為生成的文件加上了條件編譯符“GENERATED_CODE”。這樣,你便可以使用 #ifdef GENERATED_CODE
來處理部分不需要進行轉換或轉換有差異的代碼了。
這時寫代碼,是不是完全感受不到正在寫模板呢?既有代碼著色,又適用于團隊其他開發者的開發環境。是的,個人認為如果帶來便捷的同時注意不到工具的存在,那么這個工具便是好的。
如果將傳參改為自動尋找代碼文件,將此工具發布到 NuGet,那么可以通過 NuGet 安裝腳本將以上過程全自動化完成。
參考資料
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:https://walterlv.com/post/generate-code-of-generic-types.html