领先技术

C# 4.0、动态关键字与 COM

Dino Esposito

在我成为 C/C++ 开发人员之后,尤其是在 Microsoft .NET Framework 推出之前,我经常指责采用 Visual Basic 进行编程的同事选择使用那样一种弱类型化的语言。

有那么一段时间,进行静态类型化和强类型化编程是获得良好的软件开发体验的明显选择。但是事物总是要发展变化的,当今的 C# 开发人员社区(看起来几乎所有前 C/C++ 开发人员都已经转移到这里)经常发现他们明确需要一个更加动态的编程模型。上个月,我介绍了 Microsoft 在 C# 4.0 和 Visual Studio 2010 中提供的一些动态编程功能。这个月,我将深入探讨一些相关方案。首先要介绍 C# 4.0 最吸引人的原因之一:可以在 .NET Framework 中轻松实现 COM 对象编程。

轻松访问 COM 对象

如果一个对象的结构和行为不是由完全静态定义的类型(编译器全面了解该类型)描述的话,该对象就是动态的。不可否认,“动态”一词在这种情况下听起来太宽泛了,因此让我们看一个简单的示例。在 VBScript 等脚本语言中,以下代码能够成功运行:

Set word = CreateObject("Word.Application")

CreateObject 函数假设它获得的 string 参数是某个已注册 COM 对象的 progID。它创建该组件的一个实例,并返回该实例的 IDispatch 自动化接口。IDispatch 接口的细节在脚本语言的任何层级都绝对看不到。重要的是您可以编写如下代码:

Set word = CreateObject("Word.Application")
word.Visible = True
Set doc = word.Documents.Add()
Set selection = word.Selection
selection.TypeText "Hello, world"
selection.TypeParagraph()

doc.SaveAs(fileName)

在这段代码中,您首先创建对组件的引用,以便自动执行底层 Microsoft Office Word 应用程序的行为。接着,您显示 Word 主窗口,添加一个新文档,在其中输入一些文字,然后将文档保存到某个位置。这段代码清晰易懂,而且更重要的是,能够正常运行。

但它能正常运行要归功于 VBScript 提供的特殊功能:后期绑定。后期绑定意味着直到执行流程遇到给定对象之前,该对象的类型都是未知的。当执行流程需要执行给定对象时,运行时环境才会开始确保要调用的该对象成员确实存在,然后再进行调用。在代码真正执行之前,不会对其进行任何提前检查。

您可能知道,像 VBScript 这样的脚本语言并没有编译器。但是,Visual Basic(包括 CLR 版本)多年来一直有一项类似的功能。我承认,我经常会羡慕我的 Visual Basic 同事能够更轻松地使用 COM 对象,而需要进行互操作的应用程序(例如 Office)经常采用这种有价值的构造块。事实上,在有些情况下,即使整个应用程序是用 C# 编写的,我的团队也会用 Visual Basic 编写一部分互操作代码。这有点令人意外?多语言编程不是一种新的前沿技术吗?

在 Visual Basic 中,CreateObject 函数的存在是为了解决顽固的兼容性问题。重点在于基于 .NET Framework 的语言在设计时考虑的是前期绑定。.NET Framework 能够处理 COM 互操作性方案,但是这种方案从来不能由编程语言通过关键字和工具来提供支持,这种情况直到 C# 4.0 才有所改观。

C# 4.0(和 Visual Basic)拥有动态查询功能,这表明后期绑定现在对于 .NET Framework 开发人员来说已经切实可行了。借助动态查询功能,您可以绕过静态类型检查,在代码中直接访问方法、属性、索引生成器属性和字段,而留待运行时进行解析。

C# 4.0 还通过识别成员声明中的默认值来实现可选参数。这意味着,在调用拥有可选参数的成员时,可以省略可选参数。而且,既可以按名称也可以按位置来传递参数。最后,C# 4.0 中改进的 COM 绑定功能意味着以前是静态且强类型化的语言现在也支持脚本语言的一些常见功能。在您了解如何利用新的动态关键字,实现与 COM 对象的流畅操作之前,让我们稍稍深入了解一下动态类型查询的内部机制。

动态语言运行时

当您在 Visual Studio 2010 中将某个变量声明为动态时,其默认配置中根本不会有 IntelliSense。有趣的是,如果您安装一个类似 ReSharper 5.0 (jetbrains.com/resharper) 的附加工具,就可以通过 IntelliSense 获得一些有关动态对象的不完全信息。图 1 显示了带有和不带 ReSharper 的代码编辑器。该工具仅仅列出该动态类型上看起来已经定义的成员。在最低限度下,动态对象是 System.Object 的实例。


图 1 在带有和不带 ReSharper 的情况下,Visual Studio 2010 中的动态对象的 IntelliSense

