在我成为 C/ 开发人员之后尤其是在 Microsoft 推出之前我经常指责采用 Visual Basic 进行编程的同事选择使用那样一种弱类型化的语言
有那么一段时间进行静态类型化和强类型化编程是获得良好的体验的明显选择但是事物总是要发展变化的当今的 开发人员社区(看起来几乎所有前 C/C++ 开发人员都已经转移到这里)经常发现他们明确需要一个更加动态的编程模型上个月我介绍了 Microsoft 在 C# 和 Visual Studio 中提供的一些动态编程功能这个月我将深入探讨一些相关方案首先要介绍 C# 最吸引人的原因之一可以在 NET Framework 中轻松实现 COM 对象编程
轻松访问 COM 对象
如果一个对象的结构和行为不是由完全静态定义的类型(编译器全面了解该类型)描述的话该对象就是动态的不可否认动态一词在这种情况下听起来太宽泛了因此让我们看一个简单的示例在 Script 等脚本语言中以下代码能够成功运行
Set word = CreateObject(WordApplication)
CreateObject 函数假设它获得的 string 参数是某个已注册 COM 对象的 progID它创建该组件的一个实例并返回该实例的 IDispatch 自动化接口IDispatch 接口的细节在脚本语言的任何层级都绝对看不到重要的是您可以编写如下代码
Set word = CreateObject(WordApplication)
wordVisible = True
Set doc = wordDocumentsAdd()
Set selection = wordSelection
selectionTypeText Hello world
selectionTypeParagraph()
docSaveAs(fileName)
在这段代码中您首先创建对组件的引用以便自动执行底层 Microsoft Word 应用程序的行为接着您显示 Word 主窗口添加一个新文档在其中输入一些文字然后将文档保存到某个位置这段代码清晰易懂而且更重要的是能够正常运行
但它能正常运行要归功于 Script 提供的特殊功能后期绑定后期绑定意味着直到执行流程遇到给定对象之前该对象的类型都是未知的当执行流程需要执行给定对象时运行时环境才会开始确保要调用的该对象成员确实存在然后再进行调用在代码真正执行之前不会对其进行任何提前检查
您可能知道像 这样的脚本语言并没有编译器但是Visual Basic(包括 CLR 版本)多年来一直有一项类似的功能我承认我经常会羡慕我的 Visual Basic 同事能够更轻松地使用 COM 对象而需要进行互操作的应用程序(例如 Office)经常采用这种有价值的构造块事实上在有些情况下即使整个应用程序是用 编写的我的团队也会用 Visual Basic 编写一部分互操作代码这有点令人意外?多语言编程不是一种新的前沿技术吗?
在 Visual Basic 中CreateObject 函数的存在是为了解决顽固的兼容性问题重点在于基于 的语言在设计时考虑的是前期绑定NET Framework 能够处理 COM 互操作性方案但是这种方案从来不能由编程语言通过关键字和工具来提供支持这种情况直到 C# 才有所改观
C# (和 Visual Basic)拥有动态查询功能这表明后期绑定现在对于 NET Framework 开发人员来说已经切实可行了借助动态查询功能您可以绕过静态类型检查在代码中直接访问方法属性索引生成器属性和字段而留待运行时进行解析
还通过识别成员声明中的默认值来实现可选参数这意味着在调用拥有可选参数的成员时可以省略可选参数而且既可以按名称也可以按位置来传递参数最后C# 中改进的 COM 绑定功能意味着以前是静态且强类型化的语言现在也支持脚本语言的一些常见功能在您了解如何利用新的动态关键字实现与 COM 对象的流畅操作之前让我们稍稍深入了解一下动态类型查询的内部机制
动态语言运行时
当您在 Visual Studio 中将某个变量声明为动态时其默认配置中根本不会有 IntelliSense有趣的是如果您安装一个类似 ReSharper (/resharper) 的附加工具就可以通过 IntelliSense 获得一些有关动态对象的不完全信息图 显示了带有和不带 ReSharper 的代码编辑器该工具仅仅列出该动态类型上看起来已经定义的成员在最低限度下动态对象是 SystemObject 的实例
图 在带有和不带 ReSharper 的情况下Visual Studio 中的动态对象的 IntelliSense
让我们看看当编译器遇到以下代码时会发生什么情况(这段代码设计得极其简单目的是简化对实现细节的理解)
class Program
{
static void Main(string[] args)
{
dynamic x = ;
ConsoleWriteLine(x)
}
}
在第二行中编译器不会尝试解析符号 WriteLine也不会像传统的静态类型检查器一样发出警报或错误只要遇到 dynamic 关键字C# 的表现就会变得像是解释性语言结果编译器会生成一些临时代码用来解释涉及动态变量或参数的表达式解释器基于动态语言运行时 (DLR)是 机制中的一个全新组件若要使用更具体的术语编译器必须使用 DLR 所支持的抽象语法来生成表达式树并将其传递给 DLR 库进行处理在 DLR 中由编译器提供的表达式被封装在动态更新的站点对象中站点对象负责实时将方法绑定到对象图 显示了真实代码的充分简化版本该真实代码是由前述的简单程序生成的
图 中的代码已经过编辑和简化以方便阅读但它显示了实际情况的要点动态变量映射到 SystemObject 实例然后就会在 DLR 中为程序创建一个站点该站点负责管理 WriteLine 方法及其参数与目标对象之间的绑定该绑定维持在类型 Program 的上下文中为了对动态变量调用方法 ConsoleWriteLine您将调用该站点并传递目标对象(本例中为 Console 类型)及其参数(本例中为动态变量)该站点将在内部检查目标对象是否真的拥有成员 WriteLine并且该成员能够接受类似于变量 x 中目前存储的对象这样的参数如果有任何问题 运行时就会引发 RuntimeBinderException
图 动态变量的真正实现
internal class Program
{
private static void Main(string[] args)
{
object x = ;
if (MainSiteContainersite == null)
{
MainSiteContainersite = CallSite<
Action<CallSite Type object》
Create(BinderInvokeMember(
WriteLine
null
typeof(Program)
new CSharpArgumentInfo[] {
CSharpArgumentInfoCreate(…)
}))
}
MainSiteContainersiteTargetInvoke(
site typeof(Console) x)
}
private static class MainSiteContainer
{
public static CallSite<Action<CallSite Type object》 site;
}
}
使用 COM 对象
现在新的 能够在基于 的应用程序中简单轻松地使用 COM 对象让我们看看如何在 C# 中创建一个 Word 文档并且对您在 NET 和 NET 中需要的代码进行比较示例应用程序将基于给定的模板创建一个新的 Word 文档填入一些内容并将其保存到一个指定位置模板包含一些书签用于容纳一些常用信息无论您面向的是 NET Framework 还是 NET Framework 通过编程来创建 Word 文档的第一步都是添加 Microsoft Word 对象库(请参见图 )
图 引用 Word 对象库
在 Visual Studio 和 NET Framework 之前若要完成此操作您需要类似图 所示的代码
图 在 C# 中创建新的 Word 文档
public static class WordDocument
{
public const String TemplateName = @Sampledotx;
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 = MissingValue;
// Run Word and make it visible for demo purposes
var wordApp = new Application { Visible = true };
// Create a new document
Object template = TemplateName;
var doc = wordAppDocumentsAdd(ref template
ref missingValue ref missingValue ref missingValue)
docActivate()
// Fill up placeholders in the document
Object bookmark_CurrentDate = CurrentDateBookmark;
Object bookmark_Signature = SignatureBookmark;
docBookmarksget_Item(ref bookmark_CurrentDate)RangeSelect()
wordAppSelectionTypeText(currentToString())
docBookmarksget_Item(ref bookmark_Signature)RangeSelect()
wordAppSelectionTypeText(author)
// Save the document
Object documentName = file;
docSaveAs(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)
docClose(ref missingValue
ref missingValue ref missingValue)
wordAppQuit(ref missingValue
ref missingValue ref missingValue)
}
}
为了与 COM 自动化接口交互您经常需要 Variant 类型当您在基于 的应用程序中与 COM 自动化对象交互时您需要将 Variants 表示成普通对象其直接后果是您不能使用字符串来指示 Word 文档所用的模板文件的名称因为必须通过引用来传递 Variant 参数您不得不求助于 Object如下所示
Object template = TemplateName;
var doc = wordAppDocumentsAdd(ref template
ref missingValue ref missingValue ref missingValue)
要考虑的第二个方面是 Visual Basic 和脚本语言远不如 严格例如这些语言不会强制要求您指定 COM 对象声明上某个方法的所有参数Documents 集合的 Add 方法需要四个参数而除非您的语言支持可选参数否则就不能忽略这些参数
正如前文所述C# 支持可选参数这意味着尽管直接用 C# 来重新编译图 中的代码就能正常使用您可能仍会重写这段代码删除所有用来传递缺少的值的 ref 参数如下所示
Object template = TemplateName;
var doc = wordAppDocumentsAdd(template)
借助 C# 中新的省略 ref支持图 中的代码变得更加简单而且更重要的是它变得更容易阅读且语法上与脚本代码更像图 包含编辑过的版本该版本能够用 C# 进行正确编译并且与图 中的代码效果相同
图 在 C# 中创建新的 Word 文档
public static class WordDocument
{
public const String TemplateName = @Sampledotx;
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 = wordAppDocumentsAdd(TemplateName)
templatedDocumentActivate()
// Fill the bookmarks in the document
docBookmarks[CurrentDateBookmark]RangeSelect()
wordAppSelectionTypeText(currentToString())
docBookmarks[SignatureBookmark]RangeSelect()
wordAppSelectionTypeText(author)
// Save the document
docSaveAs(fileName)
// Clean up
templatedDocumentClose()
wordAppQuit()
}
}
图 中的代码允许您使用普通 类型来调用 COM 对象而且可选参数使得它更加简单
中引入的动态关键字和其他 COM 互操作功能不会使代码的运行速度明显加快但能让您像编写脚本一样编写 C# 代码对于 COM 对象来说这种成果可能与性能的提升一样重要
无 PIA 部署
从 NET Framework 推出以来您就可以将 COM 对象包装到托管类中然后从基于 NET 的应用程序中使用为了实现此目的您需要使用由 COM 对象的供应商提供的主互操作程序集 (PIA)PIA 必不可少必须与客户端应用程序一起部署但是很多时候PIA 都太大了并且会包含整个 COM API因此将它们打包到安装程序中不是什么令人愉快的经验
Visual Studio 提供了无 PIA 选项无 PIA是指编译器能够嵌入您在当前程序集中从 PIA 获取的必要定义因此只有真正需要的定义才会进入最终的程序集而不需要将供应商的 PIA 整个打包到安装程序中图 显示了属性框中的选项该选项在 Visual Studio 中实现了无 PIA
图 在 Visual Studio 中启用无 PIA 选项
无 PIA 功能基于 C# 的一项称为类型等效性的功能简而言之类型等效性就是两个截然不同的类型可在运行时被当作是等效的并且可以互换使用类型等效性的典型示例是不同程序集中定义的两个同名的接口它们是不同的类型但只要存在相同的方法它们就可以互换使用
总之使用 COM 对象仍然代价不低但是 C# 中的 COM 互操作支持使您编写的代码简单得多从基于 NET Framework 框架的应用程序处理 COM 对象可使您与传统的应用程序和关键业务方案建立联系如果不这样做您的控制力就会大大降低COM 在 NET Frameworok 中是相当棘手的问题但动态功能使这个问题变得不那么困难