为什么是一个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()
的参数,所以cmd
variables也是dynamic的,这意味着它的types在运行时被parsing为SqlCommand
。 相比之下, conn
variables不是dynamic的,编译为DbConnection
types。
基本上, SqlCommand.Connection
的types是SqlConnection
,所以types为DbConnection
的conn
variables是一个设置Connection
的无效值。 您可以通过将conn
转换为SqlConnection
来修复此问题,或者使conn
variables为dynamic
。
之前工作正常的原因是因为cmd
实际上是一个DbCommand
variables(即使它指向同一个对象),而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):
-
强制将
conn
声明为SqlConnection:using (var conn = (SqlConnection) OpenConnection())
-
使用运行时types的
conn
:using (dynamic conn = OpenConnection())
-
不要dynamic绑定CreateCommand:
var cmd = CreateCommand((object)ex);
-
静态定义
cmd
:DBCommand 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
,即使在运行时实际的对象也是B
types的。 编译器将确保任何对a
使用只符合A
提供的类,任何B
特定的function都需要明确的转换。 即使在运行时, a
仍然被认为是静态的A
,尽pipe实际情况是B
如果我们将初始声明更改为var a = new B();
,C#编译器现在推断出a
的types。 在这种情况下,从信息中可以推断出的最具体的types是a
types。 因此, a
具有静态或编译时间types的B
,并且运行时的特定实例也将是B
types。
根据可用的信息,types推断的目标是最具体的types。 以下面的例子为例:
static A GetA() { return new B(); } ... var a = GetA();
现在,types推断将推断出typesA
因为这是编译器在调用点处可用的信息。 因此, a
具有静态或编译时types的A
,并且编译器确保a
所有用法符合A
再一次,即使在运行时, a
也具有静态types的A
即使实际实例是B
types。
其次, dynamic
和运行时评估:正如前一篇文章中所述, dynamic
仍然是一个静态types,但C#编译器不会对任何具有dynamic
types的语句或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
因此,我们在第三行发生运行时错误。 运行时绑定程序评估a
是B
,因此Test
是D
types的。 但是, c
的静态types是C
而不是D
,所以即使c
实际上是D
一个实例,也不能在没有首先投射(将其静态typesC
为D
)的情况下分配。
转到您的具体代码和问题(最后!!):
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的DbConnection
。 conn
所有用法都会假设它是一个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
)。 DbCommand
的Connection
属性需要一个DbConnection
,而不是SqlConnection
,所以conn
被赋值没有错误。
弗兰克的解决scheme的工作原理基本相同。 var cmd = CreateCommand(ex);
是一个dynamic的expression。 'DbCommand cmd = CreateCommand(ex); requires a conversion from
dynamicrequires a conversion from
and consequently falls into the category of expressions involving
dynamicand 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
)。 因为它们分别是SqlCommand
和SqlConnection
实例,所以它们都可以工作。
至于语句'整个块是一个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和连接将已经设置。