为什么 ExpandoObject 会破坏原本可以正常工作的代码?

发布于 2024-12-06 09:20:27 字数 2519 浏览 1 评论 0原文

设置如下:我有一个名为 Massive 的开源项目,我正在以动态作为一种方式动态创建 SQL 和动态结果集。

为了完成数据库的工作,我使用 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;
    }

运行此代码的结果是 “它成功了!”

现在,如果我将字符串参数更改为动态 - 特别是ExpandoObject(假设某处有一个例程将 Expando 处理为 SQL) - 会抛出一个奇怪的错误。代码如下:

Dynamic Error

之前的工作现在失败了,并显示一条毫无意义的消息。 SqlConnection DbConnection - 此外,如果将鼠标悬停在调试中的代码上,您可以看到类型都是 SQL 类型。 “conn”是一个SqlConnection,“cmd”是一个SqlCommand。

这个错误完全没有意义 - 但更重要的是,它是由于 ExpandoObject 的存在而引起的,它不触及任何实现代码。两个例程的区别是: 1 - 我已更改 CreateCommand() 中的参数以接受“动态”而不是字符串 2 - 我创建了一个 ExpandoObject 并设置了一个属性。

事情变得更奇怪了。

如果简单地使用字符串而不是 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”) - 它会导致所有代码成为在运行时计算的“动态表达式”。

看来这段代码的运行时评估与编译时评估不同......这是没有意义的。

**编辑:我应该在这里补充一点,如果我对所有内容进行硬编码以显式使用 SqlConnection 和 SqlCommand,它就可以工作:) - 这是我的意思的图像:

在此处输入图像描述

Here's the setup: I have an Open Source project called Massive and I'm slinging around dynamics as a way of creating SQL on the fly, and dynamic result sets on the fly.

To do the database end of things I'm using System.Data.Common and the ProviderFactory stuff. Here's a sample that works just fine (it's static so you can run in a Console):

    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;
    }

The result of running this code is "It worked!"

Now, if I change the string argument to dynamic - specifically an ExpandoObject (pretend that there's a routine somewhere that crunches the Expando into SQL) - a weird error is thrown. Here's the code:

Dynamic Error

What worked before now fails with a message that makes no sense. A SqlConnection is a DbConnection - moreover if you mouseover the code in debug, you can see that the types are all SQL types. "conn" is a SqlConnection, "cmd" is a SqlCommand.

This error makes utterly no sense - but more importantly it's cause by the presence of an ExpandoObject that doesn't touch any of the implementation code. The differences between the two routines are:
1 - I've changed the argument in CreateCommand() to accept "dynamic" instead of string
2 - I've created an ExpandoObject and set a property.

It gets weirder.

If simply use a string instead of the ExpandoObject - it all works just fine!

    //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;
    }

If I swap out the argument for CreateCommand() to be my ExpandoObject ("ex") - it causes all of the code to be a "dynamic expression" which is evaluated at runtime.

It appears that the runtime evaluation of this code is different than compile-time evaluation... which makes no sense.

**EDIT: I should add here that if I hard-code everything to use SqlConnection and SqlCommand explicitly, it works :) - here's an image of what I mean:

enter image description here

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

叫嚣ゝ 2024-12-13 09:20:27

当您将动态传递给 CreateCommand 时,编译器会将其返回类型视为必须在运行时解析的动态。不幸的是,您在该解析器和 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 上进行了测试,但我确信它也适用于 MS。

When you pass the dynamic to CreateCommand, the compiler is treating its return type as a dynamic that it has to resolve at runtime. Unfortunately, you're hitting some oddities between that resolver and the C# language. Fortunately, it's easy to work around by removing your use of var forcing the compiler to do what you expect:

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;
}

This has been tested on Mono 2.10.5, but I'm sure it works with MS too.

昔日梦未散 2024-12-13 09:20:27

