Подтвердить что ты не робот

Почему код разрыва ExpandoObject, который в противном случае работает нормально?

Вот настройка: у меня есть проект с открытым исходным кодом под названием 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, это работает :) - вот изображение того, что я имею в виду:

enter image description here

4b9b3361

Ответ 1

Когда вы передаете динамику в CreateCommand, компилятор обрабатывает свой тип возвращаемого значения как динамический, который он должен решить во время выполнения. К сожалению, вы сталкиваетесь с некоторыми странностями между этим резольвером и языком С#. К счастью, легко обойтись, удалив использование 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.

Ответ 2

Он действует так, как будто вы пытаетесь передать динамику анонимных типов в сборках, которая не поддерживается. Однако поддерживается 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;
}

EDIT: Как указано в комментариях, вы можете передавать динамику через сборки, вы НЕ МОЖЕТЕ передавать анонимные типы в сборках без их первого литья.

Вышеприведенное решение действует по той же причине, что и Фрэнк Крюгер.

Когда вы передаете динамическое значение CreateCommand, компилятор обрабатывает его возвращаемый тип как динамический, который он должен решить во время выполнения.

Ответ 3

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

В принципе, SqlCommand.Connection имеет тип SqlConnection, поэтому переменная conn, которая имеет тип DbConnection, является недопустимым значением для установки Connection to. Вы можете исправить это, либо приведя 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):

  • Чтобы статически объявить conn как SqlConnection:   using (var conn = (SqlConnection) OpenConnection())

  • Используйте тип времени выполнения conn:  using (dynamic conn = OpenConnection())

  • Не динамическое связывание CreateCommand:   var cmd = CreateCommand((object)ex);

  • Статически определить cmd:  DBCommand cmd = CreateCommand(ex);

Ответ 4

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

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

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

Что, похоже, работает, взято из this S.O. ответ, связанный с основным типом исключения, заключается в том, чтобы фактически объявить 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, можно было бы подумать, что связующее могло бы улучшить работу "в этом случае", и это вполне может быть ошибка в реализации динамического связующего в С#.

Ответ 5

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

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

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

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

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

Ответ 6

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

Проблема, с которой вы столкнулись, точно так же, как впервые описал Шейн. Вы получаете несоответствия типов на основе комбинации вывода типа времени компиляции (частично из-за использования ключевого слова var) и разрешения типа времени выполнения из-за использования dynamic.

Во-первых, вывод типа компиляции: С# - это статически или строго типизированный язык. Даже dynamic является статическим типом, но который обходит проверку статического типа (как обсуждалось здесь). Возьмите следующую простую ситуацию:

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

В этой ситуации статический или тип времени компиляции a равен a, хотя во время выполнения фактический объект имеет тип B. Компилятор гарантирует, что любое использование a соответствует только тому, что класс a делает доступным, а для любой функции B требуется явное приведение. Даже во время выполнения a по-прежнему считается статическим a, несмотря на то, что фактическим экземпляром является B.

Если мы изменим начальное объявление на var a = new B();, компилятор С# теперь отобразит тип a. В этой ситуации наиболее конкретный тип, который он может вывести из информации, состоит в том, что a имеет тип B. Таким образом, a имеет статический или компиляционный тип B, а конкретный экземпляр во время выполнения также будет иметь тип B.

Тип вывода нацелен на наиболее специфический тип, основанный на доступной информации. Возьмем, например, следующее:

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

Теперь вывод типа выводит a на тип a, поскольку это информация, доступная компилятору на call-сайте. Следовательно, a имеет статический или компиляционный тип a, и компилятор гарантирует, что все использование a соответствует a. Еще раз во время выполнения a имеет статический тип a, хотя фактический экземпляр имеет тип B.

Во-вторых, dynamic и оценка времени выполнения. Как указано в предыдущей статье, с которой я связан, dynamic по-прежнему является статическим типом, но компилятор С# не выполняет никакой проверки статического типа для любого оператора или выражения, которое имеет тип 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, он не может быть назначен без первого литья (литье его статического типа 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.

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

Решение Frank работает по той же причине. 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.

Наконец, обращаясь к вашим комментариям по gist. Изменение using (var conn = OpenConnection()) на using (dynamic conn = OpenConnection()) работает, потому что conn теперь является dyanmic. Это означает, что у него есть статический или компиляционный тип 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(), и отладчик выводит тип экземпляра объекта, а не статический тип самой переменной.

Ответ 7

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