Прежде чем переходить к сегодняшнему невероятному приключению, я хотел бы поздравить всё подразделение разработки с потрясающим продуктом, который мы запускаем официально. (Я приложил очень мало усилий к разработке Visual Studio 2012 и языку C# 5.0, поскольку был очень занят проектом Roslyn). Асинхронные возможностив языках C# и VB являются моими любимыми; новых возможностей очень и очень много, чтобы все их здесь перечислять. Поэтому загляните на страничку запуска, и, пожалуйста, присылайте свои конструктивные пожеланияо том, что вам нравится, и что – нет. Мы не сможем ответить на каждое письмо, но ваше мнение для нас очень важно.
Возвращаемся к теме нашей дискуссии, поднятой в предыдущем сообщении: иногда, с помощью «статического» анализа (т.е. анализа, выполняемого на основе типов выражений времени компиляции, а не на знании более точных типов времени исполнения) компилятор может сказать, каким будет точный результат выполнения оператора «is».
Но прежде чем переходить к этому, давайте напомним смысл оператора «is» в языке C#. Выражение:
x is T
для выражения x и типа T дает результат типа bool. В общем случае, если существует ссылочное преобразование, или преобразование упаковки или распаковкизначения времени исполненияпеременной x к типу T, тогда результат равен true, в противном случае – false. Обратите внимание, что при этом пользовательские преобразования не учитываются. Оператор «is» предназначен для определения того, что значение времени исполнения x на самом деле является типом Т, (*) и, таким образом, нам нужно учесть следующее:
- Т не может быть типом-указателем
- x не может быть лямбда-выражением или анонимным методом
- если x является группой методов (method group) или представляет собой null-литерал (**), тогда результат будет false
- если во время исполнении x представляет собой ссылочный тип и ее значение равно null, то результатом будет false
- если во время исполнения x представляет собой nullable-тип, свойство HasValue которого равно false, то результатом будет false
- если во время исполнения x представляет собой nullable-тип, свойство HasValue которого равно true, тогда результат будет равен вычислению выражения x.Value is T
Теперь, зная об этом, попробуйте придумать ситуации, в которых вы точно знаете, что «x is T» всегда будет true, или false. Вот несколько примеров, результат выполнения которых всегда равен true:
int i = 123;
bool b1 = i is int;
bool b2 = i is IComparable;
bool b3 = i is object;
bool b4 = "hello" is string;
Здесь, в каждом случае мы точно знаем, что, во-первых, операнд не равен null, и, во-вторых, что операнд будет всегда определенного типа, и, таким образом, оператор «is» всегда будет возвращать «true».
Прежде чем продолжить, я хочу сделать еще одно отступление. Я хочу кратко напомнить о наших критериях, когда выдается предупреждение: предупреждение должно быть (1) продуманным, (2) простым в реализации, (3) находить код, который должен, во-первых, встречаться у реальных пользователей, и, во-вторых, наверняка быть ошибочным (но это должно быть неочевидным), (4) должен существовать обходной путь в том случае, если код действительно корректен и (5) не должен приводить к появлению в существующем коде огромного количества ложных ошибок.
Если рассмотреть наши четыре строки, в которых используется оператор «is», то только третья строка мне кажется вероятной и явно ошибочной; пользователь может не обратить внимание, что все целые числа всегда реализуют интерфейс IComparable. Остальные варианты кажутся просто странными. Весьма любопытно, что компилятор языка C# 5 предупреждает в первых трех случаях, но не выдает предупреждение в четвертом.
Существует множество других случаев, когда вы точно знаете, что результатом всегда будет false. Сможете вспомнить несколько других вариантов? Вот несколько вариантов, что пришли мне в голову:
bool b5 = M is Func<object>; // M является группой методов (method group)
bool b6 = null is object;
bool b7 = b5 is IEnumerable;
bool b8 = E.Blah is uint; // E является типом-перечислением (enum type)
bool b9 = i is double;
Первые два примера следуют правилам из спецификации. Для последних трех случаев благодаря статическому анализу мы знаем, что значения не могут быть преобразованы с помощью ссылочного преобразования, или преобразований упаковки или распаковки. Компилятор выдает предупреждения для всех таких простых случаев. (Хотя, конечно же, некоторые из этих примеров – особенно 6-й – очень маловероятны в реальном коде).
Все это было длительной преамбулой к вопросу, который я бы хотел сегодня рассмотреть: «как далеко мы можем зайти» при выполнении подобного статического анализа с целью выдачи предупреждения, что выражение «is» всегда равно false. Мы можем зайти значительно дальше! Я начал эту серию постов с рассмотрения случая, когда преобразование во время компиляции между x и T отсутствует, но «x is T», тем не менее, возвращало true; сегодня же я хочу обсудить вариант, когда преобразование x к T отсутствует, и x is T не можетбыть равно true. Существует множество случаев, когда мы знаем, что определенное выражение точно не может быть некоторого типа, но эти случаи могут быть довольно сложными. Давайте рассмотрим три сложных примера:
class C<T> {}
...
static bool M10<X>(X x) where X : struct { return x is string; }
static bool M11<X>(C<X> cx) where X : struct { return cx is C<string>; }
static bool M12<X>(Dictionary<int, int> d) { return d is List<X>; }
В случае М10 мы знаем, что X – это значимый тип и не существует объекта значимого типа, который может быть преобразован к типу string с помощью ссылочного преобразования, или преобразования упаковки/распаковки. Проверка типа должна возвращать false.
В случае М11 мы знаем, что cx является типом C<некоторый-значимый-тип>, или типом, наследующем от C<некоторый-значимый-тип>. Мы знаем, что не существует варианта, когда один и тот же обобщенный тип будет входить в иерархию наследования дважды; невозможно, чтобы тип наследовал от двух типов: C<некоторый-значимый-тип> и C<string>. Так что проверка типа должна возвращать false.
В случае М12 мы знаем, что не существует способа создать объект, базовым классом которого будет и словарь и список, независимо от типа X. Проверка типа должна возвращать false.
Во всех этих случаях мы могли бы выдавать предупреждения, но мы очень быстро приходим к тому, что это будет противоречить одному из наших принципов: «возможности реализовать функциональность просто и дешево»! Я могу потратить ценное время для нахождения эвристик, которые не будут играть никакого толка для пользователей, которые пишут реальный код. Нам где-то нужно провести границу.
И где она находится? На самом деле, чтобы определить, выдавать предупреждение или нет, когда мы точно знаем, что не существует преобразования во время компиляции xк T, мы используем следующие принципы:
· Если ни х, ни Т не являются открытыми типами времени компиляции (т.е. типами с обобщенными параметрами), тогда результат вычисления выражения будет равен false. Такого преобразования не существует и никакой конкретный тип времени исполнения не сможет этого изменить. Выдаем предупреждение.
· Один из типов является открытым. Если тип времени компиляции переменной х является значимым типом и тип Т является ссылочным типом, тогда мы знаем, что результат всегда будет равен false. (***) (Это наш вариант М10). Выдаем предупреждение.
· В противном случае, мы прекращаем статический анализ и никаких предупреждений не выдаем.
Такое решение явно далеко от идеального, но вполне попадает в категорию «вполне нормально». А «вполне хорошее» решение по определению является вполне хорошим. Конечно же, эти эвристики вполне могут измениться в будущем, если мы найдем убедительные сценарии, которые будут полезны нашим пользователям.
(*) И не дает ответов на другие интересные вопросы, вроде «существует ли возможность связать значение типа Т с этим значением?» или «может ли значение х быть присвоенным переменной типа Т?»
(**) По этому поводу спецификация содержит определенные пробелы, что привело к некоторому рассогласованию поведения компилятора C# 5.0 и Roslyn. В спецификации ничего не говорится о том, что будет, если выражение х будет пространством имен или типом, которые, конечно же, являются корректными выражениями. Компилятор выдает ошибку, в которой говорится, что выражение должно содержать значение. Поведение аналогично для свойств и индексаторов только для чтения. Компилятор C# 5.0 выдает ошибку для выражений, возвращающих void, хотя это и является явным нарушением спецификации; Roslyn выдает предупреждение, хотя в этом случае я бы предпочел исправить спецификацию. В спецификации сказано, что «x is T» должно приводить к ошибке компиляции, если Т является статическим классом; компилятор C# 5.0 ошибочно выдает false, а Roslyn выдает ошибку компиляции.
(***) Это тоже неправда; существует один случай, когда тип времени компиляции выражения х является открытым значимым типом, и Т является значимым типом, и преобразование между х и Т отсутствует, и при этом «x is T» может быть равным true. В этом случае предупреждение выдаваться не должно. Можете ли вы придумать пример, демонстрирующий эту возможность?
Оригинал статьи