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

使用 CodeDom 进行动态代码集成

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (23投票s)

2008年5月23日

CPOL

8分钟阅读

viewsIcon

83568

downloadIcon

1207

虽然市面上有很多表达式求值器,但CodeDom框架允许您在运行时将任何.NET语言与代码片段关联起来。

引言

我偶尔会实现一个程序,我希望能够交互式地更改某些参数或交互式地重写一部分代码。参数可以通过现有的GUI组件轻松解决。 .NET框架的System.CodeDom命名空间为后者提供了一套很好的工具。存在许多关于使用CodeDom文档的教程,其中大多数都侧重于创建源代码文件。在.NET 2.0中,添加了从内部字符串编译代码的支持。本文将介绍如何从文本框等位置获取一段代码,并立即在您的系统中使用它。我还将介绍一些简单的用例,允许您从用户那里获取单个表达式或计算,并将其嵌入到委托函数或用户看不到的自己的界面中。在第二部分,我将介绍一些健壮性问题,提供更多示例,并讨论与新的LINQ技术相关的实用性。请注意,我将使用C#来解释这一点,但这个概念对于任何.NET语言来说都同样适用。

初始设置

在本教程的这一部分,我们将专注于一项简单任务:将字符串输出到控制台。让我们从这个简单的程序开始,看看如何重构它以提供更多灵活性。文章很长,主要是因为我将采取循序渐进的方式,并在代码重构时重复相关部分。在教程结束之前,我将忽略任何错误检查或异常。考虑这个非常简单的控制台应用程序

class Foo 
{
   public void Print()
   {
      System.Console.WriteLine("Hello from class Foo");
   }
}

static void Main( string[] args )
{
   Foo myFoo = new Foo();

   myFoo.Print();
}

动态编译

我的最终目标是以文本字符串的形式获取源代码。因此,作为第一个测试,我将上面的代码转换为字符串,然后在Main中,我将使用CodeDom技术和反射创建一个Foo实例。这需要三个步骤:

  • 创建一个支持ICodeCompiler接口的对象的实例。就我们而言,我们想要一个处理C#的实例,因此我们将使用Microsoft.CSharp命名空间中的CSharpCodeProvider
  • 将源代码编译成程序集。
  • 使用反射处理程序集以获取类型信息,并创建类型实例。

为了达到这个目的,我将创建一个名为CompileSource的实用方法,该方法将接受一个字符串并输出一个程序集。代码如下:

private static Assembly CompileSource( string sourceCode )
{
   CodeDomProvider cpd = new CSharpCodeProvider();
   CompilerParameters cp = new CompilerParameters();
   cp.ReferencedAssemblies.Add("System.dll");
   cp.ReferencedAssemblies.Add("ClassLibrary1.dll");
   cp.GenerateExecutable = false;
   // Invoke compilation.
   CompilerResults cr = cpd.CompileAssemblyFromSource(cp, sourceCode);

   return cr.CompiledAssembly;
}

相当直接;除了创建ICodeCompiler和编译源代码之外,我们还需要创建一个CompilerParameters实例并进行配置。请注意,您可以添加任何您希望向用户公开的引用。结果程序集是从CompileAssemblyFromSource方法返回的CompilerResults类的实例中提取的。好的,现在让我们来使用它。下面是新的应用程序(不包含上面的CompileSource代码)。我正在使用字符串@运算符来指定源代码。使用这种语法,字符串中的任何引号都需要重复。

namespace CreateDelegate
{
   class Program
   {
      static void Main( string[] args )
      {
          Test1();
      }
      private static void Test1()
      {
         //
         // Create an instance of type Foo and call Print
         //
         string FooSource = @"
            class Foo
            {
               public void Print()
               {
                  System.Console.WriteLine(""Hello from class Foo"");
               }
            }";

         Assembly assembly = CompileSource(FooSource);
         object myFoo = assembly.CreateInstance("Foo");
         // myFoo.Print(); // - Print not a member of System.Object
         // ((Foo)myFoo).Print(); // - Type Foo unknown
      }
   }
}

请注意,Main已成功创建了Foo类的实例,但存在一些问题:

  1. Main硬编码了类名(“Foo”)。
  2. 当您编译此应用程序时,类型Foo不存在。它是在运行时创建的。因此,我们必须使用统一类型object来处理我们的Foo实例。无法将其强制转换为Foo类型。
  3. 由于上面的第2点,我无法使用myFoo.Print调用Print。这将产生编译器错误,因为类型object没有Print方法。

因此,新版本不打印任何内容。使用反射,可以获取Foo类的所有方法的列表,并直接InvokePrint方法。直接这样做相当笨拙,并且不是一个非常有用的编程实践。相反,我将介绍两种处理这些类型的方法,它们提供了更优雅的解决方案:

  • 面向接口编程
  • 使用委托

这两种方法都需要主程序、源代码字符串或两者都有额外的知识。使用哪种取决于您应用程序的具体需求,稍后将在用例中进行讨论。

面向接口编程

现代软件设计的一个基本原则是面向接口编程。对于我们的简单示例,我们将添加一个IPrint接口:

namespace FooLibrary
{
   public interface IPrint
  {
      void Print();
   }
}

我将其放在FooLibrary命名空间中。仅有一个接口是不够的。它需要能够被任何希望实现它的类访问。最简单的方法是将您的关键接口放入一个或多个类型库(DLL)中。我已经为这个项目创建了DLLIPrint.dll。我还将其添加为我们控制台应用程序的引用。修改后的测试如下:

