Почему ExpandoObject ломает код, который в остальном работает просто отлично? - PullRequest
38 голосов
/ 27 сентября 2011

Вот настройка: у меня есть проект с открытым исходным кодом под названием «Massive» (github / robconery / mass), и я использую динамику как способ создания 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 - все работает отлично) - это делает все кода «динамическим выражением», которое вычисляется во время выполнения.

Представляется, что оценка этого кода во время выполнения отличается от оценки во время компиляции... который не имеет смысла.

** РЕДАКТИРОВАТЬ: я должен добавить здесь, что если я жестко кодирую все , чтобы явно использовать SqlConnection и SqlCommand, это работает :) - вот изображениечто я имею в виду:

enter image description here

Ответы [ 7 ]

32 голосов
/ 27 сентября 2011

Когда вы передаете динамику в 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 тоже.

18 голосов
/ 27 сентября 2011

Действует так, как будто вы пытаетесь передать динамику анонимным типам между сборками, что не поддерживается.Передача ExpandoObject поддерживается, хотя.Обходной путь, который я использовал, когда мне нужно пройти через сборки, и я успешно его протестировал, заключается в приведении динамической входной переменной как 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, компилятор обрабатывает свой возвращаемый тип как динамический, который он должен разрешить во время выполнения.

15 голосов
/ 27 сентября 2011

Поскольку вы используете в качестве аргумента CreateCommand() динамический, переменная cmd также является динамической, что означает, что ее тип во время выполнения разрешается в SqlCommand. Напротив, переменная conn не является динамической и компилируется для типа DbConnection.

Как правило, SqlCommand.Connection имеет тип SqlConnection, поэтому переменная conn, имеющая тип DbConnection, является недопустимым значением для Connection. Вы можете исправить это, приведя conn к SqlConnection или сделав переменную conn dynamic.

Причина, по которой раньше он работал нормально, заключалась в том, что cmd на самом деле была переменной DbCommand (даже если она указывала на тот же объект), а свойство DbCommand.Connection имеет тип DbConnection. то есть класс SqlCommand имеет определение new свойства Connection.

Источник аннотированных проблем:

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

5 голосов
/ 27 сентября 2011

Глядя на создаваемое исключение, кажется, что хотя OpenConnection возвращает статический тип (DbConnection), а CreateCommand возвращает статический тип (DbCommand), поскольку параметр, передаваемый в DbConnection, имеет динамический тип, он по существу рассматривает следующий код какдинамический сайт связывания:

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

Из-за этого средство выполнения связывает время и пытается найти максимально возможное связывание, которое могло бы привести соединение к SqlConnection.Несмотря на то, что экземпляр технически является SqlConnection, он статически типизируется как DbConnection, так что это то, из чего пытается сшить связыватель.Поскольку прямого преобразования из DbConnection в SqlConnection нет, оно завершается неудачей.

То, что работает, взято из этого SO ответа, касающегося базового типа исключения, заключается в том, чтобы фактически объявить conn как динамический,вместо использования var, в этом случае средство связывания находит установщик SqlConnection -> 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 дляDbConnection, можно было бы подумать, что связыватель сделал бы лучшую работу по «правильному выполнению» в этом случае, и это может быть ошибкой в ​​реализации динамического связывателя в C #.

3 голосов
/ 27 сентября 2011

Этот вопрос пробудил во мне интерес, и после небольшого переворота в твиттере я подумал, что, возможно, стоит написать мое собственное мнение по этому вопросу. Приняв ответ Фрэнка, вы упомянули в твиттере, что он работает, но не объяснили «странность». Надеюсь, это может объяснить странность и объяснить, почему решения Фрэнка и Александра работают, а также добавить немного деталей к первоначальному ответу Шейна.

