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

Slang 和 CodeDOM Go! Kit:用于代码分析和代码生成的 C# 子集

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020 年 9 月 1 日

MIT

18分钟阅读

viewsIcon

8786

downloadIcon

79

使用 Slang 解析、分析和转换你的代码

Slang

引言

简而言之,Slang 是 C# 的一个子集。我稍后会解释原因。

我之前发表过关于 Slang 的文章,但没有一篇是完整全面的,而且我认为我之前的努力并不容易理解。

我希望向您清晰地展示 Slang 和 CodeDOM Go! Kit 的功能,以及如何以及在哪里使用它们。

本文假定您对 .NET CodeDOM 有一定的了解,尽管我会对其进行总结。

请注意,此解决方案使用 Microsoft 的 CodeDOM Nuget 包,而不是使用 .NET Framework 内置的包。原因是 Core 和 Standard 没有内置该包,而且我们必须在整个解决方案中始终使用一个 CodeDOM API,否则它将无法编译。

概念化这个混乱的局面

CodeDOM 简介

Microsoft 需要一种方法来促进 ASP.NET 的语言无关代码生成,以及 WinForms 中 InitializeComponent() 的 Visual Studio Designer 代码。为此,他们开发了 CodeDOMCodeDOM 是一种使用对象进行语言无关的代码表示。例如,要表示一个变量声明,我们有 CodeVariableDeclarationStatement 类。当您将代码表示为 CodeDOM 对象图时,您可以将其渲染为 VB、C# 以及可能的其他目标语言。CodeVariableDeclarationStatement 的实例可能会在 C# 中生成此内容:

int i = 1234;

以及在 VB.NET 中生成此内容:(希望如此 - 我的 VB 有点生疏!)

Dim i as Integer = 1234

Slang 到底是什么?

Slang 是一种编程语言。嗯,严格来说,它是另一种编程语言 C# 的子集。这意味着任何有效的 Slang 代码都是有效的 C# 代码,但并非所有有效的 C# 代码都是有效的 Slang 代码。我希望这能说清楚。

它之所以是子集,是因为 Slang 解析器使用 CodeDOM 构造生成抽象语法树,而 CodeDOM 根本无法表示 C# 所能做的一切。Slang 受限于 CodeDOM 能支持的内容,并且有充分的理由。

但为什么要使用 Slang?

任何好的开发者都会问这个问题。Slang 被创建的主要原因是为了促进高级的语言无关代码生成,并使结果更易于维护。

Slang 允许您使用熟悉的 C# 语法编写代码,并生成表示代码的 CodeDOM 对象图*。其好处是,您可以然后使用相同的图将其渲染为 VB、C# 或可能的其他目标语言。您还可以做的另一件事是分析和转换 CodeDOM 图。

Slang 非常适合用于代码生成工具,但如果您只想进行代码分析,那么它并非必需,因为 Roslyn 在这方面表现出色。Slang 不是 Roslyn 的替代品。底线是要记住,Slang 旨在用于代码生成,而非代码分析。可以对 Slang 生成的抽象语法树进行分析,而且在进行代码转换时,这种分析确实很有用,但它并不能取代 Roslyn 的高级代码分析功能。在适当的时候使用 Roslyn。在适当的时候使用 Slang。

* 在本文中,“图”一词的使用方式类似于“层次结构”或“树”,不同之处在于图还允许子项拥有多个父项。

这个 CodeDOM Go! Kit 是什么?

CodeDOM 可以表示代码并将其渲染为 VB 或 C#,但对于搜索、分析和转换代码呢?

我们已经简要提到了可以使用 Slang 进行解析,那么接下来呢?

如果您正在编写代码生成工具,那么其中一些代码需要以编程方式生成,否则生成代码还有什么意义?Slang 在这方面帮不了您。它只是一种语言。

进入 CodeDOM Go! Kit。CodeDOM Go! Kit 允许您搜索和转换 CodeDOM 对象图。

