使用 Roslyn 将 C# 作为脚本语言集成到你的 .NET 应用程序中






4.97/5 (26投票s)
介绍如何使用 Roslyn 将 C# 作为脚本语言集成到你的 .NET 应用程序中。
引言
微软最近发布了 Roslyn CTP,预览了 C# 和 VB.NET 的未来功能。Roslyn CTP 是一个非常激动人心的发布,它为 C# 和 VB.NET 开发者带来了许多新的可能性。Roslyn 在 .NET 的编译器服务之上提供了语言服务和 API,这将使 .NET 开发者能够做更多的事情——包括使用 C# 和 VB.NET 作为脚本语言,在你的应用程序中使用编译器作为服务来处理与代码相关的任务,开发更好的语言和 IDE 扩展等等。
此外,Roslyn 为开发者提供了许多可能性,可以围绕 Visual Studio IDE 编写代码分析和操作工具,并提供 API,让你能够快速开发 Visual Studio 增强功能。Roslyn API 将对 C# 和 VB 的语法、语义绑定、代码发射等方面提供完整的支持——你很快就会看到围绕 C# 和 VB.NET 的大量语言扩展,以及可能的新 DSL 和许多元编程想法。
Roslyn 主要有四个 API 层
- 脚本 API
- 为 C# 和 VB.NET 提供运行时执行上下文。现在你可以在你的应用程序中使用 C#/VB.NET 作为脚本语言。
- 编译器 API
- 用于访问代码的语法和语义模型。
- 工作区 API
- 提供一个对象模型来聚合解决方案中跨项目的代码模型。主要用于 IDE(如 Visual Studio)的代码分析和重构,尽管这些 API 并不依赖于 Visual Studio。
- 服务 API
- 在 Visual Studio SDK (VSSDK) 之上提供一个层,用于智能感知、代码格式化等功能。
在这篇文章中,我们将对脚本 API 和编译器 API 进行一次初步的预览。
脚本 API
不久前,我曾发表过一篇关于如何使用Mono C# 编译器作为服务在你的 .NET 应用程序中来利用 C# 作为脚本语言的文章。多亏了 Roslyn,现在 .NET 将很快拥有一个一流的脚本 API。
Roslyn.Scripting.*
命名空间提供了用于实现自己的脚本会话的类型,可以使用 C# 和 VB.NET。你可以使用 Session.Create(..)
方法创建一个新的脚本会话。Session.Create
方法还可以接受一个 Host
对象,Host
对象的方法将直接可用于运行时上下文。
要执行代码,你可以通过提供所需的程序集引用和命名空间导入信息来创建一个 ScriptEngine
实例,并调用 Scripting Engine 的 Execute
方法。为了演示这一点,让我们创建一个简单的 ScriptingHost
来包装一个脚本会话。作为演示,我们还将创建一个 ScriptedDog
类,这个示例的目的是通过脚本环境创建和训练狗。安装 Roslyn 后,创建一个新的控制台应用程序,以下代码演示了一个简单的脚本环境:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using Roslyn.Scripting;
using Roslyn.Scripting.CSharp;
namespace ScriptingRoslyn
{
//Our Dog class so that we can train dogs later
public class OurDog
{
private string _name = string.Empty;
public OurDog(string name)
{
_name = name;
}
public string Name
{
get { return _name; }
}
public void Bite(OurDog other)
{
Console.WriteLine("{0} is biting the tail of {1}",
_name, other.Name);
}
public void Walk()
{
Console.WriteLine("{0} is Walking", _name);
}
public void Eat()
{
Console.WriteLine("{0} is Eating", _name);
}
}
//Let us create a Host object, where we'll wrap our session and engine
//The methods in the host class are available directly to the environment
public class ScriptingHost
{
private ScriptEngine engine;
private Session session;
//Methods in the Host object can be called directly from the
//environment
public OurDog CreateDog(string name)
{
return new OurDog(name);
}
public ScriptingHost()
{
//Create a session
session = Session.Create(this);
//Create the engine, just pass the assemblies and namespaces
engine = new ScriptEngine(new Assembly[]
{
typeof(Console).Assembly,
typeof(ScriptingHost).Assembly,
typeof(IEnumerable<>).Assembly,
typeof(IQueryable).Assembly
},
new string[]
{
"System", "System.Linq",
"System.Collections",
"System.Collections.Generic"
}
);
}
//Pass the code to the engine, nothing much here
public object Execute(string code)
{
return engine.Execute(code, session);
}
public T Execute<T>(string code)
{
return engine.Execute<T>(code, session);
}
}
//Main driver
class Program
{
static void Main(string[] args)
{
var host = new ScriptingHost();
Console.WriteLine("Hello Dog Trainer!! Type your code.\n\n");
string codeLine = string.Empty;
Console.Write(">");
while ((codeLine = Console.ReadLine()) != "Exit();")
{
try
{
//Execute the code
var res = host.Execute(codeLine);
//Write the result back to console
if (res != null)
Console.WriteLine(" = " + res.ToString());
}
catch (Exception e)
{
Console.WriteLine(" !! " + e.Message);
}
Console.Write(">");
}
}
}
}
我假设代码是自解释的。我们创建了一个 Host
类,在其中包装了会话并在该会话中使用 ScriptEngine 执行代码。这是一个非常简单的 REPL 示例——现在,让我们去训练狗吧。
你可以看到,我们在 Host
类中调用了 CreateDog
方法来创建狗,然后我们训练它们——尽管我不确定我们是否真的需要训练它们互相咬。总之,我希望这个想法很清楚。
编译器 API
编译器 API 提供了用于访问代码的语法和语义模型的对象模型。使用编译器 API,你可以获取和操作语法树。常见的语法 API 可以在 Roslyn.Compilers
和 Roslyn.Compilers.Common
命名空间中找到,而特定语言的语法 API 可以在 Roslyn.Compilers.CSharp
和 Roslyn.Compilers.VisualBasic
中找到。
“语法”是指语法结构,“语义”是指由该结构排列的词汇符号的含义。如果你考虑英语,“Dogs Are Cats”在语法上是正确的,但在语义上是无意义的。
构建语法树并访问语义模型
Roslyn 提供了构建语法树以及进行语义分析的 API。因此,让我们编写一些代码来解析一个方法并创建一个语法树。它还展示了如何从我们语法树的语义模型中获取方法符号。
//Parse some code
SyntaxTree tree = SyntaxTree.ParseCompilationUnit
(@"class Bar {
void Foo() { Console.WriteLine(""foo""); }
}");
//Find the first method declaration inside the first class declaration
MethodDeclarationSyntax methodDecl = tree.Root
.DescendentNodes()
.OfType<ClassDeclarationSyntax>()
.First().ChildNodes().OfType<MethodDeclarationSyntax>().First();
//Create a compilation unit
Compilation compilation = Compilation.Create("SimpleMethod").AddSyntaxTrees(tree);
//Get the associated semantic model of our syntax tree
var model = compilation.GetSemanticModel(tree);
//Find the symbol of our Foo method
Symbol methodSymbol = model.GetDeclaredSymbol(methodDecl);
//Get the name of the method symbol
Console.WriteLine(methodSymbol.Name);
在上面的示例中,你可以很容易地看出我们是如何将代码解析为 SyntaxTree
,获取与该语法树关联的语义模型,然后在语义模型中查找信息的。在这种情况下,我们获取对应于语法树中第一个类声明内的第一个方法声明的方法符号。也就是说,根节点的 DescendentNodes().OfType<ClassDeclarationSyntax>().First()
给了我们第一个类声明节点,而该类声明内的 ChildNodes().OfType<MethodDeclarationSyntax>().First()
给了我们第一个方法声明节点。为了简洁起见,使用对象模型遍历语法树是相当容易的。
从语义模型中获取的符号可以用于广泛的场景,包括代码分析。
关于语法树的更多信息
除了将整个代码解析为语法树,你还可以解析代码和子语法节点——例如,你可以将一个语句解析为 StatementSyntax
。
StatementSyntax statement=Syntax.ParseStatement("for (int i = 0; i < 10; i++) { }");
语法树以完整保真的方式保存了整个源代码信息——这意味着它包含了源代码的每一个细节。此外,它们可以进行往返,从语法树或节点重新生成实际的源代码。语法树和节点是不可变的且线程安全的。但是,你可以使用 ReplaceNode
函数来用新节点替换旧节点,从而创建一个包含替换节点的新语法树。目前就这些。
请注意,这个 Roslyn 版本只是一个 CTP,许多功能(如 Dynamic、Expression Trees 等)尚未支持。此外,最终版本可能会有所更改。请参阅此帖子了解限制:http://social.msdn.microsoft.com/Forums/en-US/roslyn/thread/f5adeaf0-49d0-42dc-861b-0f6ffd731825。
享受编码,并在 Twitter 上关注我。