它的行为就好像您试图跨程序集传递 dynamics 匿名类型,但这是不受支持的。不过,支持传递 ExpandoObject。当我需要跨程序集传递并且我已经成功测试它时,我使用的解决方法是将 dynamic 输入变量 转换为 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;
}

编辑:
正如评论中所指出的,您可以跨程序集传递动态,但如果不先强制转换它们,就不能跨程序集传递匿名类型。

上述解决方案之所以有效,其原因与弗兰克·克鲁格上述所述相同。

当您将动态传递给 CreateCommand 时,编译器正在处理
它的返回类型是必须在运行时解析的动态类型。

It's acting as if you're trying to pass dynamics anonymous types across assemblies, which is not supported. Passing an ExpandoObject is supported though. The work-around I have used, when I need to pass across assemblies, and I have tested it successfully, is to cast the dynamic input variable as an ExpandoObject when you pass it in:

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;
}

EDIT:
As pointed out in the comments, you CAN pass dynamics across assemblies, you CAN'T pass anonymous types across assemblies without first casting them.

The above solution is valid for the same reason as Frank Krueger states above.

When you pass the dynamic to CreateCommand, the compiler is treating
its return type as a dynamic that it has to resolve at runtime.

云雾 2024-12-13 09:20:27

由于您使用动态作为 CreateCommand() 的参数,因此 cmd 变量也是动态的,这意味着它的类型在运行时解析为 SqlCommand< /代码>。相比之下,conn 变量不是动态的,并且被编译为 DbConnection 类型。

基本上,SqlCommand.Connection 的类型为 SqlConnection,因此 conn 变量(其类型为 DbConnection)是Connection 设置的值无效。您可以通过将 conn 转换为 SqlConnection 或将 conn 变量设置为 dynamic 来解决此问题。

之前工作正常的原因是 cmd 实际上是一个 DbCommand 变量(即使它指向同一个对象),而 DbCommand.Connection > 属性的类型为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. 使用 conn 的运行时类型:
    using (dynamic conn = OpenConnection())

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

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

Because you're using dynamic as the argument to CreateCommand(), the cmd variable is also dynamic, which means its type is resolved at runtime to be SqlCommand. By contrast, the conn variable is not dynamic and is compiled to be of type DbConnection.

Basically, SqlCommand.Connection is of type SqlConnection, so the conn variable, which is of type DbConnection, is an invalid value to set Connection to. You can fix this by either casting conn to an SqlConnection, or making the conn variable dynamic.

The reason it worked fine before was because cmd was actually a DbCommand variable (even so it pointed to the same object), and the DbCommand.Connection property is of type DbConnection. i.e. the SqlCommand class has a new definition of the Connection property.

Source issues annotated:

 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;
}

Options for fixing source (pick only 1):

  1. Cast to statically declare conn as a SqlConnection:
    using (var conn = (SqlConnection) OpenConnection())

  2. Use runtime type of conn:
    using (dynamic conn = OpenConnection())

  3. Don't dynamic bind CreateCommand:
    var cmd = CreateCommand((object)ex);

  4. Statically define cmd:
    DBCommand cmd = CreateCommand(ex);

一袭水袖舞倾城 2024-12-13 09:20:27

查看抛出的异常,似乎即使 OpenConnection 返回静态类型(DbConnection)并且 CreateCommand 返回静态类型(DbCommand),因为传递给 DbConnection 的参数是动态类型,它本质上将以下代码视为动态绑定site:

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

因此,运行时绑定程序会尝试找到可能的最具体的绑定,这会将连接强制转换为 SqlConnection。尽管该实例在技术上是 SqlConnection,但它的静态类型是 DbConnection,因此绑定器尝试从中进行转换。由于没有从 DbConnection 到 SqlConnection 的直接转换,因此它会失败。

似乎有效的方法,取自 this SO 处理底层的答案异常类型,实际上是将 conn 声明为动态的,而不是使用 var,在这种情况下绑定器会找到 SqlConnection -> SqlConnection setter 就可以工作,就像这样:

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 的返回类型静态键入到 DbConnection,人们会认为在这种情况下绑定器会更好地“做正确的事情” ,这很可能是 C# 中动态绑定器实现中的一个错误。