它提供了逐个访问图中每个对象的功能,甚至提供了对这些代码图进行类似反射的操作的能力。例如,如果您的 CodeDOM 图包含一个 class,Go! Kit 将允许您查询其成员,就像它是一个“真正”的已编译 Type 并您正在使用反射一样,同时考虑了诸如继承成员之类的因素。

此外,它还提供了一种不太冗长的方法来构建 CodeDOM 图,尤其是在 Slang 会过度使用的情况下。

整合在一起

Slang 的设计初衷是与 CodeDOM Go! Kit 一起使用,并且依赖于它。通过将 Slang 与 CodeDOM Go! Kit 结合使用,可以对代码进行复杂的代码转换。您可以通过将 Slang 代码用作模板来实现这一点。

基本上,您使用 Slang 解析代码,然后获取返回的 CodeDOM 图,并使用提供的 CodeDomVisitor 访问它,查找特定的代码模式。一旦找到某些内容,您就可以编辑找到的内容,也许可以使用 CodeDomUtility 的构建器方法。最后,您可以将结果渲染为 VB.NET、C#,或者可能还有 F#。当我们接下来深入代码时,我们将详细介绍这一点。

如何嵌入 Slang?

您可能想这样做是因为您可能创建了一种文档类型,其中代码夹杂在其他文本中,例如 ASP.NET 或 Parsley。使用 Slang 可以解析包含代码的文档片段。通常,您无法使用 C# 编译器来处理这种情况,因为它只识别代码,而忽略其他内容。如果您想以这种方式嵌入 C#,Slang 是一个选项。还有其他一些方法可以做到这一点,但使用 Slang 的优势在于,Slang 代码也可以渲染为 VB.NET。

那么它又有什么用处呢?

以下是 Slang 可以帮助提升的一些方面:

  • 解析器生成器
  • 扫描器生成器
  • 数据访问层生成器
  • 需要嵌入式编程语言的代码生成器
  • 您能想到的任何其他代码生成

Slang 帮助您编写编写代码的代码。您可以使用 Slang 提供更强大的代码生成工具。使用它,您可以将更多精力集中在编写出色的代码生成工具上,而减少手动构建和维护 CodeDOM 图的工作。

编写这个混乱的程序

解析和修补

与 C# 一样,Slang 在没有类型信息的情况下是模棱两可的,因此在解析它时,您必须返回并使用类型信息“修补” CodeDOM 树,否则某些 CodeDOM 元素将不正确。例如,调用 delegate 的语法和调用方法的语法是相同的。因此,Slang 无法知道它正在检查的代码是方法调用还是 delegate 调用,除非有类型信息可以匹配,而这些信息在解析时找不到。解析后,使用 SlangParser,您必须使用 SlangPatcher 来用您拥有的类型信息修复结果。

只要您的代码能知道预期内容,解析就很简单。

// expect that codeText contains a *compile unit* which is
// the contents of one source file.
CodeCompileUnit ccu = SlangParser.ParseCompileUnit(codeText);

// now that we have done that, we must patch what we've got
SlangPatcher.Patch(ccu);

// Note that if the code spans more than a single compile 
// unit, then you will have to parse each compile unit in 
// turn and then pass them *all* to SlangPatcher.Patch()
// at the same time like .Patch(ccu1,ccu2,ccu3);

上面,我们正在解析一个编译单元 - 一个源文件的文本,但我们可以使用其他解析方法,如 ParseExpression()。唯一的问题是修补非编译单元的代码 - 例如,单个方法或表达式。SlangPatcher.Patch() 仅适用于编译单元,因此您必须创建一个编译单元,添加必要的“粘合剂”,然后将方法或表达式插入到代码的某个位置。最后,修补整个内容,然后再次提取方法或表达式。这并不直接,但提供的演示代码通过语句和表达式来说明了这种技术。请注意,Patch() 可能需要一些时间,尤其是在处理大型图时。