让我们看看当编译器遇到以下代码时会发生什么情况(这段代码设计得极其简单,目的是简化对实现细节的理解):

class Program
{
  static void Main(string[] args) 
  { 
    dynamic x = 1;
    Console.WriteLine(x);
  }
}

在第二行中,编译器不会尝试解析符号 WriteLine,也不会像传统的静态类型检查器一样发出警报或错误。只要遇到 dynamic 关键字,C# 的表现就会变得像是解释性语言。结果,编译器会生成一些临时代码,用来解释涉及动态变量或参数的表达式。解释器基于动态语言运行时 (DLR),是 .NET Framework 机制中的一个全新组件。若要使用更具体的术语,编译器必须使用 DLR 所支持的抽象语法来生成表达式树,并将其传递给 DLR 库进行处理。在 DLR 中,由编译器提供的表达式被封装在动态更新的站点对象中。站点对象负责实时将方法绑定到对象。图 2 显示了真实代码的充分简化版本,该真实代码是由前述的简单程序生成的。

图 2 中的代码已经过编辑和简化以方便阅读,但它显示了实际情况的要点。动态变量映射到 System.Object 实例,然后就会在 DLR 中为程序创建一个站点。该站点负责管理 WriteLine 方法及其参数与目标对象之间的绑定。该绑定维持在类型 Program 的上下文中。为了对动态变量调用方法 Console.WriteLine,您将调用该站点,并传递目标对象(本例中为 Console 类型)及其参数(本例中为动态变量)。该站点将在内部检查目标对象是否真的拥有成员 WriteLine,并且该成员能够接受类似于变量 x 中目前存储的对象这样的参数。如果有任何问题,C# 运行时就会引发 RuntimeBinderException。

图 2 动态变量的真正实现

internal class Program
{
  private static void Main(string[] args)
  {
    object x = 1;

    if (MainSiteContainer.site1 == null)
    {
      MainSiteContainer.site1 = CallSite<
        Action<CallSite, Type, object>>
        .Create(Binder.InvokeMember(
          "WriteLine", 
          null, 
          typeof(Program), 
          new CSharpArgumentInfo[] { 
            CSharpArgumentInfo.Create(...) 
          }));
    }
    MainSiteContainer.site1.Target.Invoke(
      site1, typeof(Console), x);
  }

  private static class MainSiteContainer
  {
    public static CallSite<Action<CallSite, Type, object>> site1;
  }
}

使用 COM 对象

现在,新的 C# 4.0 能够在基于 .NET Framework 的应用程序中简单轻松地使用 COM 对象。让我们看看如何在 C# 中创建一个 Word 文档,并且对您在 .NET 3.5 和 .NET 4 中需要的代码进行比较。示例应用程序将基于给定的模板创建一个新的 Word 文档,填入一些内容,并将其保存到一个指定位置。模板包含一些书签,用于容纳一些常用信息。无论您面向的是 .NET Framework 3.5 还是 .NET Framework 4,通过编程来创建 Word 文档的第一步都是添加 Microsoft Word 对象库(请参见图 3)。


图 3 引用 Word 对象库

在 Visual Studio 2010 和 .NET Framework 4 之前,若要完成此操作,您需要类似图 4 所示的代码。

图 4 在 C# 3.0 中创建新的 Word 文档

public static class WordDocument
{
  public const String TemplateName = @"Sample.dotx";
  public const String CurrentDateBookmark = "CurrentDate";
  public const String SignatureBookmark = "Signature";

  public static void Create(String file, DateTime now, String author)
  {
    // Must be an Object because it is passed as a ref
    Object missingValue = Missing.Value;

    // Run Word and make it visible for demo purposes
    var wordApp = new Application { Visible = true };

    // Create a new document
    Object template = TemplateName;
    var doc = wordApp.Documents.Add(ref template,
      ref missingValue, ref missingValue, ref missingValue);
    doc.Activate();

    // Fill up placeholders in the document
    Object bookmark_CurrentDate = CurrentDateBookmark;
    Object bookmark_Signature = SignatureBookmark;
    doc.Bookmarks.get_Item(ref bookmark_CurrentDate).Range.Select();
    wordApp.Selection.TypeText(current.ToString());
    doc.Bookmarks.get_Item(ref bookmark_Signature).Range.Select();
    wordApp.Selection.TypeText(author);

    // Save the document 
    Object documentName = file;
    doc.SaveAs(ref documentName,
      ref missingValue, ref missingValue, ref missingValue, 
      ref missingValue, ref missingValue, ref missingValue, 
      ref missingValue, ref missingValue, ref missingValue, 
      ref missingValue, ref missingValue, ref missingValue,
      ref missingValue, ref missingValue, ref missingValue);

    doc.Close(ref missingValue, 
      ref missingValue, ref missingValue);
    wordApp.Quit(ref missingValue, 
      ref missingValue, ref missingValue);
  }
}

