Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >生成代码,从 T 到 T1, T2, Tn —— 自动生成多个类型的泛型

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

作者头像
walterlv
发布于 2018-09-18 05:47:42
发布于 2018-09-18 05:47:42
1.5K00
代码可运行
举报
运行总次数:0
代码可运行

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

发布于 2018-01-31 05:38 更新于 2018-05-25 12:33

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

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


我们想要的效果

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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)
    {
        // 做某些事情。
    }

    // 做其他事情。
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,然后把我写好的生成代码粘贴到新的类文件中。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 结果也是一样的。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
"$(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 ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-01-31 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Razor Engine,实现代码生成器的又一件利器
Razor Engine,之前仅仅是ASP.NET MVC的一种View引擎,目前已经完全成为一种可以独立使用的模版引擎,并且已经成为了CodePlex上一个开源的项目(http://razorengine.codeplex.com/)。对于使用过ASP.NET MVC Razor视图引擎的朋友们一定已经领略过它的灵活性和易用性,在这篇文章中我们将利用它来实现一个代码生成器使我们可以以Razor的语法来定义代码模版。[源代码从这里下载] 在《一个简易版的T4代码生成"框架"》这篇文章中,我创建了一个能够
蒋金楠
2018/01/15
1.8K0
Razor Engine,实现代码生成器的又一件利器
Roslyn 入门:使用 .NET Core 版本的 Roslyn 编译并执行跨平台的静态的源码
发布于 2018-05-25 13:24 更新于 2018-06-02 01:26
walterlv
2018/09/18
1.5K0
Roslyn 入门:使用 .NET Core 版本的 Roslyn 编译并执行跨平台的静态的源码
0x01 - 我的第一个 Object Visitor
我们需要一个测试项目来演示如何创建一个属于你的第一个 Object Visitor。
newbe36524
2020/12/14
4740
0x01 - 我的第一个 Object Visitor
从数据到代码——通过代码生成机制实现强类型编程[上篇]
我不知道大家对CodeDOM的代码生成机制是否熟悉,但是有一点可以确定:如果你使用过Visual Studio,你就应该体验过它带给我们在编程上的便利。随便列举三种典型的代码生成的场景:在创建强类型DataSet的时候,VS会自动根据Schema生成相应的C#或者VB.NET代码;当我们编辑Resource文件的时候,相应的的后台代码也会自动生成;当我们通过添加Web Reference调用Web Service或者WCF Service的时候,VS会自动生成服务代理的代码和相应的配置。总的来说,通过和VS
蒋金楠
2018/01/16
1.4K0
实现一个 WPF 版本的 ConnectedAnimation
2017-12-25 11:44
walterlv
2018/09/18
6710
实现一个 WPF 版本的 ConnectedAnimation
全新升级的AOP框架Dora.Interception[6]: 框架设计和实现原理
本系列前面的五篇文章主要介绍Dora.Interception(github地址,觉得不错不妨给一颗星)的编程模式以及对它的扩展定制,现在我们来聊聊它的设计和实现原理。
蒋金楠
2022/06/30
5490
全新升级的AOP框架Dora.Interception[6]: 框架设计和实现原理
.NET静态代码织入——肉夹馍(Rougamo)发布2.0
肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应用启动的初始化时间让服务更快可用,同时还能对静态方法进行AOP。
郑子铭
2023/10/23
3020
.NET静态代码织入——肉夹馍(Rougamo)发布2.0
C# 通过T4自动生成代码
通过T4模板生成代码,运行时实现 关键代码段:Host using Microsoft.VisualStudio.TextTemplating; using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeGene
FreeTimeWorker
2020/08/31
7230
SilverLight企业应用框架设计【四】实体层设计+为客户端动态生成服务代理(自己实现RiaService)
但是在silverlight客户端用处就非常大(等会会说道为silverlight客户端自动生成实体类型,silverlight 4.0是有Entity类的)
liulun
2022/05/09
7630
SilverLight企业应用框架设计【四】实体层设计+为客户端动态生成服务代理(自己实现RiaService)
C#/.NET 调试的时候显示自定义的调试信息(DebuggerDisplay 和 DebuggerTypeProxy)
使用 Visual Studio 调试 .NET 程序的时候,在局部变量窗格或者用鼠标划到变量上就能查看变量的各个字段和属性的值。默认显示的是对象 ToString() 方法调用之后返回的字符串,不过如果 ToString() 已经被占作它用,或者我们只是希望在调试的时候得到我们最希望关心的信息,则需要使用 .NET 中调试器相关的特性。
walterlv
2020/02/10
1.4K0
使用 Roslyn 分析代码注释,给 TODO 类型的注释添加负责人、截止日期和 issue 链接跟踪
如果某天改了一点代码但是没有完成,我们可能会在注释里面加上 // TODO。如果某个版本为了控制影响范围临时使用不太合适的方法解了 Bug,我们可能也会在注释里面加上 // TODO。但是,对于团队项目来说,一个人写的 TODO 可能过了一段时间就淹没在大量的 TODO 堆里面了。如果能够强制要求所有的 TODO 被跟踪,那么代码里面就比较容易能够控制住 TODO 的影响了。
walterlv
2023/10/23
4351
使用 Roslyn 分析代码注释,给 TODO 类型的注释添加负责人、截止日期和 issue 链接跟踪
基于 Roslyn 同时为 Visual Studio 插件和 NuGet 包开发 .NET/C# 源代码分析器 Analyzer 和修改器 CodeFixProvider
Roslyn 是 .NET 平台下十分强大的编译器,其提供的 API 也非常丰富好用。本文将基于 Roslyn 开发一个 C# 代码分析器,你不止可以将分析器作为 Visual Studio 代码分析和重构插件发布,还可以作为 NuGet 包发布。不管哪一种,都可以让我们编写的 C# 代码分析器工作起来并真正起到代码建议和重构的作用。
walterlv
2023/10/23
8690
基于 Roslyn 同时为 Visual Studio 插件和 NuGet 包开发 .NET/C# 源代码分析器 Analyzer 和修改器 CodeFixProvider
.net core HttpClient 使用之消息管道解析(二)
前面分享了 .net core HttpClient 使用之掉坑解析(一),今天来分享自定义消息处理HttpMessageHandler和PrimaryHttpMessageHandler 的使用场景和区别
Jlion
2022/04/07
7060
.net core HttpClient 使用之消息管道解析(二)
.NET Core/Framework 创建委托以大幅度提高反射调用的性能
发布于 2018-02-07 09:45 更新于 2018-02-27 11:58
walterlv
2018/09/18
5410
.NET Core/Framework 创建委托以大幅度提高反射调用的性能
2020-3-3-使用T4模板进行C#代码生成
有过前端开发经验的同学一定了解模板文件的重要用户。其实C#也有类似的模板功能(T4模板),不仅可以生成html文件,还可以生成代码。今天就给大家介绍一下。
黄腾霄
2020/06/10
3.1K0
如何根据一个绝对文件路径生成一个相对文件路径
发布于 2018-06-07 11:30 更新于 2018-09-01 00:04
walterlv
2018/09/18
1.1K0
Roslyn 入门:使用 Roslyn 静态分析现有项目中的代码
发布于 2018-03-18 12:45 更新于 2018-06-02 01:26
walterlv
2018/09/18
1.8K0
Roslyn 入门:使用 Roslyn 静态分析现有项目中的代码
.NET/C# 判断某个类是否是泛型类型或泛型接口的子类型
2018-09-01 08:28
walterlv
2018/09/18
4.4K0
C#历代版本新特性——面试题常用
掌握一门语言,当然要掌握它的特性,而随着C#历代版本的迭代更替,C#语言也日趋完善,在C#2.0~C#4.0版本所带来的新的语法特性格外重要。C#的新特性,其本质都是“语法糖”,目的是提升开发效率,在编译时会被编译器转成原始语法。下面按照版本顺序依次介绍其中在日常开发中比较常用的部分。 C# 1.0 特性 第1个版本,编程语言最基础的特性。 Classes:面向对象特性,支持类类型 Structs:结构 Interfaces:接口 Events:事件 Properties:属性,类的成员,提供访问字段
李郑
2018/03/06
2K0
ASP.NET Core 中的 ObjectPool 对象重用(二)
上一篇文章主要介绍了ObjectPool的理论知识,再来介绍一下Microsoft.Extensions.ObjectPool是如何实现的.
HueiFeng
2020/01/22
1.5K0
推荐阅读
相关推荐
Razor Engine,实现代码生成器的又一件利器
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验