CodeDOM Go Kit: CodeDOM 已逝,CodeDOM 万岁





5.00/5 (6投票s)
如果您使用 CodeDOM,这是一个不可或缺的软件包,可以使其变得非常棒。
引言
我真的很讨厌 CodeDOM,你们也应该如此。嗯,让我们先回顾一下。我喜欢 CodeDOM 的想法,但不喜欢它的实现。它笨拙、粗糙且极其有限。如果我不在乎 .NET 中语言无关的代码生成,我根本不会使用它。
然而,我确实在乎这些。我还关心利用 Microsoft 的 XML 序列化代码生成器等功能,并将其输出修改成不太糟糕的东西。
此外,Roslyn 在平台可用性和语言支持方面似乎有限,这似乎固定为 VB 和 C#。与此同时,F# 团队发布了一个 CodeDOM 提供程序,这次是为他们的语言准备的,所以看起来 CodeDOM 仍然可以生成 Roslyn 无法生成的语言的代码,并且可以在 Roslyn 目前无法运行的设备上运行。我可能对平台有所误解,也许 .NET 5 会改变一切,但这就是我们目前的状况。
以下是 CodeDOM 一直需要的一些功能:
- 一个
Parse()
函数。天哪,那该多酷啊。 - 一个
Eval()
函数,即使它只能执行某些表达式 - 一个
Visit()
函数,或者其他搜索图的方法
我决定终于对此做些什么,并且随之而来的是一种非常快速、简洁地构建和编辑 CodeDOM 树的方法。
挽起袖子吧,我们要开始启动这个弗兰肯斯坦了。
构建这个大杂烩
源代码分发包在项目目录中包含两个二进制文件。它们在运行时不使用,仅在构建步骤中用于生成SlangTokenizer.cs。该工具名为 Rolex,我在“进一步阅读”部分提供了指向 CodeProject 文章的链接。这些对于项目构建来说不是必需的,但它们允许对SlangTokenizer.rl的编辑反映在相关的源文件名中。没有这个构建步骤,即使SlangTokenizer.rl发生变化,SlangTokenizer.cs也不会改变。
使用这个烂摊子
首先,让我们谈谈解析。不,我并没有为 CodeDOM 曾经渲染过的每种语言实现解析器。我所做的是为我称之为Slang的 CodeDOM 兼容的 C# 子集创建了一个解析器。用 Slang 编写的代码将解析为 CodeDOM 图,从而渲染到存在足够 CodeDOM 提供程序的任何语言。将其视为你可以转换为其他语言(如 VB)的 C# 子集。
Slang 基本上消除了构建 CodeDOM 树的大部分繁重工作。这少量魔法会让你通过以下方式创建一个表达式:
CodeExpression expr = SlangParser.ParseExpression("(a + b * c - d) > e");
你也可以解析整个编译单元——一次处理整个文件,这样你就永远不需要直接处理 CodeDOM 了。所以我们已经完成了解析。
接下来,这只适用于有限数量的用途,除非你当然想做一个计算器应用程序。但是,在我们的 CodeDomResolver
对象上,我们有一个 Evaluate()
方法,它接受 CodeExpression
形式的代码并解释它,给你结果。它可以计算、调用方法、访问属性等,但它没有任何变量或参数的概念,所以它不是一个完整的解释器,尽管稍加努力,它就可以成为一个。结合我们上面看到的,我们可以做到这一点:
var res = new CodeDomResolver();
Console.WriteLine(res.Evaluate(SlangParser.ParseExpression("5*4-7")));
将我们刚才讲过的内容结合起来,并添加一些新技巧,下面的程序将输出 13,然后是它自己的代码(但用 VB 表示):
using CD;
using System;
using System.CodeDom;
namespace CodeDomDemo
{
class Program
{
static void Main(string[] args)
{
// evaluates a simple expression
var res = new CodeDomResolver();
Console.WriteLine(res.Evaluate(SlangParser.ParseExpression("5*4-7")));
// takes this file and converts it to vb
var ccu = SlangParser.ReadCompileUnitFrom("..\\..\\Program.cs");
ccu.ReferencedAssemblies.Add("CodeDomGoKit.dll");
ccu.ReferencedAssemblies.Add(typeof(CodeObject).Assembly.GetName().ToString());
SlangPatcher.Patch(ccu);
Console.WriteLine(CodeDomUtility.ToString(ccu, "vb"));
}
}
}
像这样
Option Strict Off
Option Explicit On
Imports CD
Imports System
Imports System.CodeDom
Namespace CodeDomDemo
Friend Class Program
Public Shared Sub Main()
'evaluates a simple expression
Dim res As CodeDomResolver = New CodeDomResolver()
System.Console.WriteLine(res.Evaluate(CD.SlangParser.ParseExpression("5*4-7")))
'takes this file and converts it to vb
Dim ccu As System.CodeDom.CodeCompileUnit = _
CD.SlangParser.ReadCompileUnitFrom("..\..\Program.cs")
ccu.ReferencedAssemblies.Add("CodeDomGoKit.dll")
ccu.ReferencedAssemblies.Add(GetType(CodeObject).Assembly.GetName.ToString)
CD.SlangPatcher.Patch(ccu)
System.Console.WriteLine(CD.CodeDomUtility.ToString(ccu, "vb"))
End Sub
End Class
End Namespace
现在你更没有理由再用 VB 编写任何东西了。更重要的是,现在你有了一种轻松创建 CodeDOM 图的方法。
用我最好的比利·梅斯(Billy Mays)的声音说:“还有更多!” 上面的内容并没有真正展示任何动态性,但显然,如果我们正在生成代码,我们需要生成是动态的。
为此,我提供了一个预处理器,允许进行简单的 T4 文本模板处理。这样,你就可以使用 T4(类似 ASP)的上下文切换 <# #>
来动态地从模板渲染。你的生成项目可以将该模板存储为嵌入式资源,并用它来生成代码。这是一个在文件Test.tt上使用预处理器的例子。首先,这是模板:
using System;
class Program
{
static void Main()
{
<#
for(var i = 0;i<5;++i) {
#>
Console.WriteLine("Hello World! #<#=i+1#>");
<#
}
#>
}
}
预处理后,它产生如下输出:
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello World! #1");
Console.WriteLine("Hello World! #2");
Console.WriteLine("Hello World! #3");
Console.WriteLine("Hello World! #4");
Console.WriteLine("Hello World! #5");
}
}
请注意,输出的格式并不重要。看看我们用 Slang 运行这个并输出到 C# 时会发生什么:
using System;
internal class Program {
public static void Main() {
System.Console.WriteLine("Hello World! #1");
System.Console.WriteLine("Hello World! #2");
System.Console.WriteLine("Hello World! #3");
System.Console.WriteLine("Hello World! #4");
System.Console.WriteLine("Hello World! #5");
}
}
不仅格式更好,而且它对程序进行了一些更改。首先,我们的类型已完全限定。其次,我们的 Main()
方法被设为公共!这是因为它被检测为入口点方法,所以 SlangPatcher.Patch()
会用 CodeEntryPointMethod
来表示它。当 CodeDOM 渲染此类时,它总是将可见性设置为 public。我们无法直接控制这一点。CodeDOM 也不支持入口点方法的参数或返回值,很遗憾。
总之,以下是实现这一目标的 T4 代码,假设Test.tt在我们的项目目录中:
var sw = new StringWriter();
using (var sr = new StreamReader(@"..\..\Test.tt"))
SlangPreprocessor.Preprocess(sr, sw);
var ccu = SlangParser.ParseCompileUnit(sw.ToString());
SlangPatcher.Patch(ccu);
Console.WriteLine(CodeDomUtility.ToString(ccu));
如果我们想美化我们得到的树,也许对一些代码进行搜索和替换,我们有 CodeDomVisitor
。
/// now let's take our code and modify it
CodeDomVisitor.Visit(ccu,(ctx) => {
// we're looking for a method invocation
var mi = ctx.Target as CodeMethodInvokeExpression;
if (null != mi)
{
// ... calling WriteLine
if ("WriteLine" == mi.Method?.MethodName)
{
// replace the passed in expression with "Hello world!"
mi.Parameters.Clear();
mi.Parameters.Add(new CodePrimitiveExpression("Hello world!"));
// done after the first WriteLine so we cancel
ctx.Cancel = true;
}
}
});
我上面没有展示它,因为我们不需要它,但在需要替换自己的目标的情况下,CodeDomVisitor
有一个非常有创意的名字的 ReplaceTarget()
方法,它只接收你当前的上下文和新对象。它使用反射来发挥其“替换自身”的魔力,设置适当父级的属性或替换父级集合中的项,视情况而定。
现在,我们已经简要地提到了 CodeDomResolver
,但如果你要做任何超级花哨的 CodeDOM 操作,比如编写一个编译器或一个解释器并用它来存放你的代码,你可以使用这个类来为你提供树的类型和范围信息。SlangPatcher
广泛使用这个类。
// create one of these lil guys
var res = new CodeDomResolver();
// add our code to it
res.CompileUnits.Add(ccu);
// give it a chance to build its information over our code
res.Refresh();
CodeDomVisitor.Visit(ccu, (ctx) => {
// for every expression...
var expr = ctx.Target as CodeExpression;
if(null!=expr)
{
// except method reference expressions...
var mri = expr as CodeMethodReferenceExpression;
if (null != mri)
return;
// get the expression type
var type = res.TryGetTypeOfExpression(expr);
// write it along with the expression itself
Console.WriteLine(
"Expression type {0}: {1} is {2}",
expr.GetType().Name,
CodeDomUtility.ToString(expr),
null!=type?CodeDomUtility.ToString(type):"unresolvable");
}
});
这会输出以下内容:
Expression type CodeObjectCreateExpression: new CodeDomResolver() is CodeDomResolver
Expression type CodeMethodInvokeExpression: System.Console.WriteLine("Hello world!") is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodePrimitiveExpression: "Hello world!" is string
Expression type CodeMethodInvokeExpression:
CD.SlangParser.ReadCompileUnitFrom("..\\..\\Demo1.cs") is System.CodeDom.CodeCompileUnit
Expression type CodeTypeReferenceExpression: CD.SlangParser is CD.SlangParser
Expression type CodePrimitiveExpression: "..\\..\\Demo1.cs" is string
Expression type CodeMethodInvokeExpression:
ccu.ReferencedAssemblies.Add("CodeDomGoKit.dll") is int
Expression type CodePropertyReferenceExpression:
ccu.ReferencedAssemblies is System.Collections.Specialized.StringCollection
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodePrimitiveExpression: "CodeDomGoKit.dll" is string
Expression type CodeMethodInvokeExpression:
ccu.ReferencedAssemblies.Add(typeof(CodeObject).Assembly.GetName().ToString()) is int
Expression type CodePropertyReferenceExpression:
ccu.ReferencedAssemblies is System.Collections.Specialized.StringCollection
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodeMethodInvokeExpression:
typeof(CodeObject).Assembly.GetName().ToString() is string
Expression type CodeMethodInvokeExpression:
typeof(CodeObject).Assembly.GetName() is System.Reflection.AssemblyName
Expression type CodePropertyReferenceExpression:
typeof(CodeObject).Assembly is System.Reflection.Assembly
Expression type CodeTypeOfExpression: typeof(CodeObject) is System.Type
Expression type CodeMethodInvokeExpression: CD.SlangPatcher.Patch(ccu) is unresolvable
Expression type CodeTypeReferenceExpression: CD.SlangPatcher is CD.SlangPatcher
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodeMethodInvokeExpression:
System.Console.WriteLine(CD.CodeDomUtility.ToString(ccu, "vb")) is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodeMethodInvokeExpression: CD.CodeDomUtility.ToString(ccu, "vb") is string
Expression type CodeTypeReferenceExpression: CD.CodeDomUtility is CD.CodeDomUtility
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodePrimitiveExpression: "vb" is string
Expression type CodeMethodInvokeExpression:
System.Console.WriteLine("Press any key...") is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodePrimitiveExpression: "Press any key..." is string
Expression type CodeMethodInvokeExpression: System.Console.ReadKey() is System.ConsoleKeyInfo
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodeMethodInvokeExpression: System.Console.Clear() is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
有一个表达式它无法解析。无法解析的原因是因为它调用了一个方法 SlangPatcher.Patch()
,该方法接受一个 params
数组参数,而我没有编写支持代码来使其工作,即使它听起来很难,但也很难。
我们还需要一种方法来解析 CodeTypeReference
对象,这就是为什么解析器有一个 TryResolveType()
,它尝试检索 CodeTypeReference
所代表的类型。根据来源,它可能是一个运行时 Type
,或者它可能是一个 CodeTypeDeclaration
。如果无法解析,它将为 null。目前,没有一个替代方法会抛出异常。
这没问题,但是如果我们想从声明的类型中提取成员呢?这就是 CodeDomReflectionBinder
的用途。它的工作方式类似于 Microsoft 的 DefaultBinder
,但它同时处理 CodeDOM 对象和运行时类型。基本上,这个类根据名称和签名进行成员发现和选择。
// once again, we need one of these
var res = new CodeDomResolver();
res.CompileUnits.Add(ccu);
res.Refresh();
// we happen to know Program is the 1st type in the 2nd namespace*
var prg = ccu.Namespaces[1].Types[0];
// we need the scope where we're at
var scope = res.GetScope(prg);
// because our binder attaches to it
var binder = new CodeDomReflectionBinder(scope);
// now get all the methods with the specified name and flags
var members = binder.GetMethodGroup
(prg, "TestOverload",BindingFlags.Public | BindingFlags.Static);
Console.WriteLine("There are {0} TestOverload method overloads.", members.Length);
// try selecting one that takes a single string parameter
var argTypes1 = new CodeTypeReference[] { new CodeTypeReference(typeof(string)) };
var m = binder.SelectMethod
(BindingFlags.Public | BindingFlags.Static, members, argTypes1, null);
if (null != m)
{
Console.WriteLine("Select TestOverload(string) returned:");
_DumpMethod(m);
}
else
Console.WriteLine("Unable to bind to method");
// try selecting one that takes a single it parameter
var argTypes2 = new CodeTypeReference[] { new CodeTypeReference(typeof(int)) };
m = binder.SelectMethod(BindingFlags.Public | BindingFlags.Static, members, argTypes2, null);
if (null != m)
{
Console.WriteLine("Select TestOverload(int) returned:");
_DumpMethod(m);
}
else
Console.WriteLine("Unable to bind to method");
这会输出以下内容:(参考Demo1.cs)
There are 2 TestOverload method overloads.
Select TestOverload(string) returned:
public static int TestOverload(string val) {
System.Console.WriteLine(val);
return val.GetHashCode();
}
Select TestOverload(int) returned:
public static string TestOverload(int val) {
System.Console.WriteLine(val);
return val.ToString();
}
正如你所见,我们根据签名成功地选择了每个方法重载。
你会注意到 Binder 在很多地方都使用 object
类型。这是由于类的双重性质。它在反射的运行时类型和代码对象上运行,所以为了接受或返回这两种类型,它们必须被表示为 object
。你必须为每种类型进行测试和转换才能弄清楚它是什么,而上面的 _DumpMethod()
所做的就是这些,尽管它没有显示出来。在这种情况下,它们碰巧都是代码中声明的方法,但如果我们的类派生自具有这些方法的运行时类型,我们将收到 MethodInfo
实例而不是 CodeMemberMethod
。
最后,我们一直在大量使用 CodeDomUtility
,但我们还没有真正介绍它。它只是一堆用于创建各种 CodeDOM 构造的缩写。如果 Slang 是自动变速器,那么 CodeDomUtility 就是手动挡。更难使用,但控制更多。
var state = CDU.FieldRef(CDU.This, "_state");
var current = CDU.FieldRef(CDU.This, "_current");
var line = CDU.FieldRef(CDU.This, "_line");
var column = CDU.FieldRef(CDU.This, "_column");
var position = CDU.FieldRef(CDU.This, "_position");
var enumInterface = CDU.Type(typeof(IEnumerator<>));
enumInterface.TypeArguments.Add("Token");
var result = CDU.Method(typeof(bool), "MoveNext", MemberAttributes.Public);
result.ImplementationTypes.Add(enumInterface);
result.ImplementationTypes.Add(typeof(System.Collections.IEnumerator));
result.Statements.AddRange(new CodeStatement[]
{
CDU.If(CDU.Lt(state,CDU.Literal(_Enumerating)),
CDU.If(CDU.Eq(state,CDU.Literal(_Disposed)),
CDU.Call(CDU.TypeRef("TableTokenizerEnumerator"),"_ThrowDisposed")),
CDU.If(CDU.Eq(state,CDU.Literal(_AfterEnd)),CDU.Return(CDU.False))),
CDU.Let(current,CDU.Default("Token")),
CDU.Let(CDU.FieldRef(current,"Line"),line),
CDU.Let(CDU.FieldRef(current,"Column"),column),
CDU.Let(CDU.FieldRef(current,"Position"),position),
CDU.Call(CDU.FieldRef(CDU.This,"_buffer"),"Clear"),
CDU.Let(CDU.FieldRef(current,"SymbolId"),CDU.Invoke(CDU.This,"_Lex")),
CDU.Var(CDU.Type(typeof(bool)),"done",CDU.False),
CDU.While(CDU.Not(CDU.VarRef("done")),
CDU.Let(CDU.VarRef("done"),CDU.True),
CDU.If(CDU.Lt(CDU.Literal(_ErrorSymbol),CDU.FieldRef(current,"SymbolId")),
CDU.Var(typeof(string),"be",CDU.ArrIndexer(CDU.FieldRef(CDU.This,"_blockEnds"),
CDU.FieldRef(current,"SymbolId"))),
CDU.If(CDU.Not(CDU.Invoke
(CDU.TypeRef(typeof(string)),"IsNullOrEmpty",CDU.VarRef("be"))),
CDU.If(CDU.Not(CDU.Invoke(CDU.This,"_TryReadUntilBlockEnd",CDU.VarRef("be"))),
CDU.Let(CDU.FieldRef(current,"SymbolId"),CDU.Literal(_ErrorSymbol)))
)),
CDU.If(CDU.And(CDU.Lt(CDU.Literal(_ErrorSymbol),
CDU.FieldRef(current,"SymbolId")),CDU.NotEq
(CDU.Zero,CDU.BitwiseAnd(CDU.ArrIndexer
(CDU.FieldRef(CDU.This,"_nodeFlags"),
CDU.FieldRef(current,"SymbolId")),CDU.One))),
CDU.Let(CDU.VarRef("done"),CDU.False),
CDU.Let(CDU.FieldRef(current,"Line"),line),
CDU.Let(CDU.FieldRef(current,"Column"),column),
CDU.Let(CDU.FieldRef(current,"Position"),position),
CDU.Call(CDU.FieldRef(CDU.This,"_buffer"),"Clear"),
CDU.Let(CDU.FieldRef(current,"SymbolId"),CDU.Invoke(CDU.This,"_Lex")))
),
CDU.Let(CDU.FieldRef(current,"Value"),CDU.Invoke(CDU.FieldRef
(CDU.This,"_buffer"),"ToString")),
CDU.If(CDU.Eq(CDU.FieldRef(current,"SymbolId"),CDU.Literal(_EosSymbol)),
CDU.Let(state,CDU.Literal(_AfterEnd))),
CDU.Return(CDU.NotEq(state,CDU.Literal(_AfterEnd))) });
return result;
如果你仔细看,你可以大致看到它里面创建的 CodeDOM 结构。基本上,它正在实现一个 IEnumerator<Token>.MoveNext()
方法。
这个类有两个普遍感兴趣的方法,即使你从不使用它来生成上面这样的代码:ToString()
,它可以将几乎任何 CodeDOM 对象呈现为字符串;以及 Literal()
,它可以将原始类型、数组和对象**序列化为 CodeDOM 结构。这对于生成表代码(如 DFA 状态表和解析表)非常有用,这些表通常存储在嵌套数组中。你的生成器只需要实例化数组的“实时”版本,然后将其传递给 Literal(array)
来获取可用于再次创建它的 CodeExpression
对象。这可以成为一个静态字段初始化程序,用于存储你预生成的表。同样,这非常有用,我在许多代码生成项目中都使用了它。如果你要生成大型数组,这样比使用 Slang 更高效、更容易。
(**需要适当的 InstanceDescriptor/TypeConverter
设置)
限制
虽然 Slang 越来越好,但它仍然存在一些未解决的问题,并且基本上是实验性的。如果它适用于你的代码,那就太好了。如果不适用,那么希望稍后的修订版能解决这个问题。错误处理也需要大量工作。
有些东西 Slang 永远无法支持,比如后置增量和减量运算符、带参数的入口点方法、try-cast、实例化多维数组或调用大多数运算符重载。这些是基于 CodeDOM 的限制,所以我对此无能为力。
Binder 和 Resolver 还没有完成,并且在处理嵌套类型和某些泛型用法时可能遇到问题,并且不支持使用可选参数或 param 数组进行绑定。
延伸阅读
- Microsoft CodeDOM 文档:https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/using-the-codedom
- 以下是与上述代码相关的文章,但为早期版本
- Slang 解析和预处理之前在这里介绍:https://codeproject.org.cn/Articles/5252827/Slang-Part-1-Parsing-a-Csharp-Subset-into-the-Code
- Slang 类型解析之前在这里介绍:https://codeproject.org.cn/Articles/5253246/Slang-Part-2-Scope-and-Type-Resolution-in-the-Code
- Slang 回溯解析器中使用的
IndexedQueue<T>
在这里介绍:https://codeproject.org.cn/Tips/5252993/IndexedQueue-T-A-Custom-Queue-in-Csharp - Rolex,在预构建步骤中使用:https://codeproject.org.cn/Articles/5252200/How-to-Build-a-Tokenizer-Lexer-Generator-in-Csharp
- 上面 Rolex 使用的 Regex:https://codeproject.org.cn/Articles/5251476/How-to-Build-a-Regex-Engine-in-Csharp
历史
- 2019 年 12 月 11 日 - 初次提交