代码生成:CodeDom分析(一)

自动代码生成

曾经有很多同僚发表过类似的观点,程序员的本质,就是懒。

 

这点我是相当的赞同,因为我也曾幻想过,写个替我写代码的程序,然后每天上班把程序打开,下班把程序关掉。其他时间打打游戏,看看动画。

 

当然现实是残酷的,一来我没有那个水平写那么智能的程序,二来真写出来估计也会被同僚打死,当然别人写出这种程序,我也肯定在他发表之前,打死他。

 

我最开始写的自动代码生成工具,是为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中代表着一个命名空间。

 

这里需要注意几个地方:

 

  1. 一个.cs文件里面有多个命名空间是完全OK,并且合法的。

 

  1. 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);
}

 

  1. 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();

 

输出结果如下:

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章