C# 4.0中的“dynamic”类型有什么用?

278

C# 4.0引入了一个叫做“dynamic”的新类型。这听起来不错,但程序员有什么用处呢?

是否存在它能挽救局面的情况?


4
可能是 https://dev59.com/J3E95IYBdhLWcg3wkeuq 的重复问题。 - Jörg W Mittag
当使用COM或动态类型语言时,这非常有用。例如,如果您使用Lua或Python进行脚本编写,只需调用脚本代码就像调用普通代码一样非常方便。 - CodesInChaos
我希望这篇文章完整地回答了你的问题。https://visualstudiomagazine.com/Articles/2011/02/01/Understanding-the-Dynamic-Keyword-in-C4.aspx?Page=1 - NoWar
10个回答

237
dynamic关键字是在C# 4.0版本中新增的一个特性,同时还加入了许多其他新功能,以使其更容易与来自其他运行时并具有不同API的代码进行交互。
举个例子:
如果您有一个COM对象,例如Word.Application对象,并且想要打开一个文档,则执行此操作的方法至少需要15个参数,其中大多数是可选的。
要调用此方法,您需要类似以下代码(我正在简化,这不是实际代码):
object missing = System.Reflection.Missing.Value;
object fileName = "C:\\test.docx";
object readOnly = true;
wordApplication.Documents.Open(ref fileName, ref missing, ref readOnly,
    ref missing, ref missing, ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing, ref missing, ref missing,
    ref missing, ref missing);

注意所有这些参数?你需要传递它们,因为在C# 4.0之前,没有可选参数的概念。通过引入以下内容,使得与COM API更易于工作:

  1. 可选参数
  2. 对于COM API,将ref变成可选的
  3. 命名参数

上述调用的新语法将是:

wordApplication.Documents.Open(@"C:\Test.docx", ReadOnly: true);

看看它变得多么容易,变得更加可读了吗?

让我们把它分解一下:

                                    named argument, can skip the rest
                                                   |
                                                   v
wordApplication.Documents.Open(@"C:\Test.docx", ReadOnly: true);
                                 ^                         ^
                                 |                         |
                               notice no ref keyword, can pass
                               actual parameter values instead

C#编译器现在的神奇之处在于它会注入必要的代码,并与运行时中的新类一起工作,几乎做与以前相同的事情,但语法已经对您隐藏了,现在您可以专注于what,而不是太过关注how。Anders Hejlsberg喜欢说您必须调用不同的"咒语",这是一种关于整个事情的魔力的双关语,其中您通常需要挥动手并按正确的顺序说出一些魔法话语才能启动某种类型的咒语。与COM对象交互的旧API方式也是如此,您需要跳过很多障碍才能让编译器为您编译代码。

在C# 4.0之前,如果您尝试与没有接口或类的COM对象交互,事情会更加复杂,您只有一个IDispatch引用。

如果您不知道它是什么,IDispatch基本上就是COM对象的反射。使用IDispatch接口,您可以询问对象:"Save方法的ID号码是多少",并构建包含参数值的特定类型的数组,最后调用IDispatch接口上的Invoke方法来调用方法,传递您成功搜集到的所有信息。

上面的Save方法可能看起来像这样(这绝对不是正确的代码):

string[] methodNames = new[] { "Open" };
Guid IID = ...
int methodId = wordApplication.GetIDsOfNames(IID, methodNames, methodNames.Length, lcid, dispid);
SafeArray args = new SafeArray(new[] { fileName, missing, missing, .... });
wordApplication.Invoke(methodId, ... args, ...);

只是为了打开一个文档而已。

很久以前,VB就拥有可选参数和大多数支持这一点的功能,所以这段C#代码:

wordApplication.Documents.Open(@"C:\Test.docx", ReadOnly: true);

在表达能力方面,dynamic 关键字基本上是 C# 赶上 VB 的步伐,但是通过使其可扩展并且不仅局限于 COM 来实现。当然,这也适用于 VB.NET 或任何其他建立在 .NET 运行时之上的语言。

