领先技术

在 C# 4.0 中使用动态关键字

Dino Esposito

Dino Esposito静态类型检查的引入是编程语言史上一个重要的里程碑。在二十世纪七十年代,Pascal 和 C 等语言开始执行静态类型和强类型检查。有了静态类型检查,对于任何无法传递相应类型方法参数的调用,编译器都将产生错误。同样,如果您试图调用类型实例中不存在的方法,编译器也应该会产生错误。

推进动态类型检查这一相对应方法的其他语言在过去几年有所发展。动态类型检查否定了以下这一观点:即变量类型是在编译时静态确定的,当变量在作用域内时应该永远不变。不过请注意,动态类型检查并非认为各个类型都是相同的,可以对类型进行自由组合。例如,即使使用动态类型检查,您仍不能为整数添加布尔值。动态类型检查的不同之处在于,检查是发生在程序执行时,而不是编译时。

静态类型化与动态类型化

Visual Studio 2010 和 C# 4.0 提供了新的动态关键字,能够在传统上采用静态类型化的语言中实现动态类型化。不过,在深入了解 C#4.0 的动态方面之前,我们需要记录一些基本术语。

让我们定义某个变量,作为只能采用特定类型数值的存储位置。下一步,让我们说明静态类型化语言的四个基本属性:

  • 每个表达式都属于已在编译时确定的类型。
  • 变量只能属于已在编译时确定的类型。
  • 编译器保证在将表达式指定给变量时采用的类型限制满足对变量的限制。
  • 重载解析等语义分析任务发生在编译时,其结果会输入到程序集。

动态语言的属性正相反。每个表达式和每个变量的类型并未在编译时就确定。存储限制(如果有的话)是在运行时进行检查,而在编译时会被忽略。语义分析也只发生在运行时。

静态类型化语言确实可以实现某些操作的动态化。它们提供了转换运算符,供您作为运行时操作尝试进行类型转换。程序代码中已加入了转换功能,您可以将转换运算符所表达的语义总结为“动态检查此转换在运行时的有效性。”

不过,就动态和静态(也可以说是强和弱)等属性而言:目前与应用到整个编程语言相比,应用到语言的各个功能效果更好。

让我们大致考察一下 Python 和 PHP。这两者都是动态语言,可供您使用变量,并提供了运行时环境,可获知实际存储在其中的类型。不过如果使用 PHP,您可以在同一作用域的同一变量中同时存储整数和字符串等等。从这一意义上讲,PHP(类似于 JavaScript)是一种弱类型化的动态语言。

与之相对,Python 只会给您一次机会来设置变量类型,这使得它更贴近强类型化。您可以对变量动态指定类型,并由运行时环境从指定值推断其类型。不过,在此之后,您不能在此变量中存储任何不当类型的值。

C# 中的动态类型

C#4.0 所拥有的功能使其兼具动态和静态以及弱类型化和强类型化这两种特点。C# 原本属于静态类型化语言,但在使用动态关键字的任何上下文中,它都可以成为动态类型化语言,如下所示:

dynamic number = 10;
Console.WriteLine(number);

而且因为动态关键字是上下文关键字,而不是保留关键字,如果您有现有变量或由方法命名的动态关键字,这一点仍然成立。

请注意,C#4.0 不强制您使用动态关键字,就像 C#3.0 不强制您使用 var、lambda 或对象初始值一样。C#4.0 提供了新的动态关键字,专门用于简化对一些众所周知的情形的处理。这种语言尽管有能力以更有效的方式与动态对象互动,从本质上讲它仍然属于静态类型化语言。

为什么要使用动态对象?首先,您可能不知道正在处理的对象的类型。对于如何确定给定变量的静态类型,您可能有线索,但却拿不准:这种情况非常常见,例如当您处理 COM 对象或使用反射来抓取实例时,就会发生这种情况。这种情况下即可使用动态关键字来简化某些操作。采用动态方式书写的代码更易于读写,这样便于理解与维护应用程序。

其次,您的对象从本质上讲可能就是变化的。您处理的可能正是 IronPython 和 IronRuby 等动态编程环境所生成的对象。不过,您还可以使用此功能来处理 HTML DOM 对象(这取决于 expando 属性)和在创建时专门指定了动态属性的 Microsoft .NET Framework 4 对象。

使用动态变量

