65.9K
CodeProject 正在变化。 阅读更多。
Home

CodeDomUtility:简化您的代码生成

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2019 年 11 月 27 日

MIT

5分钟阅读

viewsIcon

5990

downloadIcon

137

一个辅助类,可大幅减少您为 CodeDOM 编写代码的工作量。

引言

我发现自己经常使用 Microsoft 的 CodeDOM 进行大量代码生成。这通常涉及创建目标代码的引用实现,然后将其“移植”到 CodeDOM 构造中,最后混合动态/生成部分。

任何使用过 CodeDOM 的人都可能证明其模型容易引起重复性劳损。它太冗长了,简直会让您的手腕和手指抽筋。更糟糕的是,由于嵌套的构造会让您迷失方向,因此很难理解您写的内容。

我们无意在此解决所有 CodeDOM 问题,但我们将使用一个即插即用的代码片段来减轻我们的负担。代码本身并不花哨——它主要是包装器,但它们很有用。除了节省输入外,其主要目标是让您的代码生成看起来更像它正在生成的代码。

Using the Code

首先,让我们看一个我在一个项目中使用的极端但真实的例子。

using CD = CDU.CodeDomUtility;
...
var result = CD.Method(typeof(bool), "_MoveNextInput");
var input = CD.FieldRef(CD.This, "_input");
var state = CD.FieldRef(CD.This, "_state");
var line = CD.FieldRef(CD.This, "_line");
var column = CD.FieldRef(CD.This, "_column");
var position = CD.FieldRef(CD.This, "_position");
var current = CD.PropRef(input,"Current");
result.Statements.AddRange(new CodeStatement[] {
    CD.If(CD.Invoke(input,"MoveNext"),
        CD.IfElse(CD.NotEq(state,CD.Literal(_BeforeBegin)), new CodeStatement[] {
            CD.Let(position,CD.Add(position,CD.One)),
            CD.IfElse(CD.Eq(CD.Literal('\n'),current),new CodeStatement[] {
                CD.Let(column,CD.One),
                CD.Let(line,CD.Add(line,CD.One))
            },
                CD.IfElse(CD.Eq(CD.Literal('\t'),current),new CodeStatement[]
                {
                    CD.Let(column,CD.Add(column,CD.Literal(_TabWidth)))
                },
                    CD.Let(column,CD.Add(column,CD.One))
                )
            )
        },
        CD.IfElse(CD.Eq(CD.Literal('\n'),current),new CodeStatement[] {
            CD.Let(column,CD.One),
            CD.Let(line,CD.Add(line,CD.One))
        },
            CD.If(CD.Eq(CD.Literal('\t'),current),
                CD.Let(column,CD.Add(column,CD.Literal(_TabWidth-1))))
            )
        ),
        CD.Return(CD.True)
    ),
    CD.Let(state,CD.Literal(_InnerFinished)),
    CD.Return(CD.False)
});
return result;

它看起来有点吓人,但实际上很容易适应。花点时间看看,注意嵌套结构如何反映它正在生成的代码的嵌套结构,并且语句看起来更具声明性——更接近实际语言的样子,例如 CD.Let(...) 相对于其对应项 new CodeAssignStatement(...)。当然,如果您愿意,您也可以一次声明一个语句,或者将 CodeDOM 对象创建表达式与此混合。我只是觉得一旦您掌握了它,这种方式会更容易阅读和编写。它有点像 LISPy。