Looking at the exception being thrown, it seems that even though OpenConnection returns a static type (DbConnection) and CreateCommand returns a static type (DbCommand), because the parameter passed to DbConnection is of type dynamic it's essentially treating the following code as a dynamic binding site:

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

Because of this, the runtime-binder is trying to find the most specific binding possible, which would be to cast the connection to SqlConnection. Even though the instance is technically a SqlConnection, it's statically typed as DbConnection, so that's what the binder attempts to cast from. Since there's no direct cast from DbConnection to SqlConnection, it fails.

What seems to work, taken from this S.O. answer dealing with the underlying exception type, is to actually declare conn as dynamic, rather than using var, in which case the binder finds the SqlConnection -> SqlConnection setter and just works, like so:

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;
    }

That being said, given the fact that you statically typed the return type of CreateCommand to DbConnection, one would have thought the binder would have done a better job of "doing the right thing" in this case, and this may well be a bug in the dynamic binder implementation in C#.

又怨 2024-12-13 09:20:27

看来这段代码的运行时评估与
编译时评估...这没有意义。

这就是正在发生的事情。如果调用的任何部分是动态的,则整个调用也是动态的。将动态参数传递给方法会导致动态调用整个方法。这使得返回类型变得动态,等等。这就是为什么当您传递字符串时它会起作用,而不再动态调用它。

我不知道为什么会发生错误,但我猜隐式转换不会自动处理。我知道还有一些动态调用的其他情况,其行为与正常情况略有不同,因为我们在 Orchard CMS 中执行一些动态 POM(页面对象模型)内容时遇到了其中之一。但这是一个极端的例子,Orchard 相当深入地插入动态调用,并且可能只是在做一些它不是设计的事情。

至于“这毫无意义”——同意这是出乎意料的,并希望在未来的转速中得到改进。我敢打赌,我脑子里有一些微妙的原因,语言专家可以解释为什么它不能自动工作。

这就是我喜欢限制代码的动态部分的原因之一。如果您使用动态值调用非动态的内容,但您知道您期望它是什么类型,请显式转换它以防止调用是动态的。您回到“正常状态”,编译类型检查、重构等。只需在需要的地方进行动态使用即可,仅此而已。

It appears that the runtime evaluation of this code is different than
compile-time evaluation... which makes no sense.

That's what's going on. If any part of an invocation is dynamic, the entire invocation is dynamic. Passing a dynamic argument to a method causes the entire method to be invoked dynamically. And that makes the return type dynamic, and so on and so on. That's why it works when you pass a string, you're no longer invoking it dynamically.

I don't know specifically why the error occurs, but I guess implicit casts aren't handled automatically. I know there are some other cases of dynamic invocation that behave slightly differently than normal because we hit one of them when doing some of the dynamic POM (page object model) stuff in Orchard CMS. That's an extreme example though, Orchard plugs pretty deeply into dynamic invocation and may simply be doing things that it wasn't designed for.

As for "it makes no sense" -- agree that it is unexpected, and hopefully improved on in future revs. I bet there some some subtle reasons over my head that the language experts could explain on why it doesn't work just automatically.

This is one reason why I like to limit the dynamic parts of the code. If you're calling something that isn't dynamic with a dynamic value but you know what type you expect it to be, explicitly cast it to prevent the invocation from being dynamic. You get back into 'normal land', compile type checking, refactoring, etc. Just box in the dynamic use where you need it, and no more than that.

善良天后 2024-12-13 09:20:27

这个问题引起了我的兴趣,在推特上反复讨论后,我认为可能值得写下我自己对这个问题的看法。在接受弗兰克的回答后,你在推特上提到它有效,但没有解释“奇怪”。希望这可以解释奇怪之处,并解释为什么弗兰克和亚历山大的解决方案有效,并为肖恩的最初答案添加一些细节。

