Вывод общего типа не работает с динамическим?

#c# #linq #generics #dynamic #type-inference

#c# #linq #общие сведения #динамический #вывод типа

Вопрос:

Недавно я играл с Massive, микро-ORM, который возвращает коллекции IEnumerable<dynamic> .

Я обнаружил неожиданную проблему, когда попытался запросить одну из этих коллекций с помощью LINQ.

Хотя у компилятора, похоже, нет никаких проблем с обработкой этой строки.Format возвращает строку, даже если один из переданных ему аргументов объявлен как динамический…

 dynamic dynamicString = "d"; // just using a string here for simplicity, same problem occurs with any other type
string explicitString = string.Format("string is {0}", dynamicString); // works without issues
  

… похоже, он не может вывести этот факт в следующем сценарии:

 IEnumerable<string> strings = new[] { "a", "b", "c" };
IEnumerable<dynamic> dynamics = strings;

IEnumerable<string> output = dynamics.Select(d => string.Format("string is {0}", d)); // compiler error on this line
  

Компилятор жалуется:

 "Cannot implicitly convert type 'System.Collections.Generic.IEnumerable<dynamic>' to 'System.Collections.Generic.IEnumerable<string>'. An explicit conversion exists (are you missing a cast?)"
  

Поскольку компилятор должен иметь возможность сделать вывод, что мое лямбда-выражение возвращает строку, я ожидал бы, что он также сделает вывод, что результат выбора должен иметь тип string (а не dynamic).

Это было легко исправить, указав TSource и TResult явно следующим образом:

 IEnumerable<string> output2 = dynamics.Select<dynamic, string>(d => string.Format("string is {0}", d)); // works !!!
  

Или я мог бы присвоить результат IEnumerable<dynamic> …

 IEnumerable<dynamic> output3 = dynamics.Select(d => string.Format("string is {0}", d)); // also works
  

Я также подтвердил, что эта проблема не возникает, когда я заменяю свой IEnumerable<dynamic> на IEnumerable<object>:

 IEnumerable<object> objects = strings;
IEnumerable<string> output4 = objects.Select(o => string.Format("string is {0}", o)); // works
  

И, что интересно, работает даже следующее:

 IEnumerable<string> output5 = dynamics.Select(d => string.Format("string is {0}", (object)d)); // works
IEnumerable<string> output6 = dynamics.Select(d => string.Format("string is {0}", (string)d)); // works
  

Кто-нибудь может объяснить, что здесь происходит? Является ли это известным ограничением компилятора C # или я обнаружил еще одну ошибку?

Комментарии:

1. «еще одна ошибка» — в компиляторе c # были и действительно есть ошибки, но их очень мало и они далеко друг от друга. Цитирование «еще одного» немного преувеличено, не так ли?

2. Я согласен, что в ретроспективе моя формулировка кажется преувеличенной. Я не совсем это имел в виду, но вы правы. Я был явно предвзят, поскольку подсознательно оценивал вероятность наличия другой ошибки, потому что однажды я обнаружил реальную ошибку в компиляторе раньше. (По общему признанию, это была очень незначительная проблема, которая будет исправлена в следующей версии). Я рад, что это не ошибка и что сегодня я многому научился.

Ответ №1:

Вам нужно:

 IEnumerable<string> output = dynamics.Select(d => (string)string.Format(
       "string is {0}", d));
  

Он не может определить тип возвращаемого значения is string , потому dynamic что означает, что он должен предполагать возврат is dynamic , в случае, если существует более подходящая перегрузка string.Format для конкретного поставляемого типа (с другим типом возвращаемого значения). Даже если мы знаем обратное, спецификация для dynamic не согласится с нами; p Добавляя явное приведение обратно string , мы делаем возвращаемый тип понятным.

Лично я не вижу dynamic здесь никакой пользы; вы могли бы также использовать object , тогда это не проблема в первую очередь:

 IEnumerable<string> strings = new[] { "a", "b", "c" };
IEnumerable<object> dynamics = strings;

IEnumerable<string> output = dynamics.Select(d => string.Format(
      "string is {0}", d));
  

(или, действительно, оставьте как IEnumerable<string> ) Я предполагаю, что у вас есть какая-то другая причина для использования dynamic , которая не видна в этом примере.

Комментарии:

1. хммм… но тогда почему он был рад это сделать? строка explicitString = строка. Формат («строка равна {0}», dynamicString);

2. @BirgerH из-за string explicitString = — который считается неявным приведением к строке (вывод целевого типа из присваивания). Следующее также будет скомпилировано (но не запущено): XmlDocument doc = string.Format("string is {0}", dynamicString);

3. Спасибо, Марк! Я неправильно понял этот аспект динамики. (И я все еще не согласен с этим, потому что как он может найти лучшую перегрузку, которая не возвращает строку?)

4. @BirgerH такова природа dynamic ; по определению разрешение перегрузки приостанавливается до времени выполнения, а не во время компиляции. Для информации, думая об этом, я на самом деле предпочитаю d => string.Format("string is {0}", (object)d) , поскольку это приводит к перегрузке во время компиляции и не требует ненужного использования DLR.

5. Еще раз спасибо, Марк. Я действительно кое-что узнал о динамике из вашего объяснения. Теперь я понимаю, почему компилятору даже нежелательно определять возвращаемый тип в этом случае. Изначально я думал, что это должно быть очевидно, потому что string . Формат является статическим, и все перегрузки возвращают строку (но кто сказал, что в будущем не будет других перегрузок?). В моем реальном примере объекты передаются в строку. Формат был «свойствами» для ExpandoObject. Вот почему я хотел использовать dynamic. Теперь я явно присваиваю каждому «свойству» его известный тип, и проблема решена.

Ответ №2:

Я думаю, что проблема не связана с динамикой. У меня часто возникает «сбой ожидания пользователя», когда я надеюсь на это.Выберите <> выведет параметры универсального типа 1.

Вы могли бы решить это так:

  Func<dynamic, string> selector = d => string.Format("string is {0}", d);
 IEnumerable<string> output = dynamics.Select(selector);
  


1 Я постараюсь добавить пример именно таких «удивительно неоднозначных» случаев, когда у меня будет время позже