Давайте обсудим некоторые особенности использования null с коллекциями и перечислениями.
null в возвращаемых коллекциях и перечислениях
Какой смысл в null для коллекций? Для обозначения отсутствия элементов отлично подходит пустая коллекция. Для ее создания можно:
- создать экземпляр List<T> (или другой коллекции, реализующий нужный интерфейс);
- породить пустой массив с помощью new T[0];
- вызвать метод Enumerable.Empty<T>().
Практически не возможно придумать какой-то физический смысл для null. Поэтому его использование усложнит дальнейшее сопровождение проекта. Необходимо будет постоянно помнить что он значит и в чем отличие от пустой коллекции.
Кроме того, присваивая коллекции null, разработчик только усложняет жизнь себе и всем, кто будет использовать его код (а это важно при разработке библиотек). Предположим есть некий метод, возвращающий IEnumerable<int>:
public IEnumerable<int> GetIntValues() { … }
Если GetIntValues() может вернуть null, то каждый его вызов должен выглядеть примерно так:
IEnumerable<int> values = someInstance.GetIntValues();
if (values != null) {
foreach (int value in values)
Console.WriteLine(value);
}
Теперь посмотрим как выглядит тот же самый код, если метод гарантированно никогда не вернет null:
IEnumerable<int> values = this.GetIntValues();
foreach (int value in values)
Console.WriteLine(value);
Проверка на null стала не нужна. Если метод вернет пустую коллекцию, то в данном случае просто ничего не будет выведено на консоль. Но при необходимости всегда можно проверить коллекцию на наличие элементов:
IEnumerable<int> values = this.GetIntValues();
if (values.Any())
{
foreach (int value in values)
Console.WriteLine(value);
}
else
Console.WriteLine("Нет значений.");
Обратите внимание, что в исходном варианте пришлось бы делать уже 2 проверки: на null и на наличие элементов.
Но как гарантировать то, что метод не вернёт null? Для этого можно использовать постусловие контракта. Например, для GetIntValues() оно будет выглядеть следующим образом:
public IEnumerable<int> GetMyValues()
{
Contract.Ensures(Contract.Result<IEnumerable<int>>() != null);
…
}
Такое решение отлично подходит при создании интерфейса. Тогда заданный контракт будут обязаны соблюдать все классы, реализующий данный интерфейс.
null в полях и свойствах, являющихся коллекциями или перечислениями
Аналогичная ситуация с коллекциями, которые являются полями и свойствами объектов. В большинстве случаев их стоит изначально инициализировать с помощью пустого списка:
internal class MyClass
{
private ICollection<int> _myValues = new List<int>();
...
}
Гарантировать то, что них не попадет null, можно с помощью контрактов или указания readonly для поля:
internal class MyClass
{
private ICollection<int> _myValues = new List<int>();
private readonly ICollection<int> _anotherValues = new List<int>();
[ContractInvariantMethod]
private void ClassInvariant()
{
Contract.Invariant(this._myValues != null);
}
}
В данном случае попытка задать полю _myValue привет к выбросу исключения во время выполнения приложения (разумеется если обработка контрактов включена в проекте). А вот попытка присвоить любое значение в _anotherValues вне конструктора будет пресечена еще на этапе компиляции.