自动代码生成
曾经有很多同僚发表过类似的观点,程序员的本质,就是懒。
这点我是相当的赞同,因为我也曾幻想过,写个替我写代码的程序,然后每天上班把程序打开,下班把程序关掉。其他时间打打游戏,看看动画。
当然现实是残酷的,一来我没有那个水平写那么智能的程序,二来真写出来估计也会被同僚打死,当然别人写出这种程序,我也肯定在他发表之前,打死他。
我最开始写的自动代码生成工具,是为CSV表格生成自动类,一张表对应一个类,有获取行列的方法。后来才知道这玩意和protobuf思想差不多,然而那时候我还没有用到protobuf。后来又写了一个网络协议处理的半自动工具,只要在Unity的inspector配置好指定的协议,就可以自动生成一部分框架代码。
那时候的自动代码生成工具,使用的是最简单的类模板+关键字替换,以及函数模板+注释标签+代码插入的方式实现的。
虽然简单粗暴,但是确实避免了自己写一些繁琐的重复代码。
随后,在最近的项目中是用到了XLuaFramework,这个框架使用pureMVC架构组织lua代码,这就需要在拼好UI的Prefab之后,手动的获取每个控件的路径,手动创建Mode,Ctrl和View页面,手动填写响应函数,手动……
这听起来头皮发麻,别说累不累,这么多手动,我肯定会忘记其中一两项……再加上某些缺心眼的策划经常改需求,一旦prefab的页面发生了变化,就又会引发一场新的灾难。
于是我这次做的东西比第一次稍微复杂一点,构建了一个LuaCodeBuilder,利用代码构建器+多类注释标签的方式,实现了通过拼好的UGUI的prefab自动生成MVC架构框架页面,并把所有的初始化,路径查询,回调函数注入,以及其他一些可以自动化的代码都写好。并且确定,在//AFX_XXX_BEGIN和//AFX_XXX_END之内的代码为自动生成代码,其他程序员不允许修改这里面的代码。
这么做还是有两个小小的缺点:
一是出现一大堆标签,看起来美感稍微差一些,更要命的是程序员一旦把代码写到两个标签中间,下次再生成的时候,就有可能抹掉这些代码。
二是很多代码,如自动生成的回调函数函数体,在不用的时候,只能手动删除,不能自动删除。
于是我想要做这样一件事情,解决上面两个问题。一个是通过解析器解析整个工程的代码,估计现有的IDE都具备这样的能力,然后根据UGUI的Prafab生成一个描述文件,描述哪些东西是自动生成的。这样可以更加精确的定位自动代码和手动修改的代码。
二是可以使用更少的调用Builder的代码,来生成更复杂的程序。
此外还有一个好处是,使得生成更复杂的代码成为可能。
如果想要实现这个目的,单纯的使用CodeBuilder是不够的。于是我发现了微软已经提供好了CodeDom,也就是代码对象模型这个好东西,虽然他现在不支持Lua代码,但是我可以学习一下他的思想——在内存中,用树状图,表示程序。
这就使得对程序的自动修改,不再是文本和文本替换级别的,而是对象级别的。
同时,我还要去寻找一个开源的,或者自己写一个代码解析工具,把现有Lua代码解析成Dom,并构建起不同源文件之间的联系——这样我可以更灵活的,自动重构代码,比如即使用户(指其他程序员)修改了一个变量,比如一个label,lbl_user_name的名字,并且自己编写了一些代码,比如lbl_user_nane:SetText("xxxx"),当策划将这个lbl改名的时候,我也能够自动重构代码,将其所有引用的地方改成新的变量名。
为了实现这个目标,我需要先学习一下微软的CodeDom的原理。
CodeDom简介
CodeDom就是文档代码对象模型。
这张图是一个不太负责任的代码内存分析模型,不够详细,也不够全面,但是它大概的展示了一个源代码文件,其大概结构。
要在内存中表示一个源代码文件,首先应该将其分解成各种元组件,然后在将他们组成一个树图。
微软的CodeDom更多是用来生成代码而不是解析用的,用来生成的原材料可能是一张CSV表,一个protobuf源文件,或者是一个可视化代码工具——或许这个也是个不错的入手点:)
CodeDom
使用CodeDom自动生成代码,分三部分操作。
1是利用CodeDom构建代码。
2是利用IndentedTextWriter输入带有缩进的代码文件。
3是利用GenerateCodeFromCompileUnit编译代码。
这里我主要涉及的是1和2。
1 CodeCompileUnit
在我之前构建代码的工具中,采用的是文本内存模型的方式,也就是用一个类对象,里面包含一个List<String>数据结构,来记录代码的每一行。然后通过字符串匹配以及正则表达式匹配,子串查找等方式,找到目标代码,目标注释标签等,在进行代码删除,插入——基本上不存在修改的操作,只是删除和插入新代码。
单纯对于CodeDom而言,微软只提供了生成整片代码的机能——看注释就能知道:
大意是这代码是自动生成的,重生成代码会抹掉你的代码,乱改代码可能会引发不可预知的错误。
用它来实现我的最终目标似乎还远远不够,但是分析代码DOM模型还是很有价值的一步。在CodeDom模型中,CodeCompileUnit和我用来组织源代码的类作用几乎是一致的。
最简单的理解,一个CodeComplieUnit可以代表一个.cs文件。他是CodeDom在内存中表示一段代码的根节点。也就是代码树的根节点。
2 CodeNamespace
CodeNamespace在CodeDom中代表着一个命名空间。
这里需要注意几个地方:
- 一个.cs文件里面有多个命名空间是完全OK,并且合法的。
- CodeNamespaceImport可以用来增加using xxxx的代码。
CodeNamespace codeNamespace = new CodeNamespace(ns_name);
List<CodeNamespaceImport> cni_list = new List<CodeNamespaceImport>();
cni_list.Add(new CodeNamespaceImport("UnityEngine"));
cni_list.Add(new CodeNamespaceImport("System.Collections.Generic"));
foreach (var cni in cni_list)
{
codeNamespace.Imports.Add(cni);
}
- using代码块是属于namespace的
而不像很多Unity或者其他IDE,默认把using放到了namespace外面,网上有很多关于using到底应该在namespace里面还是外面的争论,按照微软官方的做法,只能在CodeNamespace中添加命名空间引用,而不能在CodeCompileUnit里面添加namespace引用来看,微软官方是支持吧using代码扔到namespace里面的。
2.向CodeCompileUnit添加Namespace
CodeCompileUnit codeUnit = new CodeCompileUnit();
CodeNamespace codeNamespace = new CodeNamespace(ns_name);
...
codeUnit.Namespaces.Add(codeNamespace);
3 输出代码文件
这用到两个核心类:IndentedTextWriter和CodeDomProvider
如果直接用TextWriter输出代码,将会非常难看,因为没有缩进。IndentedTextWriter对输出代码进行了一层包装,使得生成的代码带有缩进。
实例代码如下:
其中codeUnit是包含了两个空的namespace的CodeCompileUnit对象。
string tabString = " ";
string outputPath = "Assets/SharpCodeGen/outputs/test.cs";
IndentedTextWriter itw = new IndentedTextWriter(new StreamWriter(outputPath, false), tabString);
CodeDomProvider provide = new CSharpCodeProvider();
provide.GenerateCodeFromCompileUnit(codeUnit, itw, new CodeGeneratorOptions());
itw.Flush();
itw.Close();
输出结果如下: