Опасность, Уил Робинсон!

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

На вопрос «Почему нет?» всегда значительно сложнее дать ответ, поскольку он выворачивает наизнанку причинно-следственную связь в нашей голове; обычно мы спрашиваем, что является причиной того, чтобы что-то произошло, а не что является причиной того, что что-то не произошло. Таким образом, вместо того, чтобы отвечать на этот вопрос напрямую, я хочу перефразировать этот вопрос, в вопрос о некоторой предполагаемой функциональной возможности. (Предупреждения компилятора – это, конечно же, такая же функциональная возможность, как и что угодно другое.) Отвечая на этот вопрос, мы сможем понять, будет ли команда разработчика компилятора тратить свое ценное время на эту возможность или займется чем-то другим. Вот несколько вопросов, которые я задаю себе, размышляя над предложеним «почему вы не добавили это предупреждение?»:

Думал ли кто-нибудь об этом?

Если никто из команды разработчиков компилятора никогда не думал о некоторой ситуации, тогда, очевидно, никакого предупреждения не будет. В качестве примера, меня недавно спросили, почему следующий код:

using(null) statement;

не приводит к предупреждению. Этот код корректен, он эквивалентен следующему:

IDisposable temp = null;
try
{
statement;
}
finally
{
if (temp != null) temp.Dispose();
}

Конечно, этот код абсолютно бессмысленный; все, что он делает, это добавляет блок try/finally, который не добавляет в программу ничего полезного. Почему мы не выдаем предупреждение для этого бессмысленного кода? Ну, основная причина в том (с моей точки зрения), что никто из команды разработчиков компилятора никогда не думал о том, что пользователь будет такое писать, и, таким образом, никогда не думал о проектировании, реализации, тестировании и документировании возможности компилятора, для выдачи предупреждения в этом случае.

Теперь, мы об этом подумали и мы можем сказать, отвечает ли эта ситуация нашим критериям, например, вероятно ли, что пользователь напечатает этот код случайно, предполагая, что он приведет к чему-то разумному? Но я забегаю вперед.

А возможно ли вообще такое предупреждение? Насколько определение подобной ситуации является дорогостоящим?

Иногда люди предлагают добавить предупреждения, которые требуют решения проблемы останова (которая в общем случае неразрешима) или решения проблемы NP-трудной задачи (которая не решаема на практике за разумное время). Предупреждения типа: «выдавать предупреждение, если невозможно вызвать эту перегруженную версию метода» или «выдавать предупреждение, если для некоторых входных параметров этот рекурсивный метод будет выполняться вечно» или «выдавать предупреждение, если этот аргумент может быть равен null» требуют уровня анализа, который на практике либо невозможен или возможен, но лучше выполняется специализированными утилитами, созданными специально для этих целей, такими как Code Contracts. Компилятор должен легко отличать сценарии, в которых нужно выдавать предупреждение, от сценариев, в которых делать этого не нужно.

Тот код, для которого нужно выдать предупреждение, правдоподобный и скорее всего ошибочный? Может ли потенциально «неправильный» код быть разумным в некотором сценарии?

Существует бесконечное количество способов написания совершенно безумного кода; цель предупреждений не в том, чтобы определить все возможные варианты написания безумного кода, а скорее определить код, который возможен, но, скорее всего ошибочный. Мы не собираемся тратить время и усилия на реализации предупреждений о глупых сценариях, которые на практике никогда не возникнут.

Иногда код может казаться вполне возможным и неправильным, но это может быть сделано специально по неочевидным с первого взгляда причинам. Отличным примером подобного случая является наше подавление предупреждений о локальных переменных, в которых производится только запись (write-only locals); чтение из локальной переменной может производиться разработчиком во время отладки и никогда не производиться программой.

Автоматически сгенерированный код также часто выглядит безумно, хотя является корректным и намеренным. Бывает сложно написать генераторы кода, которые генерируют корректный код; написание кодогенераторов, которые изменяют свой результирующий код, при определении, что их код приводит к предупреждениям компилятора, является слишком обременительным. (Например, давайте рассмотрим автоматически сгенерированный код, который вы получаете при создании нового проекта в Visual Studio. Он компилируется без предупреждений!) Однако мы уделяем большее внимание отлавливанию ошибок в коде, написанном человеком, а не в осчастливливании компьютера.