您应该使用 SlangPatcher.GetNextUnresolvedElement() 来检查 Patch() 是否能够解析所有内容。如果它返回 null,则表示一切正常。否则,Patch() 无法修复返回的元素(可能还有其他元素)。您仍然会获得一个 CodeDOM 图来处理,但它可能不完全正确。话虽如此,即使不正确,VB.NET 或 C# 渲染通常仍会生成您想要的代码。但这并不能保证。

作为一种优化,您可以使用 Deslang 工具预先“预处理”解析和修补,然后包含生成的源文件,从而无需在运行时执行上述步骤。接下来将对此进行描述。

使用 Deslang 提高性能

使用 CodeDOM 作为底层抽象语法树表示的一个缺点是它没有索引,而且总体上并不适合搜索。再加上 C# 乃至 Slang 的歧义语法 - 许多内容在没有类型信息的情况下无法解析,这会给 CPU 带来很多工作。因此,Slang 的速度可能不如我们所期望的。

此解决方案附带一个名为 Deslang 的项目。Deslang 允许您获取一组 Slang 源文件,然后将它们转换为几乎可以立即重新实例化表示该 Slang 源代码的 CodeDOM 图的代码。这可能有点复杂,但其好处是您无需从项目中引用 Slang,并且性能大大提高,因为繁重的工作已从运行时转移到编译时。

您所做的是将您的 Slang 源“预处理”为 CodeDOM 图。这意味着您不必在运行时进行此操作。由于 Slang 在底层必须竭力使用 CodeDOM,因此比其他方式慢得多,但现在我们只需执行一次,然后包含 Deslang 生成的源文件。该文件允许访问传递给它的每个源文件的内容,其中每个源文件由其自己的 CodeCompileUnit 表示。它的使用和理解都比听起来要容易。

假设我们有 Slang 代码中的这个简单声明:

using System;
using System.Collections.Generic;
using System.Text;

namespace Example
{
   struct Token
    {
        public string Symbol;
        public int SymbolId;
        public int Line;
        public int Column;
        public long Position;
        public string Value;

        public override string ToString()
        {
            return Symbol + " (" + SymbolId.ToString() + ") : " + Value;
        }
    }
}

这将在 Deslang 的输出中生成,它生成 C# 源:

