为什么是一个ExpandoObject破坏代码,否则工作正常?

这里有一个设置:我有一个名为“Massive”(github / robconery / massive)的开源项目,我正在dynamic地创buildSQL,dynamic创builddynamic结果集。

要做数据库的事情我使用System.Data.Common和ProviderFactory的东西。 下面是一个很好的示例(它是静态的,所以你可以在控制台中运行):

static DbCommand CreateCommand(string sql) { return DbProviderFactories.GetFactory("System.Data.SqlClient") .CreateCommand(); } static DbConnection OpenConnection() { return DbProviderFactories.GetFactory("System.Data.SqlClient") .CreateConnection(); } public static dynamic DynamicWeirdness() { using (var conn = OpenConnection()) { var cmd = CreateCommand("SELECT * FROM Products"); cmd.Connection = conn; } Console.WriteLine("It worked!"); Console.Read(); return null; } 

运行这个代码的结果是“它工作!”

现在,如果将string参数更改为dynamic – 特别是一个ExpandoObject(假设有一个将Expando分解为SQL的例程),会引发一个奇怪的错误。 代码如下:

动态错误

现在以前的工作失败了,没有任何意义的信息。 一个SqlConnection 一个DbConnection – 另外,如果你把代码移到debugging中,你可以看到这些types都是SQLtypes。 “conn”是一个SqlConnection,“cmd”是一个SqlCommand。

这个错误完全没有意义 – 但更重要的是,这是由于不存在任何实现代码的ExpandoObject的存在。 这两个例程之间的区别是:1 – 我已经改变了CreateCommand()中的参数来接受“dynamic”,而不是string2 – 我已经创build了一个ExpandoObject并设置一个属性。

它变得更加怪异。

如果只是简单地使用string而不是ExpandoObject – 这一切都很好!

  //THIS WORKS static DbCommand CreateCommand(dynamic item) { return DbProviderFactories.GetFactory("System.Data.SqlClient").CreateCommand(); } static DbConnection OpenConnection() { return DbProviderFactories.GetFactory("System.Data.SqlClient").CreateConnection(); } public static dynamic DynamicWeirdness() { dynamic ex = new ExpandoObject(); ex.TableName = "Products"; using (var conn = OpenConnection()) { //use a string instead of the Expando var cmd = CreateCommand("HI THERE"); cmd.Connection = conn; } Console.WriteLine("It worked!"); Console.Read(); return null; } 

如果我将CreateCommand()的参数换成ExpandoObject(“ex”) – 它将导致所有代码成为在运行时评估的“dynamicexpression式”。

看起来,这个代码的运行时评估是不同于编译时评估…这是没有任何意义的。

**编辑:我应该在这里添加,如果我硬编码的一切使用SqlConnection和SqlCommand明确,它的作品:) – 这是我的意思是一个形象:

在这里输入图像说明

