C#4.0中的“动态”类型用于什么?


236

C#4.0引入了一种称为“动态”的新类型。听起来不错,但是程序员会用它做什么呢?

是否有可以节省一天的情况?



在使用COM或动态类型的语言时,它很有用。例如,如果您使用lua或python编写语言脚本,则只需调用脚本代码就如同使用普通代码一样非常方便。
CodesInChaos


我希望本文对您的问题有完整的答案visualstudiomagazine.com/Articles/2011/02/01/…–
开发人员

Answers:


196

dynamic关键字是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。

上面的示例显示了调用方法和属性时动态的工作方式。另一个强大的(可能有危险的)功能是能够为不同类型的数据重用变量。我敢肯定,那里的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键入为双精度,而第3行不编译,因为Math.Sqrt需要双精度。显然,您所需要做的就是强制转换和/或更改变量类型,但是在某些情况下,可以使用动态变量。

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


96
我个人不喜欢使用dynamicin c#解决可以通过标准c#功能和静态类型或者最多使用类型推断(var)解决的问题(甚至更好)。dynamic应该只有当它涉及到与DLR interoperabilty问题被使用。如果您使用静态类型的语言(例如c#是)编写代码,请执行该操作,而不要模拟动态语言。那太丑了。
Philip Daubmeier

40
如果您dynamic在不需要代码的地方大量使用了变量(例如在带有平方根的示例中),则放弃干净的编译时错误检查;相反,您现在遇到了可能的运行时错误。
菲利普·道布迈尔

33
通常很好,但是有一些小错误。首先,说动态意味着变量的类型可以改变是不正确。所讨论的变量的类型为“动态”(从C#语言的角度来看;从CLR的角度来看,变量的类型为object)。变量的类型永远不变。变量的运行时类型可以是与变量类型兼容的任何类型。(或在引用类型的情况下,它可以为null。)
Eric Lippert 2010年

15
关于第二点:C#已经具有“使变量可以放入任何东西”的功能-您始终可以使object类型的变量成为可能。关于动态的有趣之处在于您在第一段中指出的内容:动态与对象几乎相同,只是语义分析推迟到运行时,并且语义分析是在表达式的运行时类型上进行的。(通常。有些例外。)
Eric Lippert 2010年

18
我对此表示反对,主要是因为它隐式地提倡将关键字用于一般用途。它有一个针对性的目的(在Lasses的答案中有完整描述),尽管这个答案在技术上是正确的,但很可能会使开发人员误入歧途。
八位大师

211

dynamic加入关键字,用C#4.0的许多其他新功能一起,以使其更简单交谈代码的生命或来自其他运行时,有不同的API。

举个例子。

如果您有一个像该Word.Application对象这样的COM对象,并且想要打开一个文档,那么执行该操作的方法将带有不少于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);

注意所有这些论点吗?您需要传递这些参数,因为4.0版之前的C#没有可选参数的概念。在C#4.0中,通过引入以下内容,使COM API的使用更加容易:

  1. 可选参数
  2. 使refCOM API可选
  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#编译器现在将注入必要的代码,并在运行时中使用新的类,以执行几乎与以前完全相同的操作,但是语法已对您隐藏了,现在您可以专注于什么,而不是如何。安德斯·海斯伯格(Anders Hejlsberg)喜欢说您必须调用不同的“咒语”,这是对整件事的一种双关语,在这种情况下,您通常必须挥手示意正确的顺序并说出一些魔术词获得某种类型的咒语 与COM对象通信的旧API方式很多,您需要跳很多圈以哄骗编译器为您编译代码。

如果您尝试与没有接口或类的COM对象进行通信,则在4.0版之前,C#发生的故障甚至更多,您所拥有的只是一个IDispatch引用。

如果您不知道它是什么,IDispatch基本上就是COM对象的反映。通过IDispatch接口,您可以询问对象“什么是保存方法的ID号”,并构建包含参数值的某种类型的数组,最后InvokeIDispatch接口上调用方法以调用该方法,并传递所有您设法掌握的信息。

上面的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);

基本上,C#只是在表达能力上赶上VB,但通过使其可扩展而以正确的方式进行,而不仅仅是针对COM。当然,VB.NET或在.NET运行时之上构建的任何其他语言也可以使用此功能。

如果您想阅读更多有关该IDispatch界面的信息,请访问Wikipedia:IDispatch。这真的是血腥的东西。

但是,如果您想与Python对象对话怎么办?有一个与用于COM对象的API不同的API,并且由于Python对象本质上也是动态的,因此您需要借助反射魔术来找到正确的调用方法,它们的参数等,但.NET则不是。反射,是为Python编写的东西,与上面的IDispatch代码非常相似,只是完全不同。

还有露比吗 仍然使用其他API。

JavaScript?相同的交易,不同的API也是如此。

动态关键字由两部分组成:

  1. C#中的新关键字, dynamic
  2. 一组运行时类,它们知道如何处理不同类型的对象,这些实现了dynamic关键字所需的特定API ,并将调用映射到正确的处理方式。该API甚至都有文档记录,因此,如果您有来自运行时的对象(未涵盖),则可以添加它。

dynamic但是,该关键字并不是要替换任何现有的仅.NET代码。当然可以,但是可以这样做,但是并不是因为这个原因而添加的,并且前面是Anders Hejlsberg的C#编程语言的作者一直坚称,他们仍然将C#视为强类型语言,并且不会牺牲这个原则。

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

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

并进行编译,这并不意味着它可以让您在运行时知道什么类型的系统。

整个目的是使与其他类型的对象交谈变得更加容易。

