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

C# 4.0 中的 dynamic 关键字

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (38投票s)

2010年3月30日

CPOL

8分钟阅读

viewsIcon

147555

downloadIcon

919

对 'dynamic' 关键字的介绍。

引言

本文是系列文章的第二篇,介绍了 C# 4.0 的一些新语言特性。我的第一篇文章介绍了 命名参数和可选参数[^]。值得重申的是,本系列文章旨在提供对语言特性的简明介绍,而不是深入分析,特别适合 C# 4.0 和 C# 的初学者。

正如在第一篇文章中所讨论的,C# 4.0 的主要目标之一是增加对框架中动态语言的支持;当然,dynamic 关键字起着重要作用。我无意讨论 DLR,因为它在与动态语言打交道时大部分是不可见的,而我对动态语言的经验很少。

dynamic 类型有什么用?

dynamic 是一种新的静态类型,它充当一个占位符,表示一个类型直到运行时才知晓。一旦声明了 dynamic 对象,就可以对其进行操作、获取和设置属性,甚至几乎可以像任何普通类型一样传递 dynamic 实例。这看起来是相当标准的用法

//This could be the result of call to Python / Ruby etc !
dynamic myDynamic = GetDynamic(....);
myDynamic.Foo("Hello"); //Call a method
myDynamic.Bar = myDynamic.Baz; //Using properties
int Quux = myDynamic[0]; //calling on an indexer
int Qux = myDynamic(12,5); //Invoking as delegate
MyMethod(myDynamic); // Passing as parameter

动态支持可能是 C# 4.0 中最具争议的新特性。我听说有同行开发者抱怨它破坏了 C# 的面向对象特性,因为它提供了一种在编译时定义“未知”类型并在其上执行“未知”操作的方法。我承认,在我开始接触这些特性之前,我也持同样的观点。那么为什么会有争议呢?类型在运行时才可知(取决于与什么交互,类型可能是在运行时动态生成的!),这使得 CLR 编译器无法知道 dynamic 对象上的操作是否存在(这与 var 关键字不同)。

例如,如果动态创建的对象没有一个接受 string 的名为 Foo 的方法,那么上面的代码会编译,但在执行时会抛出 RuntimeBinderException。问题就出在这里。我们现在有了一个绕过 C# 正常严格类型的机制。现在可以编译一些在编译时如果经过彻底检查则不会通过的代码。此外,如果误用 dynamic,会使重构更加困难。例如,如果 `GetDynamic(....)` 调用中创建的对象被重构以重命名其 `Foo(string s)` 方法,我代码片段中的代码不会自动更改;事实上,它仍然可以编译。

为什么要使用 dynamic?

在静态类型世界中,dynamic 为开发人员提供了很大的自由度。当处理类型在编译时可知的情况下,应不惜一切代价避免使用 dynamic 关键字。前面我说过我最初的反应是负面的,是什么改变了我的想法?引用玛格丽特·阿特伍德的话来说,语境就是一切。在静态类型处理中,dynamic 毫无意义。如果你处理的是未知或动态类型,通常需要通过反射与其进行通信。反射代码不易阅读,并且具有上述 dynamic 类型所有的弊端。在这种情况下,dynamic 就非常有意义了。

在底层,dynamic 类型会编译成包装的反射代码,除非该对象是 COM 对象(在这种情况下,操作通过 IDispatch 动态调用)或该对象实现了 IDynamicObject(IronPython 和 IronRuby 以及一些新兴 API 大量使用该接口)。IDynamicObject 的子类会被要求直接执行操作,从而可以重新定义动态操作。从这个角度来看,dynamic 是 C# 工具集的一个极佳补充。“实际示例”部分提供了一个重写反射代码的演示。但首先,让我们看看 dynamic 对象的更一般属性。

赋值和声明

对象可以直接赋值(并隐式转换)

dynamic d = "Hello World"; //Implicit conversion
string s = d; //Conversion at Assignment