当您将dynamic传递给CreateCommand ,编译器将其返回types视为dynamic的,它必须在运行时parsing。 不幸的是,你在这个parsing器和C#语言之间有一些奇怪的地方。 幸运的是,通过删除使用var强制编译器执行所期望的操作很容易:

 public static dynamic DynamicWeirdness() { dynamic ex = new ExpandoObject (); ex.Query = "SELECT * FROM Products"; using (var conn = OpenConnection()) { DbCommand cmd = CreateCommand(ex); // <-- DON'T USE VAR cmd.Connection = conn; } Console.WriteLine("It worked!"); Console.Read(); return null; } 

这已经在Mono 2.10.5上testing过了,但是我确定它也可以和MS一起使用。

这就好像你正在试图在组件中传递dynamic 匿名types一样 ,这是不受支持的。 尽pipe支持传递ExpandoObject 。 我用过的解决方法,当我需要传递程序集,并且已经成功地进行了testing时,将传递给它的dynamic inputvariables作为ExpandoObject进行投射:

 public static dynamic DynamicWeirdness() { dynamic ex = new ExpandoObject(); ex.TableName = "Products"; using (var conn = OpenConnection()) { var cmd = CreateCommand((ExpandoObject)ex); cmd.Connection = conn; } Console.WriteLine("It worked!"); Console.Read(); return null; } 

编辑:正如在评论中指出的,你可以通过组件传递dynamic,你不能通过匿名types跨程序集,而不先传播它们。

上面的解决scheme与Frank Krueger在上面所述的相同。

当您将dynamic传递给CreateCommand时,编译器将其返回types视为dynamic的,它必须在运行时parsing。

因为使用dynamic作为CreateCommand()的参数,所以cmdvariables也是dynamic的,这意味着它的types在运行时被parsing为SqlCommand 。 相比之下, connvariables不是dynamic的,编译为DbConnectiontypes。

基本上, SqlCommand.Connection的types是SqlConnection ,所以types为DbConnectionconnvariables是一个设置Connection的无效值。 您可以通过将conn转换为SqlConnection来修复此问题,或者使connvariables为dynamic

之前工作正常的原因是因为cmd实际上是一个DbCommandvariables(即使它指向同一个对象),而DbCommand.Connection属性的types是DbConnection 。 即SqlCommand类具有Connection属性的new定义。

源代码注释:

  public static dynamic DynamicWeirdness() { dynamic ex = new ExpandoObject(); ex.TableName = "Products"; using (var conn = OpenConnection()) { //'conn' is statically typed to 'DBConnection' var cmd = CreateCommand(ex); //because 'ex' is dynamic 'cmd' is dynamic cmd.Connection = conn; /* 'cmd.Connection = conn' is bound at runtime and the runtime signature of Connection takes a SqlConnection value. You can't assign a statically defined DBConnection to a SqlConnection without cast. */ } Console.WriteLine("It will never get here!"); Console.Read(); return null; } 

固定源的选项(只选1):

  1. 强制将conn声明为SqlConnection: using (var conn = (SqlConnection) OpenConnection())

  2. 使用运行时types的connusing (dynamic conn = OpenConnection())

  3. 不要dynamic绑定CreateCommand: var cmd = CreateCommand((object)ex);

  4. 静态定义cmdDBCommand cmd = CreateCommand(ex);

看看抛出的exception,看起来即使OpenConnection返回一个静态types(DbConnection),并且CreateCommand返回一个静态types(DbCommand),因为传递给DbConnection的参数是dynamictypes的,它本质上将下面的代码视为一个dynamic绑定现场:

  var cmd = CreateCommand(ex); cmd.Connection = conn; 

正因为如此,运行时绑定器正试图find最具体的绑定可能,这将是连接到SqlConnection。 即使这个实例在技术上是一个SqlConnection,但是它的静态types却是DbConnection,所以这就是绑定器试图从中进行投影的东西。 由于没有从DbConnection到SqlConnection的直接转换,因此失败。

什么似乎工作,从这个 SO回答处理底层的exceptiontypes,是实际声明连接为dynamic,而不是使用var,在这种情况下,活页夹findSqlConnection – > SqlConnection设置器,只是工作,就像这样:

 public static dynamic DynamicWeirdness() { dynamic ex = new ExpandoObject(); ex.TableName = "Products"; using (dynamic conn = OpenConnection()) { var cmd = CreateCommand(ex); cmd.Connection = conn; } Console.WriteLine("It worked!"); Console.Read(); return null; } 

话虽如此,鉴于你将CreateCommand的返回types静态地键入到DbConnection,人们会认为在这种情况下binder会更好地“做正确的事情”,这可能是一个错误C#中的dynamic绑定器实现。

看起来,这个代码的运行时评估是不同于编译时评估…这是没有任何意义的。

这是怎么回事 如果调用的任何部分是dynamic的,则整个调用是dynamic的。 将dynamicparameter passing给方法将导致整个方法被dynamic调用。 这使得返回typesdynamic,等等。 这就是为什么当你传递一个string时,你不再dynamic地调用它。

我不知道为什么发生错误,但我猜隐式强制转换不会自动处理。 我知道还有其他一些dynamic调用的情况,其行为与正常情况稍有不同,因为我们在Orchard CMS中执行一些dynamicPOM(页面对象模型)时碰到其中一个。 尽pipe这是一个极端的例子,但Orchard非常深入地进行了dynamic调用,可能只是做一些没有devise的东西。

至于“这是没有道理的” – 同意这是意想不到的,并希望在未来的改进中有所改进。 我敢打赌,我的脑海里有一些微妙的原因,语言专家可以解释为什么它不能自动工作。

这是我喜欢限制代码的dynamic部分的一个原因。 如果您使用dynamic值调用某个不是dynamic的东西,但是您知道期望的是什么types,则明确地将其转换为阻止该调用是dynamic的。 你回到“正常的土地”,编译types检查,重构等。只需在dynamic使用的地方,在你需要的地方,不超过。

这个问题引起了我的兴趣,而且在twitter上反复提出一些意见之后,我认为可能值得自己去处理这个问题。 接受弗兰克的回答之后,你在twitter上提到它的工作,但没有解释“怪异”。 希望这可以解释怪异,以及为什么弗兰克和亚历山大的解决scheme起作用,以及为谢恩最初的答案增加一些细节。

你遇到的问题正如Shane首先描述的那样。 基于编译时types推断(部分归因于var关键字的使用)和运行时typesparsing(由于使用dynamic )的组合,您将得到types不匹配。

首先,编译时types推断:C#是一种静态或强types的语言。 即使dynamic是一个静态types,但是绕过静态types检查( 这里讨论)。 采取以下简单的情况:

 class A {} class B : A {} ... A a = new B(); 

在这种情况下,a的静态或编译时types是A ,即使在运行时实际的对象也是Btypes的。 编译器将确保任何对a使用只符合A提供的类,任何B特定的function都需要明确的转换。 即使在运行时, a仍然被认为是静态的A ,尽pipe实际情况是B

如果我们将初始声明更改为var a = new B(); ,C#编译器现在推断出a的types。 在这种情况下,从信息中可以推断出的最具体的types是atypes。 因此, a具有静态或编译时间types的B ,并且运行时的特定实例也将是Btypes。

根据可用的信息,types推断的目标是最具体的types。 以下面的例子为例:

 static A GetA() { return new B(); } ... var a = GetA(); 

现在,types推断将推断出typesA因为这是编译器在调用点处可用的信息。 因此, a具有静态或编译时types的A ,并且编译器确保a所有用法符合A 再一次,即使在运行时, a也具有静态types的A即使实际实例是Btypes。

其次, dynamic和运行时评估:正如前一篇文章中所述, dynamic仍然是一个静态types,但C#编译器不会对任何具有dynamictypes的语句或expression式执行静态types检查。 例如, dynamic a = GetA(); 有一个静态或编译时间types的dynamic ,因此没有编译时静态types检查执行a 。 在运行时,这将是一个B ,可以在任何接受静态types(即所有情况)的情况下使用。 如果在不接受B的情况下使用,则会引发运行时错误。 但是,如果操作涉及从dynamic到另一种types的转换,则该expression式不是dynamic的。 例如:

 dynamic a = GetA(); var b = a; // whole expression is dynamic var b2 = (B)a; // whole expression is not dynamic, and b2 has static type of B 

这种情况是显而易见的,但是在更复杂的例子中就不那么明显了。

 static A GetADynamic(dynamic item) { return new B(); } ... dynamic test = "Test"; var a = GetADynamic(test); // whole expression is dynamic var a2 = GetADynamic((string)test); // whole expression is not dynamic, and a2 has a static type of `A` 

这里的第二个语句不是dynamic的,因为对string进行了types转换test (即使参数types是dynamic )。 因此,编译器可以从GetADynamic的返回types推断出a2的types,而a2的静态或编译时间types为A

使用这些信息,可以创build您收到错误的简单副本:

 class A { public C Test { get; set; } } class B : A { public new D Test { get; set; } } class C {} class D : C {} ... static A GetA() { return new B(); } static C GetC() { return new D(); } static void DynamicWeirdness() { dynamic a = GetA(); var c = GetC(); a.Test = c; } 

在这个例子中,我们得到相同的运行时exception在行a.Test = c;a具有dynamic的静态types,并且在运行时将是B一个实例。 c不是dynamic的。 编译器使用可用的信息( GetC返回types)推断其types为C 因此, c有一个静态编译时间types的C ,即使在运行时它将是D一个实例,所有的用法必须符合它的静态typesC 因此,我们在第三行发生运行时错误。 运行时绑定程序评估aB ,因此TestDtypes的。 但是, c的静态types是C而不是D ,所以即使c实际上是D一个实例,也不能在没有首先投射(将其静态typesCD )的情况下分配。

转到您的具体代码和问题(最后!!):

 public static dynamic DynamicWeirdness() { dynamic ex = new ExpandoObject(); ex.TableName = "Products"; using (var conn = OpenConnection()) { var cmd = CreateCommand(ex); cmd.Connection = conn; } Console.WriteLine("It worked!"); Console.Read(); return null; } 

ex有静态types的dynamic ,因此涉及它的所有expression式也是dynamic ,因此在编译时绕过静态types检查。 然而,在这行using (var conn = OpenConnection())是没有任何dynamic的,因此所有的input都是在编译时被推断出来的。 因此,即使在运行时它将是SqlConnection一个实例, conn仍然有一个静态的编译时types的DbConnectionconn所有用法都会假设它是一个DbConnection除非它被DbConnection转换为静态types。 var cmd = CreateCommand(ex); 使用ex ,这是dynamic的,因此整个expression式是dynamic的。 这意味着cmd在运行时被评估,并且它的静态types是dynamic 。 运行时然后评估这一行cmd.Connection = conn;cmd被评估为SqlCommand ,因此Connection需要SqlConnection 。 但是, conn的静态types仍然是DbConnection ,所以运行时会抛出一个错误,因为它无法将静态types为DbConnection的对象分配给需要SqlConnection的字段,而无需先将静态types转换为SqlConnection

这不仅解释了为什么会出现错误,还解释了为什么提出的解决scheme能够正常工作。 亚历山大的解决scheme通过更改行var cmd = CreateCommand(ex);解决了问题var cmd = CreateCommand(ex); var cmd = CreateCommand((ExpandoObject)ex); 。 但是,这不是由于在组件上传递dynamic 。 相反,它符合上面描述的情况(在MSDN文章中):将ex明确地转换成ExpandoObject意味着expression式不再被评估为dynamic 。 因此,编译器可以根据CreateCommand的返回types来推断cmd的types,而cmd现在有一个静态types的DbCommand (而不是dynamic )。 DbCommandConnection属性需要一个DbConnection ,而不是SqlConnection ,所以conn被赋值没有错误。

弗兰克的解决scheme的工作原理基本相同。 var cmd = CreateCommand(ex); 是一个dynamic的expression。 'DbCommand cmd = CreateCommand(ex); requires a conversion fromdynamicrequires a conversion from and consequently falls into the category of expressions involvingdynamicand consequently falls into the category of expressions involving that are not themselves dynamic. As the static or compile-time type of that are not themselves dynamic. As the static or compile-time type of cmd that are not themselves dynamic. As the static or compile-time type of is now explicitly DbCommand , the assignment to Connection , the assignment to工作。

最后,在我的要点上处理你的意见。 using (var conn = OpenConnection())改为using (dynamic conn = OpenConnection())是可行的,因为conn现在是dyanmic。 这意味着它有一个静态或编译时间types的dynamic ,从而绕过静态types检查。 在cmd.Connection = conn处赋值时,运行时现在正在计算'cmd'和'conn',并且它们的静态types不会发挥作用(因为它们是dynamic )。 因为它们分别是SqlCommandSqlConnection实例,所以它们都可以工作。

至于语句'整个块是一个dynamicexpression式 – 给定那么没有编译时types':因为你的方法DynamicWeirdness返回dynamic ,任何调用它的代码都将导致dynamic (除非它执行显式的转换,如所讨论的)。 但是,这并不意味着方法中的所有代码都被视为dynamic的 – 只有那些明确涉及dynamictypes的语句才会被视为dynamic的。 如果整个块是dynamic的,那么你可能不会得到任何编译错误,事实并非如此。 例如下面的代码不能编译,说明整个模块不是dynamic的,静态/编译时间types很重要:

 public static dynamic DynamicWeirdness() { dynamic ex = new ExpandoObject(); ex.TableName = "Products"; using (var conn = OpenConnection()) { conn.ThisMethodDoesntExist(); var cmd = CreateCommand(ex); cmd.Connection = conn; } Console.WriteLine("It worked!"); Console.Read(); return null; } 

最后,关于您对这些对象的debugging显示/控制台输出的意见:这并不令人惊讶,并不会与此处的任何内容相抵触。 GetType()和debugging器都输出对象实例的types,而不是variables本身的静态types。

您不需要使用Factory来创build命令。 只要使用conn.CreateCommand(); 这将是正确的types和连接将已经设置。