视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
生成代码从T到T1、T2、Tn自动生成多个类型的泛型实例代码
2020-11-27 22:34:54 责编:小采
文档


前言

当你想写一个泛型 <T> 的类型的时候,是否想过两个泛型参数、三个泛型参数、四个泛型参数或更多泛型参数的版本如何编写呢?是一个个编写?类小还好,类大了就杯具!

事实上,在 Visual Studio 中生成代码的手段很多,本文采用最笨的方式生成,但效果也很明显——代码写得轻松写得爽!

本文主要给大家介绍了关于从T到T1、T2、Tn自动生成多个类型的泛型的方法,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧

我们想要的效果

我们现在有一个泛型的版本:

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)
 {
 // 做某些事情。
 }

 // 做其他事情。
}

希望生成多个泛型的版本:

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:

<#@ 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,然后把我写好的生成代码粘贴到新的类文件中。

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 中调用以上代码即可完成泛型生成。我写了一个简单的版本,可以将每一个命令行参数解析为一个需要进行转换的泛型类文件。

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 结果也是一样的。

在要生成代码的项目中添加“预先生成事件命令行”:

"$(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 安装脚本将以上过程全自动化完成。

参考资料

  • Code Generation and T4 Text Templates - Microsoft Docs
  • Custom Tools - Microsoft Docs
  • 总结

    下载本文
    显示全文
    专题