如果您想要了解有关 IDispatch 接口的更多信息,请访问 Wikipedia: IDispatch,以获取更详细的阅读内容。它确实是一些非常复杂的东西。

但是,如果您想与 Python 对象交互怎么办?与用于 COM 对象的 API 不同,这里有一个不同的 API,由于 Python 对象的动态性质,您需要使用反射机制来查找正确的方法调用、参数等,而不是使用 .NET 反射,这与上面的 IDispatch 代码类似,但完全不同。

那么对于 Ruby 呢?还有另一个不同的 API。

JavaScript 呢?同样需要不同的 API。

dynamic 关键字由两部分组成:

  1. C# 中的新关键字,dynamic
  2. 运行时类集合,知道如何处理不同类型的对象,实现特定的 API,dynamic 关键字需要这些类来映射调用方式。该 API 甚至有文档说明,所以如果您有从未涉及的运行时的对象,则可以添加它。

然而,dynamic 关键字并不意味着要取代任何现有的 .NET-only 代码。当然,您可以这样做,但是它并不是出于这个原因而被添加的,C# 编程语言的作者,Anders Hejlsberg 等人最坚定的立场仍是将 C# 视为一种强类型编程语言,并且不会牺牲这个原则。

这意味着尽管您可以编写如下代码:

dynamic x = 10;
dynamic y = 3.14;
dynamic z = "test";
dynamic k = true;
dynamic l = x + y * z - k;

它的编译不是作为一种魔法,也不是在运行时解析出你的意图。

它的目的是使得与其他类型的对象交互更加容易。

网上有很多关于关键字的材料,支持者、反对者、讨论、抱怨、赞美等等。

我建议您从以下链接开始,然后搜索更多:


13
除了在 COM 方面使用外,它还可用于 Web JSON APIs,在这些 API 中,反序列化的 JSON 对象的结构未在 C# 中指定。例如,System.Web.Helpers.Json 的 Decode 方法会 返回一个动态对象 - dumbledad
关于“他们仍然认为C#是一种强类型语言”的一点说明:Eric Lippert并不喜欢“强类型”这个描述词。详情请见此链接 - Andrew Keeton
2
我不同意他的观点,但这只是一个观点问题,而非事实问题。“强类型”对我来说意味着编译器在编译时知道使用的类型,并因此强制执行围绕这些类型设置的规则。你可以选择动态类型,将规则检查和绑定推迟到运行时,但这并不意味着语言是弱类型的。我通常不将强类型与弱类型进行对比,而是将其与动态类型进行比较,例如像Python这样的语言,其中一切都是鸭子,直到它叫起来。 - Lasse V. Karlsen
这个答案的重点是什么?其中一半涉及可选参数和IDispatch接口。 - Xam
这就是为什么添加了 dynamic,以支持其他生态系统中类似反射的方法调用方式,并提供一种黑盒子方法来处理数据结构,并提供实现的文档化方式。 - Lasse V. Karlsen

222

动态关键字是C# 4.0中的新内容,用于告诉编译器一个变量的类型可以改变或者在运行时才能确定。可以把它想象成无需强制转换就能与对象进行交互。

dynamic cust = GetCustomer();
cust.FirstName = "foo"; // works as expected
cust.Process(); // works as expected
cust.MissingMethod(); // No method found!

请注意,我们不需要将cust强制转换为Customer类型,也不需要声明它的类型。因为我们将其声明为动态类型,运行时会接管并搜索并设置FirstName属性。当然,使用动态变量时,您放弃了编译器类型检查。这意味着调用cust.MissingMethod()将编译但直到运行时才失败。此操作的结果是RuntimeBinderException,因为Customer类未定义MissingMethod。

上面的示例展示了在调用方法和属性时dynamic的工作方式。另一个强大(但潜在危险)的功能是能够为不同类型的数据重复使用变量。我相信Python、Ruby和Perl程序员可以想出无数利用这一点的方法,但我使用C#已经很长时间了,所以这种做法对我来说感觉“不对”。

