【问题标题】:Is there a way to implement custom language features in C#?有没有办法在 C# 中实现自定义语言功能?
【发布时间】:2012-07-31 21:24:14
【问题描述】:

我对此感到困惑有一段时间了,我环顾四周,找不到任何关于这个主题的讨论。

假设我想实现一个简单的例子,比如一个新的循环结构:do..until

写得很像 do..while

do {
    //Things happen here
} until (i == 15)

这样做可以将其转换为有效的 csharp:

do {
    //Things happen here
} while (!(i == 15))

这显然是一个简单的例子,但有没有办法添加这种性质的东西?理想情况下作为 Visual Studio 扩展来启用语法突出显示等。

【问题讨论】:

  • 一个更简单的解决方案是不将它们添加到 C# 本身,而是在运行 C# 编译器之前创建一种 DSL 或子语言,可以嵌入并预处理/预编译到 C#(或 MSIL)中。更多的步骤,但也更强大。
  • 这正是我想要做的。您是否有任何链接或书籍可以向我指出如何按照您的建议连接到构建管道?
  • 您可能想查看更适合定义您自己的控件结构的其他语言。 IE。 LISP/Scheme 系列以此而闻名,并有一些 .Net 实现 (stackoverflow.com/questions/110433/…)。
  • Nemerle 是一种从 C# 派生的语言,它具有允许您在其中定义自己的语法的结构。但它不是 C# 的超级集,因为它无法在不修改的情况下编译现有的 C# 代码。

标签: c# syntactic-sugar language-construct


【解决方案1】:

Microsoft 提出 Rolsyn API 作为具有公共 API 的 C# 编译器的实现。它包含每个编译器流水线阶段的单独 API:语法分析、符号创建、绑定、MSIL 发射。您可以提供自己的语法解析器实现或扩展现有的语法解析器,以获得具有您想要的任何功能的 C# 编译器。

Roslyn CTP

让我们使用 Roslyn 扩展 C# 语言!在我的示例中,我将使用相应的 do-while 替换 do-until 语句:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Roslyn.Compilers.CSharp;

namespace RoslynTest
{

    class Program
    {
        static void Main(string[] args)
        {

            var code = @"

            using System;

            class Program {
                public void My() {
                    var i = 5;
                    do {
                        Console.WriteLine(""hello world"");
                        i++;
                    }
                    until (i > 10);
                }
            }
            ";



            //Parsing input code into a SynaxTree object.
            var syntaxTree = SyntaxTree.ParseCompilationUnit(code);

            var syntaxRoot = syntaxTree.GetRoot();

            //Here we will keep all nodes to replace
            var replaceDictionary = new Dictionary<DoStatementSyntax, DoStatementSyntax>();

            //Looking for do-until statements in all descendant nodes
            foreach (var doStatement in syntaxRoot.DescendantNodes().OfType<DoStatementSyntax>())
            {
                //Until token is treated as an identifier by C# compiler. It doesn't know that in our case it is a keyword.
                var untilNode = doStatement.Condition.ChildNodes().OfType<IdentifierNameSyntax>().FirstOrDefault((_node =>
                {
                    return _node.Identifier.ValueText == "until";
                }));

                //Condition is treated as an argument list
                var conditionNode = doStatement.Condition.ChildNodes().OfType<ArgumentListSyntax>().FirstOrDefault();

                if (untilNode != null && conditionNode != null)
                {

                    //Let's replace identifier w/ correct while keyword and condition

                    var whileNode = Syntax.ParseToken("while");

                    var condition = Syntax.ParseExpression("(!" + conditionNode.GetFullText() + ")");

                    var newDoStatement = doStatement.WithWhileKeyword(whileNode).WithCondition(condition);

                    //Accumulating all replacements
                    replaceDictionary.Add(doStatement, newDoStatement);

                }

            }

            syntaxRoot = syntaxRoot.ReplaceNodes(replaceDictionary.Keys, (node1, node2) => replaceDictionary[node1]);

            //Output preprocessed code
            Console.WriteLine(syntaxRoot.GetFullText());

        }
    }
}
///////////
//OUTPUT://
///////////
//            using System;

//            class Program {
//                public void My() {
//                    var i = 5;
//                    do {
//                        Console.WriteLine("hello world");
//                        i++;
//                    }
//while(!(i > 10));
//                }
//            }

