Немного об использовании значения null

Давайте обсудим некоторые особенности использования 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 вне конструктора будет пресечена еще на этапе компиляции.

Комментарии (5) -

Михаил 27.08.2014 23:37:13

А если сравнивать количество выделяемой памяти в обоих случаях то как с этим предстоят дела?

Если для перечислений использовать Enumerable.Empty<T>() то ничего по сути не изменится. Метод возвращает один и тот же экземпляр new T[0] для каждого типа T. Это даже лучше, чем создавать такой пустой экземпляр в своих классах (хотя и так можно).

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

последовательность (IEnumerable) вообще говоря может быть вычисляемой, так что вызов Enumerable.Any() приведёт к изменению состояния.

@ valker: А как в этой ситуации как вы запретите использовать Count() или Any()? Такие реализации должны быть хорошо документированы, т.е. по сути это частные случаи.

Добавить комментарий