为了与 COM 自动化接口交互,您经常需要 Variant 类型。当您在基于 .NET Framework 的应用程序中与 COM 自动化对象交互时,您需要将 Variants 表示成普通对象。其直接后果是您不能使用字符串来指示 Word 文档所用的模板文件的名称,因为必须通过引用来传递 Variant 参数。您不得不求助于 Object,如下所示:

Object template = TemplateName;
var doc = wordApp.Documents.Add(ref template,
  ref missingValue, ref missingValue, ref missingValue);

要考虑的第二个方面是 Visual Basic 和脚本语言远不如 C# 3.0 严格。例如,这些语言不会强制要求您指定 COM 对象声明上某个方法的所有参数。Documents 集合的 Add 方法需要四个参数,而除非您的语言支持可选参数,否则就不能忽略这些参数。

正如前文所述,C# 4.0 支持可选参数。这意味着,尽管直接用 C# 4.0 来重新编译图 4 中的代码就能正常使用,您可能仍会重写这段代码,删除所有用来传递缺少的值的 ref 参数,如下所示:

Object template = TemplateName;
var doc = wordApp.Documents.Add(template);

借助 C# 4.0 中新的“省略 ref”支持,图 4 中的代码变得更加简单,而且更重要的是,它变得更容易阅读,且语法上与脚本代码更像。图 5 包含编辑过的版本,该版本能够用 C# 4.0 进行正确编译,并且与图 4 中的代码效果相同。

图 5 在 C# 4.0 中创建新的 Word 文档

public static class WordDocument
{
  public const String TemplateName = @"Sample.dotx";
  public const String CurrentDateBookmark = "CurrentDate";
  public const String SignatureBookmark = "Signature";

  public static void Create(string file, DateTime now, String author)
  {
    // Run Word and make it visible for demo purposes
    dynamic wordApp = new Application { Visible = true };
            
    // Create a new document
    var doc = wordApp.Documents.Add(TemplateName);
    templatedDocument.Activate();

    // Fill the bookmarks in the document
    doc.Bookmarks[CurrentDateBookmark].Range.Select();
    wordApp.Selection.TypeText(current.ToString());
    doc.Bookmarks[SignatureBookmark].Range.Select();
    wordApp.Selection.TypeText(author);

    // Save the document 
    doc.SaveAs(fileName);

    // Clean up
    templatedDocument.Close();
    wordApp.Quit();
  }
}

图 5 中的代码允许您使用普通 .NET Framework 类型来调用 COM 对象。而且,可选参数使得它更加简单。

C# 4.0 中引入的动态关键字和其他 COM 互操作功能不会使代码的运行速度明显加快,但能让您像编写脚本一样编写 C# 代码。对于 COM 对象来说,这种成果可能与性能的提升一样重要。

无 PIA 部署

从 .NET Framework 推出以来,您就可以将 COM 对象包装到托管类中,然后从基于 .NET 的应用程序中使用。为了实现此目的,您需要使用由 COM 对象的供应商提供的主互操作程序集 (PIA)。PIA 必不可少,必须与客户端应用程序一起部署。但是,很多时候,PIA 都太大了,并且会包含整个 COM API,因此将它们打包到安装程序中不是什么令人愉快的经验。

Visual Studio 2010 提供了无 PIA 选项。“无 PIA”是指编译器能够嵌入您在当前程序集中从 PIA 获取的必要定义。因此,只有真正需要的定义才会进入最终的程序集,而不需要将供应商的 PIA 整个打包到安装程序中。图 6 显示了“属性”框中的选项,该选项在 Visual Studio 2010 中实现了无 PIA。


图 6 在 Visual Studio 2010 中启用无 PIA 选项

无 PIA 功能基于 C# 4.0 的一项称为“类型等效性”的功能。简而言之,类型等效性就是两个截然不同的类型可在运行时被当作是等效的,并且可以互换使用。类型等效性的典型示例是不同程序集中定义的两个同名的接口。它们是不同的类型,但只要存在相同的方法,它们就可以互换使用。

总之,使用 COM 对象仍然代价不低,但是 C# 4.0 中的 COM 互操作支持使您编写的代码简单得多。从基于 .NET Framework 框架的应用程序处理 COM 对象,可使您与传统的应用程序和关键业务方案建立联系,如果不这样做,您的控制力就会大大降低。COM 在 .NET Frameworok 中是相当棘手的问题,但动态功能使这个问题变得不那么困难。

Dino Esposito* 是 Microsoft Press 出版的《Programming ASP.NET MVC》一书的作者,也是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press, 2008) 一书的合著者。Esposito 定居于意大利,经常在世界各地的业内活动中发表演讲。您可访问他的博客,网址为 weblogs.asp.net/despos。*

衷心感谢以下技术专家审阅本文:Alex Turner