现在我们可以使用 Roslyn API 编译更新的语法树或将 syntaxRoot.GetFullText() 保存到文本文件并将其传递给 csc.exe。

【讨论】:

  • 这似乎是我想去的方向。我能够找到的关于 Roslyn 的大部分信息都是自定义重构和自定义代码问题。您是否有任何资料显示如何与特定的管道阶段联系起来?谢谢:)
  • @Thebigcheeze Roslyn 上还没有太多东西。我自己实现了您的示例(请参阅更新的答案)。另外,我发现了一篇很好的文章,解释了扩展 C# 语言的原则:mindscapehq.com/blog/index.php/2011/10/20/in-bed-with-roslyn
  • 有没有办法将其插入 Visual Studio 以在自定义语言功能上获得适当的智能和语法突出显示?
  • @RamanZhylich 你有什么线索可以解决这个问题stackoverflow.com/questions/38786359/… 吗?
  • 这个例子似乎对解析器可以理解的内容过于局限。将until(被解析为函数调用)转换为其他东西更像是一种黑客行为。如何更改语法文件以便我们可以添加新的语言结构?
【解决方案2】:

最大的缺失部分是连接到管道中,否则你不会比.Emit 提供的更进一步。不要误会,Roslyn 带来了很多很棒的东西,但是对于我们这些想要实现预处理器和元编程的人来说,现在似乎还没有考虑到这一点。您可以实现“代码建议”或他们所谓的“问题”/“操作”作为扩展,但这基本上是代码的一次性转换,充当建议的内联替换,并且 不是实现新语言功能的方式。这是您始终可以使用扩展完成的事情,但 Roslyn 使代码分析/转换变得非常容易:

根据我在 codeplex 论坛上从 Roslyn 开发人员那里了解到的 cmets 信息,在管道中提供挂钩并不是最初的目标。他们在 C# 6 预览版中提供的所有新 C# 语言功能都涉及修改 Roslyn 本身。所以你基本上需要分叉 Roslyn。他们有关于如何构建 Roslyn 并使用 Visual Studio 对其进行测试的文档。这将是分叉 Roslyn 并让 Visual Studio 使用它的一种严厉方式。我说强硬是因为现在任何想要使用你的新语言特性的人都必须用你的替换默认编译器。你可以看到这会开始变得混乱。

Building Roslyn and replacing Visual Studio 2015 Preview's compiler with your own build

另一种方法是构建一个充当 Roslyn 代理的编译器。有用于构建 VS 可以利用的编译器的标准 API。不过,这不是一项微不足道的任务。您将阅读代码文件,调用 Roslyn API 来转换语法树并发出结果。

代理方法的另一个挑战是让智能感知与您实现的任何新语言功能完美配合。您可能必须拥有 C# 的“新”变体,使用不同的文件扩展名,并实现 Visual Studio 所需的所有 API 才能使智能感知正常工作。

最后,考虑 C# 生态系统,以及可扩展编译器的含义。假设 Roslyn 确实支持这些钩子,就像提供 Nuget 包或 VS 扩展来支持新的语言功能一样简单。所有利用新的 Do-Until 功能的 C# 本质上都是无效的 C#,并且在不使用自定义扩展的情况下将无法编译。如果你在这条路上走得足够远,有足够多的人实现新特性,很快你就会发现不兼容的语言特性。也许有人实现了预处理器宏语法,但它不能与其他人的新语法一起使用,因为他们碰巧使用类似的语法来描述宏的开头。如果您利用了很多开源项目并发现自己深入研究他们的代码,您会遇到很多奇怪的语法,这需要您跟踪并研究项目正在利用的特定语言扩展。 可能很疯狂。我并不是要听起来像一个反对者,因为我对语言特性有很多想法并且对此非常感兴趣,但是人们应该考虑它的含义以及它的可维护性。想象一下,如果你被雇用到某个地方工作,他们已经实现了你必须学习的各种新语法,并且如果没有像 C# 的特性一样对这些特性进行审查,你可以打赌其中一些不会很好地设计/实现.

