Если вы используете ReSharper, то вы, наверняка, знакомы с его подсветкой "Possible 'NullReferenceException'". В этой статье я кратко расскажу об анализаторе, который выводит предупреждения такого рода, и о том, как ему помочь делать это лучше.
Сразу рассмотрим пример:
`public string Bar(bool condition)
{
string iAmNullSometimes = condition? «Not null value»: null;
return iAmNullSometimes.ToUpper();
}
* This source code was highlighted with Source Code Highlighter.`
ReSharper справедливо подсветит iAmNullSometimes во второй строке метода с таким предупреждением. Теперь выделим метод:
`public string Bar(bool condition)
{
string iAmNullSometimes = GetNullWhenFalse(condition);
return iAmNullSometimes.ToUpper();
}
public string GetNullWhenFalse(bool condition)
{
return condition? «Not null value»: null;
}
* This source code was highlighted with Source Code Highlighter.`
После этой операции предупреждение пропадает. Почему так происходит?
Анализатор
Анализатор пытается выявить, какие значения могут иметь используемые переменные. Уточню, до какого уровня абстракции сокращено знание о значении переменной. С точки зрения анализатора переменная может иметь одно или несколько состояний:
- NULL, NOT_NULL — обозначает, что ссылка имеет нулевое или ненулевое значение;
- TRUE, FALSE — аналогично для типа bool;
- UNKNOWN — значение, введенное для оптимистичного анализа, с помощью которого снижается количество ложных срабатываний.
В результате работы анализатора для каждой точки использования переменных определяется набор возможных их состояний.
В первом листинге iAmNullSometimes после инициализации будет иметь два возможных состояния: NULL и NOT_NULL. Поэтому подсветка "Possible NullReferenceException" говорит нам, что есть хотя бы один путь выполнения программы, в котором iAmNullSometimes будет иметь значение null (в данном случае путь, в котором condition ложно).
Второй случай сложнее. Анализатор не знает, какие значения возвращает GetNullWhenFalse. Конечно, можно его проанализировать и убедиться, что он может вернуть null. Но при увеличении числа методов, которые тоже что-то вызывают, время, затрачиваемое на такой анализ, не позволяет использовать анализатор "на лету" на современных PC (чтобы ReSharper смог установить подсветки возможных ошибок). Тем более, вызываемый метод может оказаться в библиотеке, на которую ссылается наш проект. Не будем же мы ее так же "на лету" ее декомпилировать и анализировать.
Есть еще один вариант. Предполагать, что внешние методы, о которых ничего не известно, возвращают либо NULL, либо NOT_NULL. Так работает пессимистичный анализ.
В ReSharper по-умолчанию используется оптимистичный анализ. В нем, если о методе ничего не известно, то возвращаемое значение будет в специальном состоянии UNKNOWN. Переменная, оказавшаяся в этом состоянии к моменту ее использования, не подсвечивается (если, конечно, нет других путей на которых null ей был присвоен явно или из CanBeNull-метода). Во втором листинге это и заставляет анализатор "потерять бдительность".
Анализатор и его режимы работы требуют отдельной статьи, поэтому о них я напишу отдельно.
Как в случае оптимистичного, так и пессимистичного анализа все-таки хочется как-то знать, на что способен вызываемый метод, чтобы ReSharper находил больше потенциальных ошибок. Тут нам на помощь приходят контракты.
Контракты
Анализатор ReSharper'a может использовать дополнительные знания о вызываемых методах, получая его через контракты вида "метод никогда не возвращает null", "метод может вернуть null", "в параметр нельзя подставить null". В простейшем случае эти контракты задаются с помощью атрибутов JetBrains.Annotations.CanBeNullAttribute и JetBrains.Annotations.NotNullAttribute. Применение атрибута к методу будет говорить о том, может ли он возвращать null. К параметру — о допустимости подстановки нулевого значения. Также их можно применять к свойствам и полям. Эти атрибуты определены в библиотеке JetBrains.Annotations.dll, которая лежит в <ReSharper install directory>\Bin.
Пример, приведенный во втором листинге, можно улучшить, пометив метод GetNullWhenFalse атрибутом CanBeNull:
`public string Bar(bool condition)
{
string iAmNullSometimes = GetNullWhenFalse(condition);
return iAmNullSometimes.ToUpper();
}
[CanBeNull]
public string GetNullWhenFalse(bool condition)
{
return condition? «Not null value»: null;
}
* This source code was highlighted with Source Code Highlighter.`
При использовании метода переменной iAmNullSometimes в таком случае появляется подсветка "Possible 'NullReferenceException'".
Если вам не хочется в своем проекте тянуть за собой дополнительную сборку, которая к тому же не добавляет функциональности в рантайме, то вы можете объявить эти атрибуты прямо в своем проекте. Анализатору подойдет использование любых атрибутов из любых сборок, лишь бы их имена совпадали с теми, которые указаны в JetBrains.Annotations.dll. Определения этих атрибутов можно легко получить с помощью кнопки Copy default implementation to clipboard, расположенной на одной из страниц настроек ReSharper'a:
External Annotations
Если вам хочется использовать внешнюю библиотеку (например mscorlib.dll), прописывание контрактов для ее сущностей с помощью атрибутов не представляется возможным. Тут на помощь приходят External Annotations. Эта фича ReSharper позволяет дополнять уже скомпилированные сущности атрибутами используемыми анализатором ReSharper'a. External Annotations дают возможность "обмануть" анализатор — сделать так, чтобы он видел у методов, параметров и других объявлений атрибуты, которые не были объявлены при компиляции библиотеки. Для этого атрибуты нужно прописать в XML-файле, расположенном в директории <ReSharper install directory>\Bin\ExternalAnnotations.
Так определены контракты для стандартных библиотек, которые попадают в эту папку при установке ReSharper. Эти контракты были выведены в результате анализа исходных кодов и Microsoft Contracts. Контракты, полученные в результате первого подхода расположены в файлах с именами .Generated.xml, в результате второго — в .Contracts.xml.
Файлы, описывающие дополнительные атрибуты, имеют структуру, похожую на структуру XmlDoc-файлов. Например, для метода XmlReader.Create(Stream input) из сборки System.Xml четвертого фреймворка контракты NotNull задаются так:
`<assembly name=«System.Xml, Version=4.0.0.0»> <!-- В атрибуте name указывается имя сборки, если не указывать версию, то атрибуты из этого файла применятся ко всем версиям сборки с указанным именем -->
<member name=«M:System.Xml.XmlReader.Create(System.IO.Stream)»> <!-- Здесь указано имя члена, атрибуты которого дополняются; используется нотация такая же, как в XmlDoc-файлах -->
<attribute ctor=«M:JetBrains.Annotations.NotNullAttribute.#ctor» /> <!-- Имена конструкторов атрибутов тоже указываются в XmlDoc-нотации -->
<parameter name=«input»>
<attribute ctor=«M:JetBrains.Annotations.NotNullAttribute.#ctor» />
</parameter>
</member>
</assembly>
* This source code was highlighted with Source Code Highlighter.`
Чтобы ReSharper подхватил файл, его нужно разместить одним из следующих способов: <ReSharper install directory>\Bin\ExternalAnnotations\<Assembly name>.xml или <ReSharper install directory>\Bin\ExternalAnnotations\<Assembly name>\<Any name>.xml, где <Assembly name> — имя сборки без указания версии. Если располагать файлы вторым способом, то для одной сборки можно указать несколько наборов контрактов. Это может быть необходимо для различия контрактов сборок с разными версиями.
Сейчас редактирование этих файлов не очень удобно и подразумевает много ручной работы, для которой к тому же необходимы права администратора. Но в скором будущем планируется выход в свет инструмента, упрощающего эту работу. Скорее всего он будет оформлен в виде плагина к ReSharper'у.
Применение
Несколько практик применения External Annotations, улучшающих жизнь при работе с ReSharper'ом.
XmlDocument.SelectNodes(string xpath)
Аннотация CanBeNull для этого метода довольно часто является темой для баг-репортов. Дело в том, что SelectNodes является методом класса XmlNode и в общем случае может вернуть null (например для XmlDeclaration). Но чаще всего мы используем этот метод, когда он никогда не возвращает null, — из XmlDocument. Одним из решений может быть удаление соответствующей аннотации из External Annotations или замена ее на NotNull. Но можно поступить и корректнее, написав extension method для XmlDocument:
`public static class XmlUtil
{
[NotNull]
public static XmlNodeList SelectNodesEx([NotNull] this XmlDocument xmlDocument, [NotNull] string xpath)
{
// ReSharper disable AssignNullToNotNullAttribute
return xmlDocument.SelectNodes(xpath);
// ReSharper restore AssignNullToNotNullAttribute
}
}
* This source code was highlighted with Source Code Highlighter.`
В данном случае, конечно, было бы хорошо еще сделать методы вида SelectElements и SelectAttributes, чтобы избежать преобразования типов каждый раз, но это уже другая история.
Assertion
Если вы используете в своем проекте собственные (или сторонних производителей) методы Assert, то их можно пометить атрибутами AssertionMethodAttribute и AssertionConditionAttribute. Прямые кандидаты на такую пометку — это методы Contracts.Assert, если вы используете Microsoft Contracts:
`<assembly name=«Microsoft.Contracts»>
<member name=«M:System.Diagnostics.Contracts.Contract.Assert(System.Boolean)»>
<attribute ctor=«M:JetBrains.Annotations.AssertionMethodAttribute.#ctor»/>
<parameter name=«condition»>
<attribute ctor=«M:JetBrains.Annotations.AssertionConditionAttribute.#ctor(JetBrains.Annotations.AssertionConditionType)»>
<argument>0</argument>
</attribute>
</parameter>
</member>
<member name=«M:System.Diagnostics.Contracts.Contract.Assert(System.Boolean,System.String)»>
<attribute ctor=«M:JetBrains.Annotations.AssertionMethodAttribute.#ctor»/>
<parameter name=«condition»>
<attribute ctor=«M:JetBrains.Annotations.AssertionConditionAttribute.#ctor(JetBrains.Annotations.AssertionConditionType)»>
<argument>0</argument>
</attribute>
</parameter>
</member>
</assembly>
* This source code was highlighted with Source Code Highlighter.`
А еще можно посмотреть в сторону TerminatesProgramAttribute, если у вас есть методы, которые всегда бросают исключение.