Статический анализ оператора «is»

 

Прежде чем переходить к сегодняшнему невероятному приключению, я хотел бы поздравить всё подразделение разработки с потрясающим продуктом, который мы запускаем официально. (Я приложил очень мало усилий к разработке 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. В этом случае предупреждение выдаваться не должно. Можете ли вы придумать пример, демонстрирующий эту возможность?

 

Оригинал статьи