【讨论】:

  • 从历史上看,这是一个很好的答案,但我想明确指出,近年来 Roslyn 允许的远不止这些。问题在于编译器编程极其复杂且容易搞砸,并不是说 Roslyn 不允许您包含新的语言特性。
  • 同意。当然,我没有涵盖您可以使用 Roslyn 做的所有事情。然而,关键是,仍然没有用于修改 C# 编译器的集成点。做到这一点的唯一方法是重新编译它并进行完全替换,这实际上仅用于测试。如果我实现了语言功能 A,而您实现了语言功能 B,我们必须合并我们的编译器源才能使用这两个功能:github.com/dotnet/roslyn/blob/master/docs/contributing/…
  • 我开始研究使用 Roslyn 为 C# 语言添加一些调整的可能性。我不确定的一件事是,在您发布的旧 CodePlex 链接中,它们似乎暗示修改后的 Roslyn 可用于为 .Net Framework 发出修改后的代码。在您发布的新 GitHub 链接中,他们只谈论 .Net Core、.Net Core、.Net Core,就像他们忘记了 .Net Framework 一样。您认为仍然可以修改 .Net Framework 发出的代码吗?还是我误解了一切?
  • 我的理解是,替换 Visual Studio 编译器的指南将替换您的编译器,以便用于编译面向 .NET Framework 或 Core 的项目。请注意,如果您想在运行时使用 Roslyn 发出代码,则无需替换 VS 编译器。您利用 Roslyn 作为编译器作为服务来发出。更换 VS 编译器是您尝试向 VS 编译器添加功能的方法,通常不称为“发射”。
【解决方案3】:

您可以查看www.metaprogramming.ninja(我是开发人员),它提供了一种简单的方法来完成语言扩展(我提供了构造函数、属性甚至 js 样式函数的示例)以及成熟的基于语法的 DSL。

该项目也是开源的。您可以在github 找到文档、示例等。

希望对你有帮助。

【讨论】:

  • 您更改了项目名称?
  • 第一个链接坏了。
【解决方案4】:

您无法在 C# 中创建自己的语法抽象,因此您能做的最好的事情就是创建自己的高阶函数。你可以创建一个Action 扩展方法:

public static void DoUntil(this Action act, Func<bool> condition)
{
    do
    {
        act();
    } while (!condition());
}

你可以用作:

int i = 1;
new Action(() => { Console.WriteLine(i); i++; }).DoUntil(() => i == 15);

尽管这是否比直接使用do..while 更可取值得怀疑。

【讨论】:

    【解决方案5】:

    我发现扩展 C# 语言的最简单方法是使用 T4 文本处理器来预处理我的源代码。 T4 脚本会读取我的 C#,然后调用基于 Roslyn 的解析器,该解析器将使用自定义生成的代码生成新的源代码。

    在构建期间,我的所有 T4 脚本都将被执行,从而有效地作为扩展的预处理器工作。

    在您的情况下,可以按如下方式输入不兼容的 C# 代码:

    #if ExtendedCSharp
         do 
    #endif
         {
                        Console.WriteLine("hello world");
                        i++;
         }
    #if ExtendedCSharp
                    until (i > 10);
    #endif
    

    这将允许在您的程序开发过程中检查您的其余(C# 兼容)代码的语法。

    【讨论】:

      【解决方案6】:

      不,没有办法实现您所说的。

      因为您要问的是定义新的语言结构,因此新的词法分析、语言解析器、语义分析器、生成的IL 的编译和优化。

      在这种情况下您可以做的是使用一些宏/函数。

      public bool Until(int val, int check)
      {
         return !(val == check);
      }
      

      并像使用它一样

      do {
          //Things happen here
      } while (Until(i, 15))
      

      【讨论】:

      • 正如您在第一句话中暗示的那样,这正是我想要做的。但是,与其编写新的解析器、词法分析器和编译器,我想知道是否有办法扩展现有的。
      • 嗯,extend 使用 your 函数,它将在IL 中编译,无需任何更改。
      • 是的,我明白了,这是一个简单的例子来说明这一点,但作为另一个例子:Csharp 有扩展方法。它们是非常简单的语法糖。有没有办法我自己实现扩展方法?
      • @Thebigcheeze:不,没有办法做到这一点,如果不是通过自定义词法分析器、解析器、编译器...
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-04
      • 1970-01-01
      • 2021-08-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多