您遇到的问题与 Shane 首先描述的完全一样。基于编译时类型推断(部分由于使用 var 关键字)和由于使用 dynamic 导致的运行时类型解析的组合,您会遇到类型不匹配的情况>。

首先,编译时类型推断:C# 是一种静态或强类型语言。即使 dynamic 也是一种静态类型,但它绕过静态类型检查(如 此处)。考虑以下简单情况:

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

在这种情况下,a 的静态或编译时类型为 A,即使在运行时实际对象的类型为 B。编译器将确保对 a 的任何使用仅符合类 A 可用的内容,并且任何 B 特定功能都需要显式强制转换。即使在运行时,a 仍被视为静态 A,尽管实际实例是 B

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

类型推断旨在根据可用信息找到最具体的类型。以下例为例:

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

类型推断现在会将 a 推断为 A 类型,因为这是编译器在调用站点可用的信息。因此,a 具有静态或编译时类型 A,并且编译器确保 a 的所有使用都符合 A< /代码>。同样,即使在运行时,a 也具有静态类型 A,即使实际实例的类型为 B

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

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`

这里的第二个语句不是动态的,因为将 test 类型转换为 string(即使参数类型是 dynamic)。因此,编译器可以从 GetADynamic 的返回类型推断出 a2 的类型,并且 a2 具有静态或编译时类型 A

使用此信息,可以创建您收到的错误的简单副本:

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;
}

在此示例中,我们在 a.Test = c; 行得到相同的运行时异常。 a 具有dynamic 的静态类型,并且在运行时将是B 的实例。 c 不是动态的。编译器使用可用信息(GetC 的返回类型)推断其类型为 C。因此,c 具有 C 的静态编译时类型,即使在运行时它将是 D 的实例,所有使用必须符合 C 的静态类型。因此,我们在第三行收到运行时错误。运行时绑定器将 a 计算为 B,因此 Test 的类型为 D。但是,c 的静态类型是 C 而不是 D,因此即使 c 实际上是一个实例D,如果不先进行转换(将其静态类型 C 转换为 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 具有 dynamic 的静态类型,因此涉及它的所有表达式也是 dynamic从而在编译时绕过静态类型检查。但是,这一行 using (var conn = OpenConnection()) 中的任何内容都是动态的,因此所有类型都是在编译时推断的。因此,conn 具有静态编译时类型 DbConnection,即使在运行时它将是 SqlConnection 的实例。 conn 的所有用法都将假定它是 DbConnection,除非它被强制转换以更改其静态类型。 var cmd = CreateCommand(ex); 使用 ex,它是动态的,因此整个表达式是动态的。这意味着 cmd 在运行时评估,其静态类型为 dynamic。然后,运行时会计算此行 cmd.Connection = conn;cmd 被评估为 SqlCommand,因此 Connection 需要 SqlConnection。但是,conn 的静态类型仍然是 DbConnection,因此运行时会抛出错误,因为它无法分配静态类型 DbConnection 的对象到需要 SqlConnection 的字段,而无需先将静态类型转换为 SqlConnection

这不仅解释了为什么会出现错误,还解释了为什么建议的解决方案有效。 Alexander 的解决方案通过将 var cmd = CreateCommand(ex); 行更改为 var cmd = CreateCommand((ExpandoObject)ex); 解决了该问题。但是,这并不是由于跨程序集传递动态所致。相反,它适合上述情况(以及 MSDN 文章中的情况):将 ex 显式转换为 ExpandoObject 意味着表达式不再被计算为 dynamic.因此,编译器能够根据 CreateCommand 的返回类型推断 cmd 的类型,并且 cmd 现在具有静态类型 <代码>DbCommand(而不是动态)。 DbCommandConnection 属性需要一个 DbConnection,而不是 SqlConnection,因此 conn > 已分配且没有错误。

弗兰克的解决方案基本上出于相同的原因而起作用。 var cmd = CreateCommand(ex); 是一个动态表达式。 'DbCommand cmd = CreateCommand(ex);需要从动态进行转换,因此属于涉及动态且本身不是动态的表达式类别。由于cmd的静态或编译时类型现在显式为DbCommand,因此对Connection` 的赋值有效。