Очевидно, что мы хотим предупреждать только о коде, который с высокой долей вероятности является неправильным. Если мы предупреждаем о корректном коде, тогда мы заставляем пользователей изменять корректный код, вероятно, на некорректный. Предупреждения компилятора должны устранять ошибки, а не приводить к их появлению. Это приводит к следующему замечанию:

Существует ли четкий способ переписывания корректного кода, который приводит к ненужному предупреждению, к корректному коду, который этого предупреждения не содержит?

В идеале, предупреждения должны легко обходиться. Например, следующий код:

bool x = N();
...
if (x == null ) Q();

Приводит к предупреждению, что условие == null никогда не выполняется для значимых non-nullable типов. Очевидно, вы можете легко переписать этот код, чтобы избавиться от этого предупреждения; вы можете избавиться от всего условного выражения, или вы можете изменить переменную на nullable bool, или еще что-то подобное.

Предупреждение, от которого нельзя избавиться путем переписывания кода очень раздражает тех людей, которые осознанно написали внешне неправильный код. Конечно, предупреждение всегда можно отключить с помощью директивы #pragma, но это просто ужасно заставлять кого-то делать подобное.

Приведет ли это предупреждение к появлению ошибок в большом количестве существующего кода?

Многие люди компилируют проекты с включенным флагом «рассматривать предупреждения, как ошибки». Каждый раз, когда мы добавляем предупреждения, которые приводят к появлению предупреждений в существующем коде, мы потенциально ломаем код многих людей. Даже если это предупреждение является полезным, поломка кода множества людей будет играть против добавления этой возможности. Возможно, с такими предупреждениями лучше справятся другие инструменты, что приводит нас к следующему:

Должна ли команда компилятора заниматься этим? Или это предупреждение должно выдаваться другим инструментом?

Microsoft разрабатывает такие инструменты как FxCop, StyleCop и Code Contracts для более тщательного анализа кода, по сравнению с анализом, выполняемым компилятором. Кроме того, существуют и сторонние утилиты, такие как ReSharper. Если предупреждение может быть реализовано в одном из этих инструментов также легко, как и в компиляторе, это означает меньше работы для меня и это просто здорово. Кроме того, поскольку FxCop проверяет откомпилированный код, то предупреждение этого инструмента сможет определить проблемы в языках C# и VB, не меняя каждый из компиляторов.

Найдет ли пользователь эту ошибку с помощью тестирования?

Чем раньше обнаружена ошибка, тем дешевле ее исправить. Существует несколько возможностей поиска ошибки, в порядке экспоненциального увеличения стоимости исправления ошибки:

* При написании кода

* При компиляции кода

* При тестировании кода

* При запуске кода пользователем

Предупреждения о возможных ошибках, которые вы получаете при написании кода или при его компиляции относятся к ошибкам, которые пользователь никогда не увидит. Однако потенциальное предупреждение, указывающее на ошибку, которая будет всегда отловлена при тестировании менее полезно, нежели предупреждение об ошибке, которая может просочиться к пользователю вопреки тестированию.

Например, у меня недавно спрашивали, почему компилятор не выдает предупреждение на эту распространенную опечатку:

private decimal cost;
public decimal Cost { get { return this.Cost; } }

Упс, этот код приведет к переполнению стека (а на компьютерах с оптимизацией хвостовой рекурсии – к бесконечному циклу). В принципе, компилятор легко может определить, что функция получения значения свойства (property getter) просто вызывает саму себя, так почему компилятор не выдает об этом предупреждения? Мы можем сделать все, что необходимо для определения подобного шаблона, но зачем выдавать предупреждение об ошибке, которая и так будет найдена? Как только вы попробуете проверить ваш код, вы сразу же найдете проблему и будет совершенно ясно, как ее исправить. Эта ситуация не похожа на наш предыдущий пример, if (x == null), когда тот факт существования недостижимого кода может быть не замечен в процессе тестирования, особенно, если в большинстве случаев x равен false.

Подведем итоги:

Хотя наличие некоторого предупреждения кажется полезным, мы считаем, что обычно правильным решением является «не добавлять предупреждение». Наиболее вероятной ситуацией добавления множества предупреждений является появление новой возможности языка, которая может быть использована неправильно; мы добавили довольно много предупреждений в C# 4.0 при неправильно использовании ключевого слова dynamic или именованных и необязательных параметров. Предупреждения для новых возможностей не могут привести к поломке уже существующего кода.

Как я уже говорил, улучшать можно до бесконечности; если у вас есть идеи для новых предупреждений – присылайте.

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