dynamic foo = 123;
foo = "bar";

好的,所以你很可能不会经常编写类似上面的代码。然而,有时变量重用可能会有用,或者可以清理一些脏的旧代码。我经常遇到的一个简单情况是不得不在十进制和双精度之间不断地进行类型转换。

decimal foo = GetDecimalValue();
foo = foo / 2.5; // Does not compile
foo = Math.Sqrt(foo); // Does not compile
string bar = foo.ToString("c");

第二行无法编译,因为2.5被定义为double类型,并且第三行无法编译,因为Math.Sqrt需要一个double类型的参数。显然,你只需要进行强制转换和/或更改变量类型,但有些情况下使用dynamic可能更合适。

dynamic foo = GetDecimalValue(); // still returns a decimal
foo = foo / 2.5; // The runtime takes care of this for us
foo = Math.Sqrt(foo); // Again, the DLR works its magic
string bar = foo.ToString("c");

阅读更多功能:http://www.codeproject.com/KB/cs/CSharp4Features.aspx


117
我个人不喜欢在 C# 中使用 dynamic 来解决可以用标准 C# 特性和静态类型甚至是类型推断(var)来解决的问题,而且即使使用 dynamic 解决问题也可能比这些特性更劣。dynamic 应当仅限于与 DLR 进行互操作时使用。如果你在一个像 C# 这样的静态类型语言中编写代码,那就直接使用静态类型进行编码,而不要模仿动态语言,那样只会让代码变得难看。 - Philip Daubmeier
45
如果您在代码中大量使用 dynamic 变量,而实际上并不需要它们(例如在求平方根的示例中),这将放弃干净的编译时错误检查,而是可能导致运行时错误。 - Philip Daubmeier
36
大部分正确,但有几个小错误。首先,说动态类型意味着变量的类型可以改变是不正确的。所讨论的变量是类型为 "dynamic" 的(从 C# 语言的角度来看;从 CLR 的角度来看,变量的类型是 object)。变量的类型永远不会改变。变量的的运行时类型可以是与变量类型兼容的任何类型(对于引用类型,它可以是 null)。 - Eric Lippert
17
关于你的第二点:C#已经具备“创建可以存放任何内容的变量”的功能 - 你总是可以创建一个类型为object的变量。动态语言最有趣的地方在于你在第一段所指出的:动态类型几乎与object相同,但是语义分析被推迟到运行时,并且语义分析是基于表达式的运行时类型完成的。(大多数情况下是这样,也有一些例外情况。) - Eric Lippert
19
我已经在这个回答上花费了一个踩的点数,主要是因为它隐含地倡导将该关键字用于普通用途。该关键字具有特定的目的(在Lasses的回答中描述得非常完美),虽然此答案从技术上讲是正确的,但很可能会误导开发人员。 - Eight-Bit Guru
显示剩余4条评论

34

我很惊讶没有人提到多重分派。通常解决这个问题的方法是通过访问者模式,但这并不总是可能的,所以你最终会得到堆叠的is检查。

以下是我自己应用的一个真实例子。不要这样做:

public static MapDtoBase CreateDto(ChartItem item)
{
    if (item is ElevationPoint) return CreateDtoImpl((ElevationPoint)item);
    if (item is MapPoint) return CreateDtoImpl((MapPoint)item);
    if (item is MapPolyline) return CreateDtoImpl((MapPolyline)item);
    //other subtypes follow
    throw new ObjectNotFoundException("Counld not find suitable DTO for " + item.GetType());
}

你做:

public static MapDtoBase CreateDto(ChartItem item)
{
    return CreateDtoImpl(item as dynamic);
}

private static MapDtoBase CreateDtoImpl(ChartItem item)
{
    throw new ObjectNotFoundException("Counld not find suitable DTO for " + item.GetType());
}

private static MapDtoBase CreateDtoImpl(MapPoint item)
{
    return new MapPointDto(item);
}

private static MapDtoBase CreateDtoImpl(ElevationPoint item)
{
    return new ElevationDto(item);
}
请注意,在第一种情况下,ElevationPointMapPoint 的子类,如果它不放在 MapPoint 之前,则永远无法到达。对于动态方法调用,情况并非如此,因为会调用最匹配的方法。
从代码中可以猜出,这个特性在我将 ChartItem 对象转换为可序列化版本时非常方便。我不想用访问者来污染我的代码,也不想在我的 ChartItem 对象中添加无用的序列化特定属性。

不知道这种用法。尽管有点hacky,但最好避免使用它,因为它会把任何静态分析器都搞糊涂。 - Kugel
2
@Kugel说得没错,但我不会称它为“hack”。静态分析很好,但我不会让它阻止我寻找优雅的解决方案,而其他选择则是:违反开闭原则(访问者模式)或增加恐怖的is堆叠的圈复杂度。 - Stelios Adamantidis
4
你可以选择在C# 7中使用模式匹配,对吧? - Kugel
2
好的,运算符这样做要便宜得多(避免双重转换),而且您可以获得静态分析和更好的性能。 - Kugel
@idbrii请不要更改我的答案。如果需要,随时留下评论,我会进行澄清,因为我仍然活跃在这个社区中。此外,请不要使用“magic”;不存在所谓的魔法。 - Stelios Adamantidis
显示剩余2条评论

12

这使得静态类型语言(CLR)更容易与在动态语言运行时(DLR)上运行的动态语言(如Python、Ruby…)进行交互,参见MSDN

例如,您可以使用以下代码来在C#中对XML中的计数器进行递增。

Scriptobj.SetProperty("Count", ((int)GetProperty("Count")) + 1);

使用DLR,您可以使用以下代码执行相同的操作。

scriptobj.Count += 1;

MSDN列举了以下优点:

  • 简化将动态语言移植到.NET Framework
  • 在静态类型语言中启用动态特性
  • 提供 DLR 和 .NET Framework 的未来优势
  • 启用库和对象的共享
  • 提供快速的动态分派和调用

更多细节请参见MSDN


1
改变虚拟机以适应动态语言实际上使得动态语言更加容易。 - Dykam
2
@Dykam:虚拟机没有变化。DLR 在 .NET 2.0 版本就已经可以正常工作了。 - Jörg W Mittag
@Jörg,是的,有一个变化。DLR部分被重写了,因为现在VM内置了对动态解析的支持。 - Dykam
我有点过于乐观了,研究表明变化并不是那么大。 - Dykam

7

一个使用示例:

您使用了许多具有共同属性“CreationDate”的类:

public class Contact
{
    // some properties

    public DateTime CreationDate { get; set; }        
}

public class Company
{
    // some properties

    public DateTime CreationDate { get; set; }
    
}

public class Opportunity
{
    // some properties

    public DateTime CreationDate { get; set; }
    
}

如果你想编写一个通用方法来获取“CreationDate”属性的值,你需要使用反射:
static DateTime RetrieveValueOfCreationDate(Object item)
{
    return (DateTime)item.GetType().GetProperty("CreationDate").GetValue(item);
}

使用“动态”概念,您的代码更加优雅:
static DateTime RetrieveValueOfCreationDate(dynamic item)
{
    return item.CreationDate;
}

8
鸭子类型,很好。然而,如果这些是你的类型,请使用接口来实现。 - Kugel

3
我最近使用dynamic类型的变量来编写基于ADO.NET的数据访问层,使用SQLDataReader调用现有的遗留存储过程时,发现它是最理想的应用情况。这些遗留存储过程包含大量业务逻辑,我的数据访问层需要返回某种结构化数据给基于C#的业务逻辑层进行一些操作(尽管几乎没有)。每个存储过程返回不同的数据集(表列)。因此,我编写了下面的代码,看起来相当优雅和简洁,而不是创建数十个类或结构体来保存返回的数据并将其传递给BLL层。
public static dynamic GetSomeData(ParameterDTO dto)
        {
            dynamic result = null;
            string SPName = "a_legacy_stored_procedure";
            using (SqlConnection connection = new SqlConnection("my connection string"))
            {
                SqlCommand command = new SqlCommand(SPName, connection);
                command.CommandType = System.Data.CommandType.StoredProcedure;                
                command.Parameters.Add(new SqlParameter("@empid", dto.EmpID));
                command.Parameters.Add(new SqlParameter("@deptid", dto.DeptID));
                connection.Open();
                using (SqlDataReader reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        dynamic row = new ExpandoObject();
                        row.EmpName = reader["EmpFullName"].ToString();
                        row.DeptName = reader["DeptName"].ToString();
                        row.AnotherColumn = reader["AnotherColumn"].ToString();                        
                        result = row;
                    }
                }
            }
            return result;
        }

