生成代码从T到T1、T2、Tn自动生成多个类型的泛型实例代码
前言
当你想写一个泛型 <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 安装脚本将以上过程全自动化完成。
参考资料
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。