using FooLibrary;
private static void Test2()
{
   //
   // Create an instance of type FooLibrary.IPrint and call Print
   //
   string FooInterface = @"
      class Foo : FooLibrary.IPrint {
         public void Print() {
            System.Console.WriteLine(""Hello from class Foo"");
         }
      }";

   Assembly assembly = CompileSource(FooInterface);
   IPrint foo = assembly.CreateInstance("Foo") as IPrint
   if (foo != null)
   {
      foo.Print();
   }
}

我们做了以下更改:

  1. 我们添加了FooLibraryusing语句。
  2. 我们将CreateInstance的结果强制转换为IPrint类型(使用as运算符)。
  3. 如果转换成功,我们现在可以调用Print()。

为了使我们的基于字符串的源代码能够编译,它需要访问IPrint.dll程序集中的IPrint类型。我们的CompileSource方法需要添加以下行:

cp.ReferencedAssemblies.Add("IPrint.dll");

请注意,即使我们包含了FooLibraryusing语句,我们仍然需要完全限定字符串fooSource中的IPrint。这是有道理的,因为它被视为一个单独的编译单元。我们也可以在字符串中添加一个额外的using FooLibrary

使用反射搜索类型

这解决了上述三个问题中的两个。Main仍然硬编码了对字符串名称“Foo”的引用。将类名硬编码可以很容易地替换为额外的用户控件,让用户指定要创建的类型的名称。这可能会出错,因为用户更改了代码,但可能忘记更改名称。为了从Main中移除知道具体类型名称的实现智能,我们可以搜索程序集。我们想要的是获取一个程序集和一个已知的接口名称,然后搜索实现该接口的具体类型名称。这可以通过以下方式实现:

private static void Test2()
{
   //
   // Create an instance of type FooLibrary.IPrint and call Print
   //
   string FooInterface = @"
      class Foo : FooLibrary.IPrint {
         public void Print() {
            System.Console.WriteLine(""Hello from class Foo"");
         }
      }";

   Assembly assembly = CompileSource(FooInterface);

   Type fType = assembly.GetTypes()[0];
   Type iType = fType.GetInterface("FooLibrary.IPrint");
   if (iType != null)
   {
      FooLibrary.IPrint foo = (FooLibrary.IPrint)
            assembly.CreateInstance(fType.FullName);
      foo.Print();
   }
}

在这里,我们假设只有一个类型,或者我们感兴趣的类型是第一个。这可以很容易地更改为搜索所有类型,留给读者作为练习。因此,我们现在可以创建支持我们接口的类型的实例,其中类型定义最初是一个文本字符串。如果最终目标只是执行Print()方法中的语句,那么可以使用委托构建一个更简单的方案,我们将在下面讨论。

动态委托

让我们考虑这样一种情况,我们希望允许用户使用某个用户界面组件指定一个任意的二维函数 f(x,y)。示例如下:

float func2D( float x, float y)
{
   float value; value = Math.Cos(x) * Math.Sin(x) + 1.0f;
   return value;
}

有很多关于创建自己的表达式引擎和解析此类函数的文章。使用CodeDom,您可以将任何.NET语言视为脚本语言。这个函数与我们上面的Print方法有什么不同?嗯,首先,它不包含在类或命名空间中。您可以使用反射从全局命名空间访问此函数,但我将采取不同的路线。我将把它包装在一个类中,以便我们可以使用与上面类似的逻辑。我以非常特定的方式构建了这段代码。我希望用户输入的只是行(或一组语句):

value = Math.Cos(x) * Math.Sin(y) + 1.0f;

这将使用户不必知道确切的函数签名、类名、接口名称等。如何?通过字符串操作!我定义了两个额外的字符串,称为methodHeadermethodFooter。然后,我取表达式的字符串,并将其夹在头部和尾部字符串之间,然后再将其传递给CompileSource方法。例如,原始示例可以这样编码:

string FooSourcHeader = @"
   using System;
   class Foo {
      static public void Print()
      {";
string FooSourceFooter = @"
      }
   }";

string myPrint = FooSourcHeader
               + @"Console.WriteLine(""Embedded Hello"");"
               + FooSourceFooter;

Assembly fooAssembly = CompileSource(myPrint);
IPrint myFoo = fooAssembly.CreateInstance(“Foo”) as IPrint;
myFoo.Print();

这样做有什么好处?嗯,现在我知道“Foo”是类名,并且它实现了IPrint接口。我对这些字符串有控制权!用户只需要知道他们需要提供一个零个或多个语句序列。我甚至可以提供一个更详细的类定义,并告诉用户他们可以访问哪些字段。好的,这很酷,但这一部分是关于动态委托的。委托定义了方法签名的类型。它们通常用于回调和事件。任何具有相同签名的方法都可以被视为委托的类型。

        delegate void PrintDelegate();
        private static void Test3()
        {
            //
            // Get a PrintDelegate from an instance of the type Foo.
            //
            string FooSourcHeader = @"
               using System;
               class Foo {
                   static public void Print()
                   {";
            string FooSourceFooter = @"
                   }
               }";

            string myPrint = FooSourcHeader
                + @"Console.WriteLine(""Hello from Print delegate"");"
                + FooSourceFooter;

            Assembly assembly = CompileSource(myPrint);
            //
            // Try to Invoke a method
            //
            Type fooType = assembly.GetType("Foo");
            MethodInfo printMethod = fooType.GetMethod("Print");
            PrintDelegate fooPrint = (PrintDelegate)
                  Delegate.CreateDelegate(typeof(PrintDelegate), printMethod);
            fooPrint();
        }

摘要

好了,这就是全部。我们可以使用文本字符串在运行时向我们的系统添加功能。我们可以根据需要允许完整的类定义,或者简单的函数体。在第二部分,我将提供一些用户控件和基于这些想法构建的示例演示。我还将讨论一些健壮性问题,以及如何将任何编译器错误反馈给用户。

历史

  • 2008-05-22 - 初始文章。
© . All rights reserved.