互联网上有很多有关关键字,支持者,反对者,讨论,骂,称赞等的材料。

我建议您从以下链接开始,然后从谷歌获取更多信息:


12
除了COM之外,对于Web JSON API来说,它也很有用,因为在C#中未指定反序列化JSON对象的结构。例如,System.Web.Helpers.Json的Decode方法返回一个动态对象
dumbledad

除了“他们仍然将C#视为强类型语言”外,Eric Lippert 并不喜欢 “强类型”作为描述。
安德鲁·基顿

我不同意他的观点,但这是见解,而不是事实。对我来说,“严格类型化”是指编译器在编译时知道使用哪种类型,从而强制执行围绕这些类型设置的规则。对于我来说,您可以选择延迟规则检查并绑定到运行时的动态类型,这一事实对我而言并不意味着该语言的类型很弱。我通常不将强类型与弱类型进行对比,但是,我通常将其与动态类型进行比较,例如Python之类的语言,在这种情况下,一切都是鸭子。
Lasse V. Karlsen

这个答案有什么意义?其中一半是关于可选参数和IDispatch接口的。
Xam

这就是为什么dynamic要添加它的原因,以支持其他生态系统如何完成类似于反射的方法调用,并为数据结构提供一种黑盒方法,并以书面形式实现此目的。
Lasse V. Karlsen

29

我很惊讶没有人提到多次派遣。解决此问题的通常方法是通过“ 访问者”模式,但这并非总是可能的,因此最终会堆积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);
}

请注意,在第一种情况下ElevationPoint是的子类,MapPoint并且如果未放置 子类中,MapPoint它将永远无法到达。动态情况并非如此,因为将调用最接近的匹配方法。

您可能会从代码中猜到,当我执行从ChartItem对象到其可序列化版本的转换时,该功能非常方便。我不想与访问者一起污染我的代码,也不想ChartItem与无用的序列化特定属性一起污染我的对象。


不知道这个用例。充其量不过是一点点hacky。它将丢弃任何静态分析器。
库格尔

2
@Kugel是真的,但我不会称其为hack。静态分析是好的,但我不会阻止它提供一种优雅的解决方案,其中的替代方法是:违反开放-闭合原理(Visitor模式)或增加的循环复杂性以及is一个令人恐惧的堆叠。
Stelios Adamantidis

4
那么您可以选择使用C#7进行模式匹配,不是吗?
库格尔

2
好吧,那样的话,操作员的成本要便宜得多(避免双重转换),并且您可以获得静态分析(-)和性能。
库格尔

@idbrii,请不要更改我的答案。随时发表评论,我会澄清(如果需要的话),因为我仍然活跃在该社区中。另外,请不要使用magic; 没有魔术。
Stelios Adamantidis

11

它使静态类型语言(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 '04

2
@Dykam:虚拟机没有变化。DLR可以很好地一直工作到.NET 2.0。
约尔格W¯¯米塔格

@Jörg,是的,有一个变化。DLR之所以被部分重写是因为它现在使VM内置了对动态解析的支持。
Dykam'4

我有点太乐观了,研究表明变化并不大。
Dykam'4

4

使用示例:

您消耗了许多具有公共属性'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;
    }

7
鸭子打字,很好。但是,如果您是这些类型,则应为此使用接口。
库格尔


2

RAD和Python受害者将主要使用它来破坏代码质量,IntelliSense和编译时错误检测。


愤世嫉俗的答案,但很容易是真的。我已经看到这样做是为了避免声明结构,以使代码在一切正常的情况下仍然可以正常工作,但是一旦移动了奶酪,它就会以无法预测的方式破坏其堆栈。
AnthonyVO

是的,您会看到具有许多其他语言功能的经典切角。在这里也看到它也就不足为奇了。
Hawkeye4040

1

它在运行时求值,因此您可以像在JavaScript中一样将类型切换为所需的任何类型。这是合法的:

dynamic i = 12;
i = "text";

因此,您可以根据需要更改类型。不得已时使用它;这是有益的,但是我听说在幕后发生了很多有关生成IL的事情,而这可能是以性能为代价的。


7
我会犹豫地说这是“合法”。它肯定会编译,因此从某种意义上说它就是“合法代码”,即编译器现在将对其进行编译,并且运行时将对其运行。但是我永远都不会希望在我维护的任何代码中看到特定的代码段(或类似的代码),否则将是近乎射击的冒犯。
Lasse V. Karlsen 2010年

6
可以,但是那应该是“合法”和“对象”,而不是“动态”。您尚未在此处显示有关动态的任何有趣信息。
埃里克·利珀特

对于对象,您必须将其转换为适当的类型,以便实际调用其任何方法。您可以让您的代码调用任何方法而不会发生编译错误,并且在运行时会出错。急于输入,抱歉未指定。@Lasse,我同意,我可能不会使用太多动态功能。
Brian Mains'4

1
最后的用例没有得到说明
denfromufa

1

对我来说,“动态”类型变量的最佳用例是,最近,当我在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(DataConnection.ConnectionString))
            {
                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;
        }

0
  1. 您可以使用pythonnet调用动态语言,例如CPython:

dynamic np = Py.Import("numpy")

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

T y = x * (dynamic)x,在哪里 typeof(x) is T


0

dynamic键入的另一个用例是遇到协变量或协变量问题的虚拟方法。其中一个例子就是臭名昭著的Clone方法,该方法返回与调用它的对象类型相同的对象。通过动态返回不能完全解决此问题,因为它绕过了静态类型检查,但至少您不需要像使用plain那样一直使用丑陋的强制转换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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.