Этот вопрос пробудил во мне интерес, и после небольшого переворота в твиттере я подумал, что, возможно, стоит написать мое собственное мнение по этому вопросу. Приняв ответ Фрэнка, вы упомянули в твиттере, что он работает, но не объяснили «странность». Надеюсь, это может объяснить странность и объяснить, почему решения Фрэнка и Александра работают, а также добавить немного деталей к первоначальному ответу Шейна.
Проблема, с которой вы столкнулись, в точности соответствует описанию Шейна. Вы получаете несоответствия типов на основе комбинации вывода типов во время компиляции (частично из-за использования ключевого слова 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()
и отладчик выводят тип экземпляра объекта, а не статический тип самой переменной.