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

事实上,在 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 安装脚本将以上过程全自动化完成。


参考资料

生成代码,从 T1 到 T16 —— 自动生成多个类型的泛型的更多相关文章

  1. TensorFlow从1到2(十二)生成对抗网络GAN和图片自动生成

    生成对抗网络的概念 上一篇中介绍的VAE自动编码器具备了一定程度的创造特征,能够"无中生有"的由一组随机数向量生成手写字符的图片. 这个"创造能力"我们在模型中 ...

  2. java如何在eclipse编译时自动生成代码

    用eclipse写java代码,自动编译时,如何能够触发一个动作,这个动作是生成本项目的代码,并且编译完成后,自动生成的代码也编译好了, java编辑器中就可以做到对新生成的代码的自动提示? 不生成代 ...

  3. python之gui-tkinter可视化编辑界面 自动生成代码

    首先提供资源链接 http://pan.baidu.com/s/1kVLOrIn#list/path=%2F

  4. 代码自动生成工具MyGeneration之一(程序员必备工具)

    代码自动生成工具MyGeneration之一(程序员必备工具) 转 分类: C#2008-08-06 18:12 16064人阅读 评论(12) 收藏 举报 工具数据库相关数据库stringbrows ...

  5. 代码自动生成工具MyGeneration之一

    前段时间用C#做网站,用到了大量数据库相关的东西.网站采用3层结构,即数据访问层(Data Access Layer),业务逻辑层(Business Logic Layer),页面表现层().做了一段 ...

  6. 基于数据库的代码自动生成工具,生成JavaBean、生成数据库文档、生成前后端代码等(v6.0.0版)

    TableGo v6.0.0 版震撼发布,此次版本更新如下: 1.UI界面大改版,组件大调整,提升界面功能的可扩展性. 2.新增BeautyEye主题,界面更加清新美观,也可以通过配置切换到原生Jav ...

  7. FXForms,自动生成iOS表单

    1.简介 FXForms是一个简单的表单提交框架,他的作者是鼎鼎大名的 Nick Lockwood,你也许听说过他的其他的一些框架,比如 iCarousel. 为什么使用FxForms? 表单处理简单 ...

  8. JDBC学习笔记(6)——获取自动生成的主键值&处理Blob&数据库事务处理

    获取数据库自动生成的主键 [孤立的技术是没有价值的],我们这里只是为了了解具体的实现步骤:我们在插入数据的时候,经常会需要获取我们插入的这一行数据对应的主键值. 具体的代码实现: /** * 获取数据 ...

  9. 使用jsdoc-toolkit来自动生成js api文档

    近来前端组小盆友开发的类库越来越多,很多情况下彼此不知道写了些什么方法,为了更好的合作提高工作效率,找了个比较好的api文档生成方法.使用jsdoc-toolkit来自动生成js api文档. 一.  ...

随机推荐

  1. Codeforces Round #360 (Div. 2) D. Remainders Game

    D. Remainders Game time limit per test 1 second memory limit per test 256 megabytes input standard i ...

  2. Moment.js的一些用法

    前记:项目开发用到了日历插件(Pikaday.js),同时也用到了Moment.js(日期处理类库) 1.subtract:减去,下面代码的意思是减去1天 this.yestdayStr = mome ...

  3. 如何使移动web页面禁止横屏?

    https://segmentfault.com/q/1010000005813183 一般只有移动版有这种需求,我们一般不去禁止,而是比例缩放,css实现,竖屏1rem = 9pt ,横屏1rem ...

  4. 关于Gulp

    Gulp & webpack 配置详解http://www.jianshu.com/p/2d9ed1fe3e8c 使用 Gulphttp://hwaphon.site/?p=439 前端构建工 ...

  5. 配置Eclipse可以查看JDK类库源码

    一.配置方法 配置Eclipse可以查看JDK类库源码 Window->Preferences->Java->Installed JREs 若没有JRE,需要自己添加进来,有的话,点 ...

  6. import sys sys.path.append(...)

    模块搜索路径: 当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错 默认情况下,Python解释器会搜索当前目录.所有已安装的内置模块和第三方模块,搜索路 ...

  7. 【转】通过blktrace, debugfs分析磁盘IO

    前几天微博上有同学问我磁盘util达到了100%时程序性能下降的问题,由于信息实在有限,我也没有办法帮太大的忙,这篇blog只是想给他列一下在磁盘util很高的时候如何通过blktrace+debug ...

  8. main函数的参数:argc和argv

    程序一般是从main函数开始执行的,main函数标准格式: int main(int argc,char **argv) 其中argc是来自shell(或CMD)的参数的个数,argv是char型的二 ...

  9. Linux 内核驱动自动创建设备节点并挂载设备

    *注:本文来自http://blog.csdn.net/lwj103862095/article/details/17470573 一.首先需要在最开始定义两个数据结构: static struct ...

  10. Linux之FTP服务

     一.ftp服务 ftp是一个文件传输协议(File Transfer Protocal).lftp相当于一个浏览器,用来向服务器发送请求的. 进行FTP服务的相关操作的时候,要先修改 vim /et ...