C# 4.0 中的“动态”类型是做啥用的?

Posted

技术标签:

【中文标题】C# 4.0 中的“动态”类型是做啥用的?【英文标题】:What is the 'dynamic' type in C# 4.0 used for?C# 4.0 中的“动态”类型是做什么用的? 【发布时间】:2011-02-11 01:09:45 【问题描述】:

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

有什么情况可以拯救世界吗?

【问题讨论】:

***.com/questions/2255982/… 的可能重复项 在使用 COM 或动态类型语言时很有用。例如,如果您使用 lua 或 python 来编写您的语言的脚本,那么只需调用脚本代码就好像它是普通代码一样非常方便。 What is the practical use of "dynamic" variable in C# 4.0?的可能重复 希望这篇文章能完整回答你的问题visualstudiomagazine.com/Articles/2011/02/01/… 【参考方案1】:

添加了 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);

注意到所有这些论点了吗?你需要传递这些,因为在 4.0 版之前的 C# 没有可选参数的概念。在 C# 4.0 中,COM API 变得更易于使用,引入了:

    可选参数 将ref 设为 COM API 可选 命名参数

上述调用的新语法是:

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 方式很多,您需要跳过很多圈才能哄骗编译器为您编译代码。

如果您尝试与没有接口或类的 COM 对象通信,那么在 4.0 版之前的 C# 中情况会更加糟糕,您所拥有的只是一个 IDispatch 引用。

如果你不知道它是什么,IDispatch 基本上是对 COM 对象的反射。使用IDispatch 接口,您可以询问对象“称为Save 的方法的ID 号是多少”,并构建包含参数值的特定类型的数组,最后在@987654339 上调用Invoke 方法@interface 来调用方法,传递你设法收集到的所有信息。

上面的 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 运行时之上的语言。

如果您想了解更多信息,可以在Wikipedia: IDispatch 上找到有关IDispatch 接口的更多信息。这真是血腥的东西。

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

对于 Ruby?仍然是不同的 API。

javascript?同样的交易,不同的 API 也是如此。

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

    C# 中的新关键字dynamic 一组运行时类,它们知道如何处理不同类型的对象,实现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;

并让它编译,它并不是作为一种魔法让我们在运行时弄清楚你的意思是什么类型的系统。

整个目的是为了更容易与其他类型的对象交谈。

互联网上有大量关于关键字、支持者、反对者、讨论、咆哮、赞美等的材料。

我建议你从以下链接开始,然后谷歌了解更多:

DevDays 2010: Anders Hejlsberg - C# 4.0 and beyond Channel 9: Mads Torgersen - Inside C# 4.0: dynamic typing + + DevX: COM Interop Gets Much Better in C# 4.0 Scott Hanselman - C# 4 and the dynamic keyword - Whirlwind Tour around .NET 4 (and Visual Studio 2010) Beta 1

【讨论】:

除了 COM 之外,它对于 Web JSON API 也很有用,其中反序列化 JSON 对象的结构未在 C# 中指定。比如System.Web.Helpers.Json的解码方法returns a dynamic object。 关于“他们仍然认为 C# 是一种强类型语言”的旁白:Eric Lippert 是 not a fan of "strongly typed" 作为描述。 我不同意他的观点,但这是见仁见智,而不是事实。 “强类型”对我来说意味着编译器在编译时知道使用哪种类型,从而强制执行围绕这些类型设置的规则。对我来说,您可以选择延迟规则检查和绑定到运行时的动态类型这一事实并不意味着该语言是弱类型的。我通常不会将强类型与弱类型进行对比,但是,我通常会将其与动态类型进行比较,例如 Python 之类的语言,在这种语言中,一切都是鸭子,直到它吠叫为止。 这个答案有什么意义?其中一半是关于可选参数和 IDispatch 接口的。 这就是为什么添加了dynamic,以支持其他生态系统如何进行类似反射的方法调用,并提供一种具有记录的数据结构的黑盒方法实现这一目标。【参考方案2】:

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

【讨论】:

我个人不喜欢在 c# 中使用 dynamic 来解决可以通过标准 c# 功能和静态类型解决(甚至更好)的问题,或者最多使用类型推断(@ 987654327@)。 dynamic 应该在涉及与 DLR 的互操作性问题时使用。如果你用静态类型语言编写代码,比如 c#,那么就去做,不要模拟动态语言。那只是丑陋。 如果您在代码中大量使用了不需要它们的dynamic 变量(例如在使用平方根的示例中),您将放弃干净的编译时错误检查;相反,您现在可能会遇到运行时错误。 大部分都很好,但有几个小错误。首先,说动态意味着变量的类型可以改变是不正确的。有问题的变量是“动态”类型的(从 C# 语言的角度来看;从 CLR 的角度来看,变量是对象类型)。变量的类型从不改变。变量的的运行时类型可以是与该变量的类型兼容的任何类型。 (或者在引用类型的情况下,它可以为 null。) 关于您的第二点:C# 已经具有“创建一个可以放入任何内容的变量”的特性——您始终可以创建一个对象类型的变量。关于动态的有趣之处在于您在第一段中指出的内容:动态与对象几乎相同,只是语义分析被推迟到运行时,并且语义分析是在表达式的运行时类型上完成的。 (大多数情况下。也有一些例外。) 我对此投了反对票,主要是因为它含蓄地提倡将关键字用于一般用途。它有一个明确的目标(在 Lasses 的回答中完美地描述了),虽然这个答案在技术上是正确的,但它可能会导致开发人员误入歧途。【参考方案3】:

我很惊讶没有人提到multiple dispatch。解决此问题的常用方法是通过Visitor pattern,但这并不总是可行的,因此您最终会收到堆叠的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 没错,但我不会称其为 hack。静态分析很好,但我不会让它阻止我提供一个优雅的解决方案,其中的替代方案是:开闭原则违反(访问者模式)或增加圈复杂度,可怕的 is 堆叠在一起。 那么您可以选择使用 C# 7 进行模式匹配,不是吗? 那么运营商的成本要低得多(避免双重转换),并且您可以获得静态分析;-) 和性能。 @idbrii 请不要更改我的答案。随意发表评论,我会澄清(如果需要),因为我仍然活跃在这个社区中。另外,请不要使用magic;没有魔法这回事。【参考方案4】:

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

【讨论】:

动态所需的对 VM 的更改实际上使动态语言更容易。 @Dykam:VM 没有变化。回到 .NET 2.0,DLR 工作正常。 @Jörg,是的,有变化。 DLR 被部分重写,因为它现在 VM 已内置支持动态解析。 我有点过于乐观了,研究表明变化并没有那么大。【参考方案5】:

使用示例:

您使用了许多具有公共属性“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;
    

【讨论】:

鸭子打字,不错。但是,如果这些是您的类型,您应该为此使用接口。【参考方案6】:

COM 互操作。尤其是未知。它是专门为它设计的。

【讨论】:

【参考方案7】:

对我来说,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;
        

【讨论】:

这样做有缺点吗?我遇到了同样的问题,并认为编写一个类来表示每个查询结果是荒谬的。数据库表可能不会改变,所以我使用类进行插入更新和删除。选择查询过于宽泛,无法为每个查询编写类或翻译 当时,我没有注意到任何不利因素。我可能会,但我被转移到另一个项目。【参考方案8】:
    您可以使用 pythonnet 调用 CPython 等动态语言:

dynamic np = Py.Import("numpy")

    在对泛型应用数字运算符时,可以将泛型转换为dynamic。这提供了类型安全并避免了泛型的限制。这本质上是 *duck 类型:

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

【讨论】:

【参考方案9】:

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
    

【讨论】:

【参考方案10】:

它在运行时进行评估,因此您可以像在 JavaScript 中一样将类型切换为您想要的任何类型。这是合法的:

dynamic i = 12;
i = "text";

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

【讨论】:

我会犹豫说它是“合法的”。它肯定会编译,因此它是“合法代码”,因为编译器现在将编译它,运行时将运行它。但我永远不想在我维护的任何代码中看到那段特定的代码(或类似的代码),否则这将是一种近乎致命的冒犯。 当然,但那将是“合法”的“对象”而不是“动态”。您在这里没有展示任何关于动态的有趣内容。 对于对象,您必须将其转换为适当的类型,才能实际调用其任何方法……您会丢失签名;您可以让您的代码调用任何方法而不会出现编译错误,并且在运行时会出错。急着打字,抱歉没有具体说明。还有@Lasse,我同意,我可能不会过多使用动态。 未解释不得已的用例

以上是关于C# 4.0 中的“动态”类型是做啥用的?的主要内容,如果未能解决你的问题,请参考以下文章

Windows系统中的lib文件是做啥用的?

SetPixelFormat() 中的 PIXELFORMATDESCRIPTOR 参数是做啥用的?

Laravel 中的 `HtmlString` 是做啥用的?

TTF/OTF 头表中的 checkSumAdjustment 是做啥用的?

Visual Studio 中的“stdafx.h”是做啥用的?

Python 中的“__docformat__”是做啥用的?