如上所述,dynamic 对象可以声明为某个方法的返回值

dynamic d = GetDynamicObject(...);

以下代码将无法编译,会报告使用了未赋值的变量 message

dynamic d;
dynamic type = d.GetType();

常见的 RuntimeBindingException

现在是时候看看 RuntimeBindingException 了。通常,当调用的操作在运行时无法解析时,就会发生这些异常。以下代码可以编译,但在执行时会抛出 RuntimeBindingException,具体描述在注释中

//Attempting to access null reference
dynamic nulled = null;
dynamic type = nulled.GetType();
//Cannot perform runtime binding on null reference.
//Accessing a non-existant property
dynamic foo = 7;
dynamic baz = myInt.Bar; //'int' does not contain a definition for 'Bar'
//Accessing a non-existant method
dynamic foo = 7;
dynamic baz = myInt.Baz(); //'int' does not contain a definition for 'Baz'
//Bad implicit conversion
dynamic foo = "Hello";
int bar = foo; // Cannot implicitly convert type 'string' to 'int'.

上述代码的静态类型版本将无法编译,因此当类型在编译时已知时,dynamic 代码会更弱。但如果与反射调用等进行比较,异常机制会更整洁、对开发人员更透明。

重载解析

Foo foo = new Foo();
dynamic myDynamic = new Bar();
var returnedValue = foo.Baz(myDynamic);

运行时将根据 `myDynamic` 的运行时类型来解析 `Foo` 中 `Baz` 的重载。请注意,不需要动态和非动态参数之间的重载解析,因为以下代码是无效的

public class Foo
{
   public void Bar(AnyType baz)
   {
   }
   
   //Won't compile as conflicts with first overload
   public void Bar(dynamic baz)
   {
   }
}

dynamic 的局限性

  • 无法调用扩展方法
  • 无法调用匿名函数(lambda)
  • 由于上述原因,LINQ 支持有限;请在 MSDN 上查看仅 List<T> 的扩展方法;所有这些方法在 dynamic 实例上都不可用

实际示例

我的实际示例(可在本文附带的解决方案中找到)展示了如何使用 dynamic 来改进一个代码库,其中对对象的反射调用操作(方法调用和 get 访问器)被等效的基于 dynamic 的代码替换。

测试框架

我的解决方案有一个非常简单的对象模型。有两个类,Class1Class2,它们实现了以下接口

public interface IBaseClass
{
    string Property { get; set; }
    void Method();
}

在实现的类中,调用 Method 会将“Method Called on ClassName写入控制台,而访问 Property 的 getter 会返回“Property of ClassName”。

在解决方案中,有一个 InstanceCreator 类,它通过调用相关构造函数来返回 IBaseClass 类型的实例化对象。这是为了支持一组使用直接操作 POCO 调用进行测试,这需要已知类型的具体实例。由 InstanceCreator 返回的对象可以被认为来自 COM 或动态源,用于 dynamic 测试(但要注意它们没有实现相应的 IDispatchIDynamicObject 接口)。

最后,有三个测试器类代表三种执行类型。StaticTester 以静态类型 POCO 的方式调用对象,RelectiveTester 模拟在运行时以未知类型仿佛反射创建的对象调用,而 DynamicTester 用更简洁的 dynamic 代码替换了反射代码。请注意,在源代码中,对于反射和 dynamic 示例,实例化可以采用反射方式进行,因为它们是类型无关的。

静态调用

没有意外

public class StaticTester : Tester
{
    void WriteClassDetails(IBaseClass instance)
    {
        instance.Method();
        Console.WriteLine(instance.Property);
    }
}

方法和属性直接调用。请注意,无法通过这种方式调用不存在的属性或方法,因为代码将无法编译,因为 CLR 编译器知道类型。

通过反射调用