最后,回应您​​对我的要点的评论。将 using (var conn = OpenConnection()) 更改为 using (dynamic conn = OpenConnection()) 有效,因为 conn 现在是动态的。这意味着它具有静态或编译时动态类型,从而绕过静态类型检查。在 cmd.Connection = conn 行赋值后,运行时现在正在评估“cmd”和“conn”,并且它们的静态类型不会发挥作用(因为它们是动态< /代码>)。因为它们分别是 SqlCommandSqlConnection 的实例,所以一切正常。

至于语句“整个块是一个动态表达式 - 假设没有编译时类型”:当您的方法 DynamicWeirdness 返回 dynamic 时,调用它的任何代码将导致动态(除非它执行显式转换,如所讨论的)。然而,这并不意味着该方法内的所有代码都被视为动态的——只有那些显式涉及动态类型的语句,如所讨论的。如果整个块是动态的,您可能不会得到任何编译错误,但事实并非如此。例如,以下内容不会编译,这表明整个块不是动态的,静态/编译时类型确实很重要:

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;
}

最后,关于您对对象的调试显示/控制台输出的评论:那不是'这并不奇怪,并且与这里的任何内容都不矛盾。 GetType() 和调试器都输出对象实例的类型,而不是变量本身的静态类型。

This question piqued my interest, and after a bit of back and forth on twitter I thought it might be worth writing my own take on the issue. After accepting Frank's answer, you mentioned on twitter that it worked, but didn't explain the 'weirdness'. Hopefully this can explain the weirdness, and exlpain why Frank's and Alexander's solutions work, as well as adding a bit of detail to Shane's initial answer.

The issue you've encountered is exactly as Shane first described. You are getting type mismatches based on a combination of compile-time type inference (due in part to the use of the var keyword), and runtime type resolution due to use of dynamic.

Firstly, compile-time type inference: C# is a statically or strongly typed language. Even dynamic is a static type, but one that bypasses static type checking (as discussed here). Take the following simple situation:

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

In this situation, the static, or compile-time type of a is A, even though at runtime the actual object is of type B. The compiler will ensure that any use of a conforms only to what class A makes available, and any B specific functionality will require an explicit cast. Even at runtime, a is still considered to be statically A, despite the actual instance being a B.

If we change the initial declaration to var a = new B();, the C# compiler now infers the type of a. In this situation, the most specific type it can infer from the information is that a is of type B. Thus, a has a static or compile-time type of B, and the specific instance at runtime will also be of type B.

Type inference aims for the most specific type based on the information available. Take the following for example:

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

Type inference will now infer a to be of type A as that is the information available to the compiler at the callsite. Consequently, a has a static or compile-time type of A and the compiler ensures that all usage of a conforms to A. Once again, even at runtime, a has a static type of A even though the actual instance is of type B.

Secondly, dynamic and runtime-evaluation: As stated in the previous article I linked to, dynamic is still a static type, but the C# compiler does not perform any static type checking on any statement or expression that has a type dynamic. For instance, dynamic a = GetA(); has a static or compile-time type of dynamic, and consequently no compile-time static type checks are performed on a. At run time, this will be a B, and can be used in any situation that accepts a static type of dynamic (i.e. all situations). If it is used in a situation that doesn't accept a B then a run-time error is thrown. However, if an operation involves a conversion from dynamic to another type, that expression is not dynamic. For instance:

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

