Динамическое заражение, часть вторая

В прошлый раз мы обсуждали, как параметр «dynamic» стремится расшириться по программе подобно вирусу: если выражение типа dynamic «касается» другого выражения, то последнее зачастую тоже становится типа dynamic. Сегодня я хочу остановиться на менее всего понимаемом аспекте вывода типа метода, который также применяет модель заражения, когда в игру вступает параметр «dynamic».

Постоянные читатели знают, что вывод типа метода является одной из моих излюбленных тем в языке C#; для новых читателей, не знакомых с предметом, позвольте дать краткое введение. Идея в том, что когда вы имеете метод, скажем, Select<A, R>(IEnumerable<A> items, Func<A, R> projection), и вызываете его, например, следующим образом Select(customers, c=>c.Name), мы делаем вывод, что вы имели в виду Select<Customer, string>(customers, c=>c.Name), вместо разбирательства, что же вы хотели сказать. В данном случае мы, прежде всего, предполагаем, что список заказчиков есть IEnumerable<Customer>, и поэтому тип аргумента, соответствующий A есть Customer. Из этого мы выводим, что лямбда-параметр «c» имеет тип Customer, и поэтому результатом лямбда-выражения является строка, следовательно, типом аргумента, соответствующего R является строка. Алгоритм уже завершен, но когда в игру вступает параметр «dynamic» получается что-то совершенно фантастическое.

Проблема заключается в том, что конструкторы языка столкнулись при решении того, как работает вывод типа метода с параметром dynamic, с усложнением нашей основной цели конструирования, о которой я недавно рассказывал: анализ во время исполнения динамических выражений требует всей информации, которую мы вывели при компиляции. Мы используем лишь выведенные при исполнении типы для частей выражений, которые на самом деле динамические; части, которые были статически типизированы при компиляции, остаются статически типизированными и при исполнении, а не динамическими. Выше мы определили R после того, как узнали A, но что если бы «customers» был типа dynamic? Теперь у нас проблема: в зависимости от типа во время исполнения customers, вывод типа может успешно завершиться динамически даже в случае, если, казалось бы, он должен окончиться неудачно при статическом анализе. Но если вывод типа оканчивается неудачей в статическом случае, то он не является кандидатом, и, как осуждалось ранее, если набор кандидатов в группу динамически диспетчеризуемых методов пуст, то разрешения перезагрузки оканчиваются неудачей во время компиляции, а не во время выполнения. Так что, выходит, что вывод типа должен завершиться успехом при статическом анализе!

Какой беспорядок. Как нам выйти из этого затруднения? Спецификация удивительно скупа на детали, она говорит лишь:

Для аргумента любого типа, не зависящего прямо или косвенно от аргумента типа dynamic, предполагается использование [обычных правил статического анализа]. Оставшиеся типы аргументов являются неизвестными. [...] Применимость проверяется согласно [обычным правилам статического анализа] игнорируя параметры, чей тип неизвестен. (*)

Итак, что мы теперь имеем, так это совершенно другой тип, распространяющийся по вирусной модели, тип «unknown» (неизвестный). Так как утверждение «возможно заражен» является транзитивным замыканием отношения экспозиции в упрощенной эпидемиологии, «неизвестный» – транзитивное замыкание отношения «зависит от» в выводе типа метода.

Например, если у нас есть:

void M<T, U>(T t, L<U> items)

с вызовом

M(123, dyn);

то вывод типа заключает, что T является целым (int) из первого аргумента. Так как второй аргумент типа dynamic, и формальный параметр типа включает параметр U, мы «помечаем» U «неизвестным типом».

Когда помеченный параметр типа «зафиксирован» к окончательному типу аргумента, мы игнорируем все остальные ограничения, вычисленные до сих пор, даже если некоторые из них противоречивы, и считаем, что тип будет «неизвестным». Поэтому в данном случае вывод типа будет успешным и сможем добавить к набору кандидатов M<int, unknown>. Как упоминалось ранее, мы опустили проверку применимости для аргументов, которые соответствуют параметрам, чьи типы были так или иначе помечены.

Но каким образом появляется транзитивное замыкание отношения зависимости? В компиляторах C# 4 и 5 мы не управляем этим достаточно уверенно, но в Roslyn мы действительно заставляем метку расширяться. Предположим, у нас есть:

void M<T, U, V>(T t, L<U> items, Func<T, U, V> func)

и вызов

M(123, dyn, (t, u)=>u.Whatever(t));

Мы считаем, что T имеет тип int, а U – неизвестный. Затем мы говорим, что V зависит от T и U, и поэтому выводим, что тип V также неизвестен. Поэтому вывод типа завершается успешно с результатом M<int, unknown, unknown>.

Внимательный читатель здесь может возразить, что не важно, что произойдет с выводом типа метода, все идет к динамическому вызову, и лямбда-выражения, первым делом, не разрешены в динамических выводах. Однако мы бы хотели получить столько высококачественного анализа, сколько это возможно, и чтобы IntelliSense и другие средства анализа кода работали корректно даже в неверном коде. Лучше позволить U заразить V меткой «unknown» и получить успешный результат при выводе типа, чем сперва начать выпутываться и получить отказ в выводе типа. И кроме того, если произойдет чудо, и мы в будущем разрешим лямбда-выражения при динамических вызовах, мы уже будем иметь здравую реализацию вывода типа метода.


(*) Последний пункт слегка неясен по двум причинам. Во-первых, необходимо действительно сказать «чьи типы являются неизвестными при любом способе». L<unknown> рассматривается как неизвестный тип. Во-вторых, наряду с пропуском проверки применимости мы также пропускаем проверку выполнения ограничивающих условий. Что означает, мы предполагаем конструкция времени выполнения L<unknown> обеспечит тип аргумента, удовлетворяющий всем необходимым ограничениям типа общий (generic).