这样做有什么不利之处吗?我遇到了同样的问题,认为为每个查询结果编写一个类是荒谬的。数据库表可能不会改变,因此我使用类来进行插入、更新和删除操作。选择查询太广泛了,无法为每个查询编写一个类或翻译。 - Papa Kojo
当时我没有注意到任何不利因素,但我可能已经留意到了,只是我被调到了另一个项目。 - user1451111

3

COM互操作。特别是IUnknown接口。它是专门为此设计的。


1
另一个使用 dynamic 类型的案例是针对具有协变或逆变问题的虚拟方法。其中一个例子是臭名昭著的 Clone 方法,它返回与调用它的对象相同类型的对象。这个问题不能完全通过动态返回解决,因为它绕过了静态类型检查,但至少你不需要像使用普通的 object 时那样经常使用丑陋的转换。换句话说,强制转换变得隐式化了。
public class A
{
    // attributes and constructor here
    public virtual dynamic Clone()
    {
        var clone = new A();
        // Do more cloning stuff here
        return clone;
    }
}

public class B : A
{
    // more attributes and constructor here
    public override dynamic Clone()
    {
        var clone = new B();    
        // Do more cloning stuff here
        return clone;
    }
}    

public class Program
{
    public static void Main()
    {
        A a = new A().Clone();  // No cast needed here
        B b = new B().Clone();  // and here
        // do more stuff with a and b
    }
}