Проблема, с которой вы столкнулись, в точности соответствует описанию Шейна. Вы получаете несоответствия типов на основе комбинации вывода типов во время компиляции (частично из-за использования ключевого слова 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, и, следовательно, никакие проверки статического типа времени компиляции не выполняются для a. Во время выполнения это будет B и может использоваться в любой ситуации, которая принимает статический тип dynamic (то есть во всех ситуациях). Если он используется в ситуации, которая не принимает B, то возникает ошибка во время выполнения. Однако если операция включает преобразование из 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`

Второй оператор здесь не является динамическим из-за приведения типов test к string (даже если тип параметра dynamic). Следовательно, компилятор может вывести тип a2 из возвращаемого типа GetADynamic, а 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 не является динамическим.Компилятор определяет его тип как C, используя доступную информацию (тип возврата GetC).Таким образом, c имеет статический тип времени компиляции C, и хотя во время выполнения это будет экземпляр D, все виды использования должны соответствовать его статическому типу C.Следовательно, мы получаем ошибку во время выполнения в третьей строке.Связыватель во время выполнения оценивает a как B, и, следовательно, Test имеет тип D.Однако статический тип c равен C, а не D, поэтому даже если c на самом деле является экземпляром D, его нельзя назначить без первого приведения (приведение к статическому типу * 1095).* to 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.

Это не только объясняет, почему вы получаете ошибку, но и почему предложенные решения работают.Решение Александра устранило проблему, изменив строку var cmd = CreateCommand(ex); на var cmd = CreateCommand((ExpandoObject)ex);.Однако это не связано с передачей dynamic между сборками.Вместо этого он вписывается в ситуацию, описанную выше (и в статье MSDN): явное приведение ex к ExpandoObject означает, что выражение больше не оценивается как dynamic.Следовательно, компилятор может выводить тип cmd на основе возвращаемого типа CreateCommand, а cmd теперь имеет статический тип DbCommand (вместо dynamic).Свойство Connection DbCommand предполагает DbConnection, а не SqlConnection, поэтому conn назначается без ошибок.

Решение Фрэнка работает по той же причине.var cmd = CreateCommand(ex); является динамическим выражением.'DbCommand cmd = CreateCommand (ex); requires a conversion from динамический and consequently falls into the category of expressions involving динамический that are not themselves dynamic. As the static or compile-time type of cmd is now explicitly DbCommand , the assignment to Connection` работает.

Наконец, обращаясь к вашим комментариям к моей gist .Изменение using (var conn = OpenConnection()) на using (dynamic conn = OpenConnection()) работает, потому что conn теперь является дизанмическим.Это означает, что он имеет статический тип или тип времени компиляции dynamic и, таким образом, обходит статическую проверку типов.После назначения в строке cmd.Connection = conn среда выполнения теперь оценивает и cmd, и conn, и их статические типы не вступают в игру (потому что они dynamic).Поскольку они являются экземплярами SqlCommand и SqlConnection соответственно, все это работает.

Что касается выражения «весь блок является динамическим выражением - учитывая, что тогда нет типа времени компиляции»: как вашметод DynamicWeirdness возвращает dynamic, любой код, который вызывает его, приведет к 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() и отладчик выводят тип экземпляра объекта, а не статический тип самой переменной.

3 голосов
/ 27 сентября 2011

Похоже, что оценка этого кода во время выполнения отличается от оценки во время компиляции ... что не имеет смысла.

Вот что происходит.Если какая-либо часть вызова является динамической, весь вызов является динамическим.Передача динамического аргумента методу вызывает динамический вызов всего метода.И это делает тип возвращаемого значения динамическим, и так далее, и так далее.Вот почему это работает, когда вы передаете строку, вы больше не вызываете ее динамически.

Я не знаю точно, почему возникает ошибка, но я предполагаю, что неявные преобразования не обрабатываются автоматически.Я знаю, что есть некоторые другие случаи динамического вызова, которые ведут себя немного иначе, чем обычно, потому что мы сталкиваемся с одним из них, когда выполняем некоторые вещи с динамическим POM (объектная модель страницы) в Orchard CMS.Хотя это крайний пример, Орчард довольно глубоко подключается к динамическому вызову и может просто делать то, для чего он не предназначен.

Что касается «это не имеет смысла» - согласитесь, что это неожиданно, инадеюсь, улучшится в будущих оборотах.Могу поспорить, что у меня над головой есть несколько тонких причин, по которым эксперты по языку могут объяснить, почему он не работает просто автоматически.

Это одна из причин, по которой мне нравится ограничивать динамические части кода.Если вы вызываете что-то, что не является динамическим, с динамическим значением, но вы знаете, какого типа вы ожидаете, это явное приведение, чтобы предотвратить динамический вызов.Вы возвращаетесь в «нормальную землю», проверяете тип компиляции, рефакторинг и т. Д. Просто включите динамическое использование там, где оно вам нужно, и не более того.

2 голосов
/ 27 сентября 2011

Вам не нужно использовать Фабрику для создания команды.Просто используйте conn.CreateCommand ();это будет правильный тип, и соединение уже будет установлено.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...