public static System.CodeDom.CodeCompileUnit Token {
    get {
        return Deslanged._CompileUnit(new string[0], new CodeNamespace[] {
                    Deslanged._Namespace("", new CodeNamespaceImport[] {
                                new CodeNamespaceImport("System"),
                                new CodeNamespaceImport("System.Collections.Generic"),
                                new CodeNamespaceImport("System.Text")}, 
                                new CodeTypeDeclaration[0], new CodeCommentStatement[0]),
                    Deslanged._Namespace("Example", 
                    new CodeNamespaceImport[0], new CodeTypeDeclaration[] {
                                Deslanged._TypeDeclaration("Token", false, false, 
                                false, true, false, (MemberAttributes.Final | 
                                MemberAttributes. Private), 
                                TypeAttributes.NotPublic, new CodeTypeParameter[0], 
                                new CodeTypeReference[0], new CodeTypeMember[] {
                                            Deslanged._MemberField
                                            (new CodeTypeReference(typeof(string)), 
                                            "Symbol", null, 
                                            (MemberAttributes.Final | 
                                            MemberAttributes. Public), 
                                            new CodeCommentStatement[0], 
                                            new CodeAttributeDeclaration[0], 
                                            new CodeDirective[0], 
                                            new CodeDirective[0], null),
                                            Deslanged._MemberField
                                            (new CodeTypeReference(typeof(int)), 
                                            "SymbolId", null, 
                                            (MemberAttributes.Final | 
                                            MemberAttributes. Public), 
                                            new CodeCommentStatement[0], 
                                            new CodeAttributeDeclaration[0], 
                                            new CodeDirective[0], 
                                            new CodeDirective[0], null),
                                            Deslanged._MemberField
                                            (new CodeTypeReference(typeof(int)), 
                                            "Line", null, 
                                            (MemberAttributes.Final | 
                                            MemberAttributes. Public), 
                                            new CodeCommentStatement[0], 
                                            new CodeAttributeDeclaration[0], 
                                            new CodeDirective[0], 
                                            new CodeDirective[0], null),
                                            Deslanged._MemberField
                                            (new CodeTypeReference(typeof(int)), 
                                            "Column", null, 
                                            (MemberAttributes.Final | 
                                            MemberAttributes. Public), 
                                            new CodeCommentStatement[0], 
                                            new CodeAttributeDeclaration[0], 
                                            new CodeDirective[0], 
                                            new CodeDirective[0], null),
                                            Deslanged._MemberField
                                            (new CodeTypeReference(typeof(long)), 
                                            "Position", null, 
                                            (MemberAttributes.Final | 
                                            MemberAttributes. Public), 
                                            new CodeCommentStatement[0], 
                                            new CodeAttributeDeclaration[0], 
                                            new CodeDirective[0], 
                                            new CodeDirective[0], null),
                                            Deslanged._MemberField
                                            (new CodeTypeReference(typeof(string)), 
                                            "Value", null, 
                                            (MemberAttributes.Final | 
                                            MemberAttributes. Public), 
                                            new CodeCommentStatement[0], 
                                            new CodeAttributeDeclaration[0], 
                                            new CodeDirective[0], 
                                            new CodeDirective[0], null),
                                            Deslanged._MemberMethod
                                            (new CodeTypeReference(typeof(string)), 
                                            "ToString", 
                                            (MemberAttributes.Override | 
                                            MemberAttributes. Public), 
                                            new CodeParameterDeclarationExpression[0], 
                                            new CodeStatement[] {
                                                        new CodeMethodReturnStatement
                                                        (new CodeBinaryOperatorExpression
                                                        (new CodeFieldReferenceExpression
                                                        (new CodeThisReferenceExpression(), 
                                                        "Symbol"), CodeBinaryOperatorType.Add, 
                                                        new CodeBinaryOperatorExpression
                                                        (new CodePrimitiveExpression(" ("), 
                                                        CodeBinaryOperatorType.Add, 
                                                        new CodeBinaryOperatorExpression
                                                        (new CodeMethodInvokeExpression
                                                        (new CodeMethodReferenceExpression
                                                        (new CodeFieldReferenceExpression
                                                        (new CodeThisReferenceExpression(), 
                                                        "SymbolId"), "ToString"), 
                                                        new CodeExpression[0]), 
                                                        CodeBinaryOperatorType.Add, 
                                                        new CodeBinaryOperatorExpression
                                                        (new CodePrimitiveExpression(") : "), 
                                                        CodeBinaryOperatorType.Add, 
                                                        new CodeFieldReferenceExpression
                                                        (new CodeThisReferenceExpression(), 
                                                        "Value"))))))}, 
                                                        new CodeTypeReference[0], null, 
                                                        new CodeCommentStatement[0], 
                                                        new CodeAttributeDeclaration[0], 
                                                        new CodeAttributeDeclaration[0], 
                                                        new CodeDirective[0], 
                                                        new CodeDirective[0], null)}, 
                                                        new CodeCommentStatement[0], 
                                                        new CodeAttributeDeclaration[0], 
                                                        new CodeDirective[0], 
                                                        new CodeDirective[0], null)}, 
                                                        new CodeCommentStatement[0])}, 
                                                        new CodeAttributeDeclaration[0], 
                                                        new CodeDirective[0], 
                                                        new CodeDirective[0]);
    }
}

抱歉换行。这段代码是重新生成的,所以它并没有真正地为了可读性而格式化。您不必理解它在做什么,只要理解它正在创建一个包含您所有已解析 Slang 代码的 CodeCompileUnit 即可。它等同于使用 Slang 程序集直接获得的编译单元。只要您所有的 Slang 代码在编译时都已知,您就可以使用 Deslang 将其精炼成一个 C# 源文件,其中包含您使用 Slang 本身获得的 CodeDOM 图。实例化这些图速度很快。使用 Slang 则不是。