现在,让我们假设我们需要通过反射调用操作,因为对象类型直到运行时才知道。由于属性和方法是通过它们的名称(表示为字符串)调用的,因此可以尝试调用一个不存在的操作。已添加代码来防止这种情况(用斜体表示)

public class ReflectiveTester : Tester
{

    static void WritePropertyReflectively(object instance, string propertyName)
    {
        Type type = instance.GetType();
        PropertyInfo propertyInfo = type.GetProperty(propertyName);
        if (propertyInfo == null)
            Console.WriteLine("Property \"{0}\" not found, propertyInfo " + 
                              "is null\r\npropertyInfo.GetValue(...) will result " + 
                              "in a NullReferenceException", propertyName);
        else
            Console.WriteLine(propertyInfo.GetValue(instance, null));
    }

    static void CallMethodReflectively(object instance, string methodName)
    {
        Type type = instance.GetType();
        MethodInfo methodInfo = type.GetMethod(methodName);
        if (methodInfo == null)
            Console.WriteLine("Method \"{0}\" not found, " + 
                              "methodInfo set to null\r\nmethodInfo.Invoke(...) " + 
                              "will result in a NullReferenceException", methodName);
        else
            methodInfo.Invoke(instance, null);
    }

    static void WriteClassDetails(object instance)
    {
        Type type = instance.GetType();
        WritePropertyReflectively(instance, "Property");
        CallMethodReflectively(instance, "Method");
    }
}

请注意,现在代码的行为不像反射代码那样明显,“隐藏”了它。开发者也经常错误地忘记保护以防止“找到”的 PropertyInfoMethodInfonull。在这种情况下进一步执行将导致 NullReferenceException。这可能会误导不明就里的人,因为经常会传递可能引发此错误类型的参数,尽管在本例中这些参数为 null。此外,我的示例稍微简化了防御性代码,通常需要抛出并捕获异常。

使用 dynamic

现在,我们将使用 dynamic,实现与反射代码相同的功能。同样,对象类型直到运行时才知道,但代码非常接近我们的第一个示例。通过调用方法和属性,我们可以清楚地知道意图是要调用什么,就好像它们在运行时就已经知道一样。但是,我们仍然需要进行防御性编程,因为我们不确定所传入 `instance` 上的操作是否存在。幸运的是,我们只需要担心 RuntimeBinderException,这使得代码更简洁。

public class DynamicTester : Tester
{
    void WriteClassDetails(dynamic instance)
    {
        try
        {
           Console.WriteLine(instance.Property);
           instance.Method();
        }
        catch (RuntimeBinderException ex)
        {
            ErrorWriters.WriteRuntimeBinderException(ex);
        }
    }
}

有一点需要说明的是,这并不比反射调用“不面向对象”;它直接等效,但具有简洁的语法和不那么晦涩的异常机制。从这个角度来看,添加 dynamic 是一个巨大的好处,即使在不考虑动态语言支持的情况下也是如此。

结论

Dynamics 是一个强大的新工具,可以简化与动态语言以及 COM 的互操作,并且可以用来替代大量繁琐的反射代码。它们可以用来告知编译器对对象执行操作,而这些操作的检查被推迟到运行时。

最大的危险在于在不恰当的上下文中(例如,在静态类型系统中,或者更糟的是,在正确类型的系统中代替接口/基类)使用 dynamic 对象。

关于解决方案的说明

解决方案提供了此处示例的源代码。它是使用 VS2010 RC1 创建的;当然,您必须拥有 C# 4.0 才能编译。该解决方案是一个简单的控制台应用程序,它运行了三种主要代码变体的测试:调用普通 C 对象的方法、通过反射调用以及使用 dynamics。`Program.cs` 中的一个方法包含多个注释掉的坏代码和无法编译的代码示例,可以取消注释并执行以查看其效果。有关详细信息,请参阅文件。

历史

  • 2010 年 3 月 29 日:创建文章。
  • 2010 年 3 月 31 日:修正了文章标题中的错误。
© . All rights reserved.