1
  1. 您可以使用pythonnet调用诸如CPython之类的动态语言:

dynamic np = Py.Import("numpy")

  1. 当对泛型应用数值运算符时,您可以将它们转换为dynamic。这提供了类型安全性并避免了泛型的限制。本质上这是*鸭子类型:

T y = x * (dynamic)x,其中typeof(x) is T


0
它在运行时进行评估,因此您可以像在JavaScript中一样随意切换类型。这是合法的:
dynamic i = 12;
i = "text";

因此,您可以根据需要更改类型。将其用作最后的手段;它是有益的,但我听说在生成的IL方面会发生很多事情,这可能会导致性能问题。


7
我不敢说它是“合法的”。它肯定可以编译,所以从这个意义上说,它是“合法的代码”,因为编译器现在可以编译它,而运行时将运行它。但我永远不想在我维护的任何代码中看到那个特定的代码片段(或类似的代码),否则它将接近被解雇的行为。 - Lasse V. Karlsen
7
可以的,但是用“对象”而不是“动态”的话更加“正统”。你没有展示关于“动态”的任何有趣内容。 - Eric Lippert
对于对象,您必须将其转换为适当的类型,以便实际调用其任何方法... 您会失去签名; 您可以让您的代码调用任何方法而不会出现编译错误,并且在运行时出错。我匆忙打字,很抱歉没有说明。@Lasse,我同意,我可能不会经常使用动态。 - Brian Mains
1
最后的备选方案未被解释说明。 - denfromufa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接