使用命令行看起来是这样的:

deslang "Token.cs" /output "Deslanged.cs"

如果您列出它们,它将接受多个输入文件。您必须始终包含相互依赖的源文件,即使您不打算直接使用这些依赖项中的代码。

在代码中使用它看起来是这样的:

CodeCompileUnit tokenCcu = Deslanged.Token; // now tokenCcu can be manipulated 
                                            // or rendered as normal

搜索 CodeDOM 图

在修补 CodeCompileUnit(s) 或从 Deslang 输出获取它们之后,您经常需要搜索并修改它们。我们可以为此使用 CodeDomVisitor。此类在任何 CodeDOM 对象图或子图上实现访问者模式。您可以这样使用它:

CodeDomVisitor.Visit(ccu, (ctx) => { ... });

这里的 ctxCodeDomVisitContext 的一个实例。您可以使用该上下文获取正在访问的当前 TargetParent 对象、Root、检索它的 Member 以及访问者所在的集合的 Index(如果适用)。我们来看一个简单的例子:

CodeDomVisitor.Visit(Deslanged.Token, (ctx) => {
    var tr = ctx.Target as CodeTypeReference;
    if (null != tr)
        Console.WriteLine(CodeDomUtility.ToString(tr));
});

上面的代码查找图中的任何和所有 CodeTypeReference 对象 - 在这种情况下,Deslanged.Token 是图的根。然后它使用 CodeDomUtility 为找到的每个 CodeTypeReference 获取一个字符串,并将其写入控制台。

这是有效的,因为 Visit() 会为遇到的图中的每个对象调用,所以我们只需要检查当前的 Target 是否为 CodeTypeReference

由于经常需要访问某个对象然后替换被访问的值,因此提供了 CodeDomVisitor.ReplaceTarget() 来完成此操作。只需传递当前上下文和新值即可。

手动创建 CodeDOM 图

在许多情况下,您需要创建 CodeDOM 图,而无需使用 Slang 编写。对于那些将根据某种输入数据动态生成的 CodeDOM 的部分,这种情况非常普遍。您可以使用 CodeDOM 本身,或者使用 CodeDomUtility 的构建器方法。后者的优势在于它不那么冗长。

通常,您会想要为 CodeDomUtility 类设置一个更短的别名,然后您可以使用它所有缩写的创建者。例如:

using CU = CD.CodeDomUtility;
...
var result = CU.For(
    CU.Var(typeof(int),"i",CU.Zero),
    CU.Lt(CU.VarRef("i"),CU.Literal(10)),
    CU.Let(CU.VarRef("i"),CU.Add(CU.VarRef("i"),CU.One)),
    CU.Call(CU.TypeRef(typeof(Console)),"WriteLine",CU.Literal("Hello World!"))
);

出于演示目的,这完全是静态的,但通常您创建的代码将基于某种输入数据动态构建。上面的代码乍一看很难读,但随着练习会变得更容易。它也比直接使用标准的 CodeDOM 类更容易。

这是上面渲染为 C# 的 CodeDOM 图:

for (int i = 0; (i < 10); i = (i + 1)) {
    System.Console.WriteLine("Hello World!");
}

尝试将第一个图中的调用与第二个图中的代码进行匹配。您会开始看到它们如何对应,并且通过一些练习,它会变得更容易阅读和使用。在这种情况下,我们本可以使用 Slang 编写整个内容来节省输入,但正如我所说,通常您会做一些事情来动态生成该代码(基于某些输入数据),在这种情况下,这种技术是有意义的。

将 Slang 代码模板化

为了在保持灵活性的同时提高可维护性,您可以将 Slang 的强大功能与 CodeDOM 图的编程编辑相结合,从而将 Slang 代码模板化。总体而言,步骤如下:

  1. 创建要模板化的 Slang 代码
  2. 将 Slang 代码解析为 CodeDOM 图并进行修补
  3. 查询或访问图,根据需要移除、替换和添加代码元素

