当前位置: 首页 > news >正文

生成代码,从 T 到 T1, T2, Tn —— 自动生成多个类型的泛型

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

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


我们想要的效果

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

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}", 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 ");
            return GeneratedHeader + NewLine + content.Trim() + NewLine + GeneratedFooter;
        }

        private static string FromTemplate(string template, string part, string seperator, int count)
        {
            return string.Format(template,
                string.Join(seperator, 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.csDemo3.csDemo4.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

相关文章:

  • 应该抛出什么异常?不应该抛出什么异常?(.NET/C#)
  • 关闭模态窗口后,父窗口居然跑到了其他窗口的后面
  • 语义耦合(Semantic Coupling)
  • .NET Core/Framework 创建委托以大幅度提高反射调用的性能
  • 在 Windows 安装期间将 MBR 磁盘转换为 GPT 磁盘
  • 解决大于 4GB 的 Windows 10 镜像在 UEFI 模式下的安装问题
  • 为什么 UEFI 方式启动的 U 盘必须使用 FAT32 文件系统?
  • 不再为命名而苦恼!使用 MSTestEnhancer 单元测试扩展,写契约就够了
  • Windows 10 自带那么多图标,去哪里找呢?
  • 如何删除 Windows 10 系统生成的 WindowsApps 文件夹
  • 命令“xxx.exe xxx”已退出,代码为 n。这些错误是什么意思?
  • 将 async/await 异步代码转换为安全的不会死锁的同步代码
  • 屏幕上那个灰色带有数字的框是什么?看着好难受!
  • Roslyn 入门:使用 Roslyn 静态分析现有项目中的代码
  • Roslyn 入门:使用 Visual Studio 的语法可视化窗格查看和了解代码的语法树
  • JS中 map, filter, some, every, forEach, for in, for of 用法总结
  • “大数据应用场景”之隔壁老王(连载四)
  • Android Volley源码解析
  • github从入门到放弃(1)
  • Mocha测试初探
  • node-glob通配符
  • seaborn 安装成功 + ImportError: DLL load failed: 找不到指定的模块 问题解决
  • Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比...
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • Web Storage相关
  • 飞驰在Mesos的涡轮引擎上
  • 计算机在识别图像时“看到”了什么?
  • 简单基于spring的redis配置(单机和集群模式)
  • 来,膜拜下android roadmap,强大的执行力
  • 浅谈web中前端模板引擎的使用
  • 用mpvue开发微信小程序
  • 原创:新手布局福音!微信小程序使用flex的一些基础样式属性(一)
  • 原生 js 实现移动端 Touch 滑动反弹
  • #预处理和函数的对比以及条件编译
  • (20050108)又读《平凡的世界》
  • (M)unity2D敌人的创建、人物属性设置,遇敌掉血
  • (顶刊)一个基于分类代理模型的超多目标优化算法
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (源码版)2024美国大学生数学建模E题财产保险的可持续模型详解思路+具体代码季节性时序预测SARIMA天气预测建模
  • ******之网络***——物理***
  • .net 使用ajax控件后如何调用前端脚本
  • .net的socket示例
  • .NET与java的MVC模式(2):struts2核心工作流程与原理
  • :如何用SQL脚本保存存储过程返回的结果集
  • [100天算法】-x 的平方根(day 61)
  • [4.9福建四校联考]
  • [ACTF2020 新生赛]Upload 1
  • [Android Pro] listView和GridView的item设置的高度和宽度不起作用
  • [Android]使用Android打包Unity工程
  • [Angular 基础] - 指令(directives)
  • [E链表] lc83. 删除排序链表中的重复元素(单链表+模拟)
  • [Java] IDEA Scala环境搭建
  • [Java] 模拟Jdk 以及 CGLib 代理原理
  • [JavaWeb学习] tomcat简介、安装及项目部署