上面的代码没有任何动态部分。它生成的是静态代码。事实上,使用上面的 CodeDOM 的唯一原因是为了能够以任何 CodeDOM 提供程序语言发出此静态代码。为了进行比较,这是它生成的代码(以 C# 编写)

bool _MoveNextInput() {
    if (this._input.MoveNext()) {
         {
            this._position = (this._position + 1);
            if (('\n' == this._input.Current)) {
                this._column = 1;
                this._line = (this._line + 1);
            }
            else {
                if (('\t' == this._input.Current)) {
                    this._column = (this._column + 4);
                }
                else {
                    this._column = (this._column + 1);
                }
            }
        }
        else {
            if (('\n' == this._input.Current)) {
                this._column = 1;
                this._line = (this._line + 1);
            }
            else {
                if (('\t' == this._input.Current)) {
                    this._column = (this._column + 3);
                }
            }
        }
        return true;
    }
    this._state = -1;
    return false;
}

即使是粗略的检查,经验丰富的 CodeDOM 用户也应该可以看出生成此代码所需代码的量要少得多。而且,尽管需要一些时间来适应,但它也更容易阅读。它更准确地反映了它要生成的代码。同样,您不必像我上面那样内联声明所有内容,但正如我所说,我发现这样做可以使代码看起来最接近正在生成的代码。

您可以看到有时生成的代码看起来有点奇怪,比如这样

if ((false 
                    == (this._state == -3)))

不用担心,这是设计使然。当您使用 CodeDomUtility.NotEq() 之类的东西时,它必须这样做,因为没有真正独立于语言的值不等式方法。这是最接近的了。它生成的所有代码在语义上将等同于您使用 CodeDomUtility 生成的代码,即使生成的代码本身看起来有点奇怪。无论哪种方式,使用此类可让您的代码更有可能真正独立于语言,尤其是在处理二元运算符时。

使用 Class()Struct()Enum() 方法即可轻松声明类型。

var result = CD.Struct("Token", false,
                CD.Field(typeof(int), "Line", MemberAttributes.Public),
                CD.Field(typeof(int), "Column", MemberAttributes.Public),
                CD.Field(typeof(long), "Position", MemberAttributes.Public),
                CD.Field(typeof(int), "SymbolId", MemberAttributes.Public),
                CD.Field(typeof(string), "Value", MemberAttributes.Public));

在此方法本身中填写字段、方法或属性并非必需——您可以随时稍后进行,但我发现使用此机制“内联”创建成员,如上所示,特别有用,因为它节省了大量输入。

这个辅助类有一个非常强大的功能隐藏在 CodeDomUtility.Literal() 中。与 CodePrimitiveExpression() 不同,它甚至可以实例化数组,包括嵌套数组,并且通过一些准备工作,使用 Microsoft 的 TypeConverter 框架和 InstanceDescriptor,您可以告诉它如何实例化您的复杂类和结构。只需小心实例化泛型数组,例如 KeyValuePair<TKey,TValue>[],因为 Microsoft 的 VBCodeProvider 中存在一个 bug,阻止它正确渲染泛型数组。但是,C# 可以很好地处理它,但当然,这样您就失去了语言独立性。

我通常使用数组渲染来处理状态机表或解析表之类的事情。只需一次调用即可。

var array = new string[] {"Hello","World"};
...
// generate the expression to instantiate our array 
var expr = CD.Literal(array);
// render the code in (default) C#
Console.WriteLine(CD.ToString(expr));

这将输出

new string[] {
        "Hello",
        "World"}

我发现使用此功能对于我通常需要生成(我生成大量表驱动代码)的复杂嵌套数组结构非常有效。在某些情况下,它可以为我节省数十行代码,尽管这是极端情况。大约 90% 的代码将用于渲染标量类型和非嵌套数组(如上所述,带有标量元素类型),但它仍然可以节省您的手指,并且对于复杂的事情来说,它简直是救星。

您可以使用 CD.ToString() 并传递一个代码 object 和一个可选的 language 参数来轻松渲染它。

所有代码都带有文档注释,因此您应该能够通过 IntelliSense 了解每个方法的作用。其中约 90% 只是快捷包装器。另外 10% 是 Literal() 序列化代码以及一个内部助手,用于按系列创建二元运算符表达式。我可能会在将来添加更多功能,但就目前而言,我更喜欢保持简单和切题,不像 CodeDOM 本身。

我没有包含演示代码,因为这基本上是我即将提交的一个项目的预览代码,并且创建代码生成器(即使是演示)是大量工作,即使有辅助类。

希望通过文档注释和文章,您会发现它有用且易于使用。如果没有,请等待我即将发表的关于如何构建词法分析器/标记生成器的文章,该文章使用了这个类。

关注点

使 CodeDOM 代码更像它所生成代码的目标最初并不是这个项目的一部分。只有在我开始使用它之后,我才意识到这是其中一个好处,所以一旦我意识到这一点,我就开始朝着这个方向编写代码。我发现这使得代码生成更容易调试。我甚至在 CodeDOM 中创建了整个库,以便将源代码作为任何 CodeDOM 语言可用——如果不是这个小工具,我永远不会尝试这样做。

历史

  • 2019 年 11 月 27 日 - 首次提交
© . All rights reserved.