考虑以下 Slang “模板”:

// namespace will be replaced
namespace T_NAMESPACE
{
    // type name will be replaced
    class T_TYPE
    {
        // init value will be replaced
        public static int[] Primes = null;

    }
}

请注意,当您在 Visual Studio 中包含模板文件时,应转到模板文档的属性,并将“生成操作”设置为“无”,这样它就不会作为项目的一部分进行编译。

除了设置命名空间和类型名称外,我们还将预先计算素数,并用我们构建的素数数组填充 Primes

这是我们如何进行操作的。由于它相对简单,我们将避免 CodeDomVisitor 的开销,而是直接查询图:

// compute the primes. algorithm borrowed
// from SLax at https://stackoverflow.com/questions/1510124/program-to-find-prime-numbers
var primesMax = 100;
var primesArr = Enumerable.Range(0, 
    (int)Math.Floor(2.52 * Math.Sqrt(primesMax) / Math.Log(primesMax))).Aggregate(
    Enumerable.Range(2, primesMax - 1).ToList(),
    (result, index) =>
    {
        var bp = result[index]; var sqr = bp * bp;
        result.RemoveAll(i => i >= sqr && i % bp == 0);
        return result;
    }
).ToArray();

// read the template into the compile unit
CodeCompileUnit ccu;
using (var stm = File.OpenRead(@"..\..\Template.cs"))
    ccu=SlangParser.ReadCompileUnitFrom(stm);

// find the target namespace and change it
var ns = CU.GetByName("T_NAMESPACE", ccu.Namespaces);
ns.Name = "TestNS";

// find the target class
var type= CU.GetByName("T_TYPE", ns.Types);

// change the name
type.Name = "TestPrimes";

// get the Primes field:
var primes = CU.GetByName("Primes", type.Members) as CodeMemberField;

// change the init expression to the primes array
primes.InitExpression = CU.Literal(primesArr);

// now write the result out
Console.WriteLine(CU.ToString(ccu));

忽略素数生成代码,因为它仅用于演示。相反,请查看 ToArray() 调用后面的行。我们在这里所做的是读取模板,查找 T_NAMESPACET_TYPEPrimes。当我们找到它们时,我们会对图进行适当的修改。如果您非常仔细地观察,您可能会注意到我们正在使用 CU.Literal() 来序列化整个数组为一个字面值。这非常强大,对于许多生成项目来说,这将是创建硬编码数据表的主要方法,用于您生成的代码中。我没有在上面添加错误处理代码,以免混乱,所以如果您的 Template.cs 模板中没有它所查找的内容,它会抛出异常。

使用 T4 预处理器模板

您可以使用内置的 mini T4 引擎预处理 Slang 代码。它很简单,不支持诸如代码隐藏或任何 T4 模板指令之类的功能。但是,它非常适合其预期用途。

您可以通过编程方式使用 SlangPreprocessor.Preprocess() 访问它,或者通过在输入文档中使用 T4 标签来与 Deslang 一起使用它。

这是使用 Deslang 的示例:

模板

这是 TestTemplace.cst

using System;

namespace Test
{
    class TestTemplate
    {
        public void <#=Arguments["Method1"]#>() { Console.WriteLine("foo"); }
        public void <#=Arguments["Method2"]#>() { Console.WriteLine("bar"); }
        <# for(var i=0;i<10;++i) {
            #>public int TestField<#=(i+1).ToString()#> = <#=i.ToString()#>;<#
        }
        #>
    }
}

命令行

这是 Deslang 命令行:

deslang TestTemplate.cst /t4args Method1=Test1&Method2=Test2

注意 /t4args 参数。这允许您将一组参数传递给每个模板。相同的列表用于所有模板。这些参数的格式类似于查询字符串参数列表,并且是 URL 编码的。可以在模板内部通过 Arguments 字典访问它们。

后处理输出