一定要理解以下概念:在 C# 类型系统中,动态是一种类型。动态的含义非常特殊,但它绝对是一种类型,一定要将它看作类型。您可以将动态指定为自己所声明变量的类型、集合中的项目类型或某方法的返回值,还可以将动态用作方法参数的类型;不过,您不可以将动态用于运算符类型,也不可以将其用作类的基类型。

下面的代码说明了如何在方法主体中声明动态变量:

public void Execute()  { 
  dynamic calc = GetCalculator();
  int result = calc.Sum(1, 1);
}

如果您充分了解由 GetCalculator 方法返回的对象类型,您可以声明该类型的变量 calc,也可以作为 var 声明变量,以供编译器了解具体细节。不过,使用 var 或显式静态类型,要求您确定 GetCalculator 所返回类型的约定上存在 Sum 方法。如果该方法不存在,您就会收到编译器错误。

采用动态方法,您可以推迟到执行时再确定表达式是否正确。只要方法 Sum 存在于变量 calc 所存储的类型中,代码即会得到编译,并在运行时得到解析。

您还可以使用此关键字在类上定义属性。这样做,您就可以用公共、受保护甚至静态等所需的任何可见性修饰符修饰此成员。

图 1 显示了动态关键字的通用性。主程序包含了根据某函数调用的返回值进行实例化的动态变量。因为此函数会接收并返回动态对象,所以即便没有实现实例化,也无关紧要。在这个例子中,您传递一个数字,然后尝试在此函数中对它进行 double 操作,看看发生什么,会很有趣。

图 1 在函数的签名中使用动态属性

class Program {
  static void Main(string[] args) {
    // The dynamic variable gets the return 
    // value of a function call and outputs it.
    dynamic x = DoubleIt(2);
    Console.WriteLine(x);

    // Stop and wait
    Console.WriteLine(“Press any key”);
    Console.ReadLine();
  }

  // The function receives and returns a dynamic object 
  private static dynamic DoubleIt(dynamic p) {
    // Attempt to "double" the argument whatever 
    // that happens to produce
    
    return p + p;
  }
}

如果您输入数值 2,然后运行此代码,您将得到数值 4;而如果您作为字符串输入 2,您会得到 22。此函数会根据操作数的运行时类型对 + 运算符进行动态解析。如果您将类型更改为 System.Object,会收到编译错误,原因即在于 + 运算符未在 System.Object 上进行定义。动态关键字能使不可能变成可能。

动态与 System.Object

直到 .NET Framework 4,还是只能求助于通用基类,才能使方法可根据不同的情况返回不同的类型。您可能已经通过求助于 System.Object 解决了此问题。返回 System.Object 的函数会向调用者提供可以转换几乎任何内容的实例。这种情况下,为什么还说使用动态的效果要好于使用 System.Object 呢?

在 C# 4 中,被声明为动态的变量后面的实际类型是在运行时加以解析,编译器会认定:声明为动态的变量中的对象完全支持任何操作。也就是说,您编写的代码完全可以调用您认为会在运行时出现的对象上的方法,如下所示:

dynamic p = GetSomeReturnValue();
p.DoSomething();

在 C# 4.0 中,编译器不会对该代码报错。而使用 System.Object 的类似代码将无法进行编译,为了解决这一问题,需要您自己采取一定措施,如进行反射或冒一定风险进行转换。

var 与动态

var 与动态关键字只是表面上相似。var 认为,此变量的类型必须被设置为初始值设定项的编译时类型。

但是,动态意味着此变量的类型可以是 C#4.0 中可用的任何动态类型。动态与 var 的最终含义基本上是相反的。var 讲的是加强与改进静态类型化。其目标是确保编译器可根据初始值设定项返回的确切类型推断变量类型。

动态关键字的目标是完全避免静态类型化。用于变量声明中时,动态会指示编译器完全停止求解变量类型。该类型需要采纳它在运行时的类型。而使用 var 时,您的代码会采用静态类型化方式,其结果与选择在变量声明中使用显式类型的经典方式一致。

两类关键字之间的另一区别是:var 只能出现在局部变量声明中。您不能使用 var 来定义类属性,也不能用它来指定返回值或函数的参数。

作为一名开发人员,如果预计变量包含不确定类型的对象,您就应该使用动态关键字,如对象从 COM 或 DOM API 返回;从动态语言(如 IronRuby)获得;从反射获得;从使用新扩展功能在 C# 4.0 中动态构建的对象中获得。

不过,动态类型不是绕过类型检查,只是将其全部移至运行时。如果在运行时发现不兼容的类型,则引出异常。

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

衷心感谢以下技术专家对本文的审阅:Eric Lippert