This is situation is obvious, but it becomes less so in more complex examples.

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`

The second statement here is not dynamic, due to type-casting test to string (even though the parameter type is dynamic). Consequently, the compiler can infer the type of a2 from the return type of GetADynamic and a2 has a static or compile-time type of A.

Using this information, it's possible to create a trivial replica of the error you were receiving:

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;
}

In this example, we get the same run-time exception at line a.Test = c;. a has the static type of dynamic and at run-time will be an instance of B. c is not dynamic. The compiler infers its type to be C using the information available (return type of GetC). Thus, c has a static compile-time type of C, and even though at run-time it will be an instance of D, all uses have to conform to its static type of C. Consequently, we get a run-time error on the third line. The run-time binder evaluates a to be a B, and consequently Test is of type D. However, the static type of c is C and not D, so even though c is actually an instance of D, it can't be assigned without first casting (casting its static type C to D).

Moving onto your specific code and problem (finally!!):

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 has the static type of dynamic and consequently all expressions involving it are also dynamic and thus bypass static type checking at compile time. However, nothing in this line using (var conn = OpenConnection()) is dynamic, and consequently all typing is inferred at compile-time. Therefore, conn has a static compile-time type of DbConnection even though at runtime it will be an instance of SqlConnection. All usages of conn will assume it is a DbConnection unless it is cast to change its static type. var cmd = CreateCommand(ex); uses ex, which is dynamic, and consequently the whole expression is dynamic. This means that cmd is evaluated at run-time, and its static type is dynamic. The run-time then evaluates this line cmd.Connection = conn;. cmd is evaluated to be a SqlCommand and thus Connection expects SqlConnection. However, the static type of conn is still DbConnection, so the runtime throws an error as it can't assign an object with static type DbConnection to a field requiring SqlConnection without first casting the static type to SqlConnection.

This not only explains why you get the error, but also why the proposed solutions work. Alexander's solution fixed the problem by changing the line var cmd = CreateCommand(ex); to var cmd = CreateCommand((ExpandoObject)ex);. However, this isn't due to passing dynamic across assemblies. Instead, it fits into the situation described above (and in the MSDN article): explicitly casting ex to ExpandoObject means the expression is no longer evaluated as dynamic. Consequently, the compiler is able to infer the type of cmd based on the return type of CreateCommand, and cmd now has a static type of DbCommand (instead of dynamic). The Connection property of DbCommand expects a DbConnection, not a SqlConnection, and so conn is assigned without error.

Frank's solution works for essentially the same reason. var cmd = CreateCommand(ex); is a dynamic expression. 'DbCommand cmd = CreateCommand(ex);requires a conversion fromdynamicand consequently falls into the category of expressions involvingdynamicthat are not themselves dynamic. As the static or compile-time type ofcmdis now explicitlyDbCommand, the assignment toConnection` works.

Finally, addressing your comments on my gist. Changing using (var conn = OpenConnection()) to using (dynamic conn = OpenConnection()) works because conn is now dyanmic. This means it has a static or compile-time type of dynamic and thus bypasses static type checking. Upon assignment at line cmd.Connection = conn the run-time is now evaluating both 'cmd' and 'conn', and their static types are not coming into play (because they are dynamic). Because they are instances of SqlCommand and SqlConnection respectively, it all works.

As for the statement 'the entire block is a dynamic expression - given that then there is no compile time type': as your method DynamicWeirdness returns dynamic, any code that calls it is going to result in dynamic (unless it performs an explicit conversion, as discussed). However, that doesn't mean that all code inside the method is treated as dynamic - only those statements that explicitly involve dynamic types, as discussed. If the entire block was dynamic, you presumably wouldn't be able to get any compile errors, which is not the case. The following, for instance, doesn't compile, demonstrating that the entire block is not dynamic and static / compile-time types do matter:

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;
}

Finally, in regards to your comments about the debug display / console output of the objects: that isn't surprising and doesn't contradict anything here. GetType() and the debugger both output the type of the instance of the object, not the static type of the variable itself.

染年凉城似染瑾 2024-12-13 09:20:27

您不需要使用工厂来创建命令。只需使用 conn.CreateCommand();它将是正确的类型并且连接已经设置。

You don't need to use the Factory to create the command. Just use conn.CreateCommand(); it will be the correct type and the connection will already be set.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文