Deslang 不会显示此内容,但这是上述内容的后处理输出,在被 Deslang 解析和序列化之前:

using System;

namespace Test
{
        class TestTemplate
        {
                public void Test1() { Console.WriteLine("foo"); }
                public void Test2() { Console.WriteLine("bar"); }
                public int TestField1 = 0;
                public int TestField2 = 1;
                public int TestField3 = 2;
                public int TestField4 = 3;
                public int TestField5 = 4;
                public int TestField6 = 5;
                public int TestField7 = 6;
                public int TestField8 = 7;
                public int TestField9 = 8;
                public int TestField10 = 9;
        }
}

上述技术是使用 Slang 动态构建 CodeDOM 图的一种方法。

解析/“反射” CodeDOM 图

CodeDOM Go! Kit 包含一个强大的设施,用于在 CodeDOM 图上执行类似反射的查询。它处理诸如绑定到特定方法重载或解析成员(包括继承成员)的复杂性。SlangPatcher 大量使用此设施来确定 Slang 源代码(实际上是任何 CodeDOM 图)中的类型信息,例如所有声明的类和结构及其每个成员,或者方法或属性中所有声明的变量。您也可以使用它,尽管您可能不太经常使用它,甚至根本不使用。

CodeDomResolver 负责大部分繁重的工作,它是一个复杂且功能丰富的类。

主要功能之一是能够从 CodeDOM 图中的任何点捕获所有作用域信息快照,例如当前作用域中的所有变量,以及该作用域中所有可访问的成员和类型。这是一种非常强大的分析,为 CodeDOM 图提供了与 C# 编译器在编译 C# 代码时使用的信息相同的类型。您可以使用 GetScope() 从任何点执行此操作,并将您想要检索作用域的 CodeDOM 元素传递给它。它返回一个 CodeDomResolverScope 实例,其中包含所有作用域信息。这可能是使用此类最常见的操作。

另一个常见功能是 GetTypeOfExpression(),它接受一个 CodeExpression 实例并返回一个 CodeTypeReference 实例,该实例表示表达式将求值的类型。

使用 CodeDomResolver 大致如下:

// create a resolver
var res = new CodeDomResolver();
            
// read the resolver sample into the compile unit
CodeCompileUnit ccu;
using (var stm = File.OpenRead(@"..\..\Resolver.cs"))
    ccu = SlangParser.ReadCompileUnitFrom(stm);
            
// remember to patch it!
SlangPatcher.Patch(ccu);
            
// add the compile unit to the resolver
res.CompileUnits.Add(ccu);

// prepare the resolver
// any time you add compile units you'll need
// to call Refresh()
res.Refresh();

// go through all expressions in the
// graph and try to get their type
CodeDomVisitor.Visit(ccu, (ctx) => {
    var expr = ctx.Target as CodeExpression;
    if (null != expr) 
    {
        // we want everything except CodeTypeReferenceExpression
        var ctre = expr as CodeTypeReferenceExpression;
        if (null == ctre)
        {
            // get the scope of the expression
            var scope = res.GetScope(expr);
            CodeTypeReference ctr = res.TryGetTypeOfExpression(expr,scope);
            if (null != ctr)
            {
                Console.WriteLine(CU.ToString(expr) + " is type: " + CU.ToString(ctr));
                Console.WriteLine("Scope Dump:");
                Console.WriteLine(scope.ToString());
            }
        }
    }
})

最后,我们来介绍一个不太常用的功能 - 成员选择和绑定。基本上,这允许您根据成员方法和属性的签名(包括它们接受的参数)进行查询,因为方法和索引属性可以重载。

考虑以下 Slang 代码(Binding.cs):

using System;

namespace scratch
{
    class Binding
    {
        public void Test(string text)
        {
            Console.WriteLine(text);
        }
        public void Test(int value)
        {
            Console.WriteLine(value);
        }
    }
}

假设我们想选择具有兼容签名的重载方法,就像编译器那样:

// we'll need the resolver in a bit
var res = new CodeDomResolver();
            
// read the binding sample into the compile unit
CodeCompileUnit ccu;
using (var stm = File.OpenRead(@"..\..\Binding.cs"))
    ccu = SlangParser.ReadCompileUnitFrom(stm);
            
// add the compile unit to the resolver
res.CompileUnits.Add(ccu);

// prepare the resolver
res.Refresh();

// get the first class available
var tdecl = ccu.Namespaces[1].Types[0];

// capture the scope at the typedecl level
var scope = res.GetScope(tdecl);

// create a new binder with that scope
var binder = new CodeDomBinder(scope);

// get the method group for Test(...)
var methodGroup = binder.GetMethodGroup
                  (tdecl, "Test", BindingFlags.Public | BindingFlags.Instance);
            
// select the method that can take a string value
var m =binder.SelectMethod(BindingFlags.Public, methodGroup, new CodeTypeReference[] 
       { new CodeTypeReference(typeof(string)) }, null);
Console.WriteLine(CU.ToString((CodeMemberMethod)m));
            
// select the method that can take a short value
// (closest match accepts int)
m = binder.SelectMethod(BindingFlags.Public, methodGroup, new CodeTypeReference[] 
         { new CodeTypeReference(typeof(short)) }, null);
Console.WriteLine(CU.ToString((CodeMemberMethod)m));

在这里,我们使用 CodeDomBinderCodeDomResolver 来选择特定的方法重载。请注意对“方法组”的引用。方法组是所有方法集合,它们的签名相同,只是参数的类型和数量有所不同。换句话说,它是方法及其所有重载。请注意,如果继承自已编译类型,这也可能返回实际的反射 MethodInfo 对象。上面,我们始终假定它们不会。在实际情况中,您必须检查 m 值的类型,以确定它是 CodeMemberMethod 还是 MethodInfo 类型。

设置一个使用 Slang 的项目

独立项目

以下是我建议设置构建环境的方式:

  1. Deslang.exe、Slang.dll 和 CodeDomGoKit.dll 复制到您的解决方案根文件夹。您将与源代码一起分发这些文件。
  2. 在您的项目中创建一个文件夹。我通常称之为 Exports。它将包含您所有的 Slang 代码文件。
  3. 添加一个预构建事件,该事件将启动 Deslang,处理 Exports 下的每个文件,并指定一个您选择的输出文件名,我称之为“Deslanged.cs”,并确保输出路径是您项目根源文件夹。
  4. 如果您的项目需要,可以选择引用 CodeDomGoKit.dll

确保当您添加包含 Slang 代码的 C# 文件时,在文档的属性中将其“生成操作”设置为“无”。此外,请务必在每次添加或删除 Exports 文件夹中的文件时更新预构建步骤命令行。

嵌入 Slang 的项目

首先,您需要执行独立项目部分中的所有步骤。

接下来,添加对 Slang 和 CodeDOMGoKit 的引用。

现在,您可以使用 Deslang 来处理您的静态 Slang 部分,并且仍然可以使用 Slang 和 CodeDOM Go! Kit 的所有功能来在运行时使用。

错误和限制

  • 目前,Slang 可以解析嵌套类型,但它不一定能正确解析或修补它们。问题在于 CodeDOM Go! Kit。在当前版本中,请避免将嵌套类型与 Slang 和 CodeDOM Go! Kit 一起使用。
  • 在某些情况下,解析器错误处理可能无法始终返回导致错误的精确行号和列号。
  • 首先,解析器错误处理可以做得更好,但对于一个人来说,要“正确”完成这项工作(尤其是在涉及泛型的情况下)是一项庞大的工作。
  • 您会发现您希望能够从 Slang 代码中引用您主项目中的 C# 代码,但由于结构原因,您无法在预构建步骤中这样做。
  • 在当前版本中,Slang 不会保留注释和预处理器指令。
  • 您无法绑定到具有 param 数组或可选参数的方法。

历史

  • 2020 年 9 月 1 日 - 初始提交
© . All rights reserved.