Optional Value. Уменьшение количества null reference-ов.
От: Alexander Polyakov  
Дата: 09.09.10 15:26
Оценка: 25 (3) +1
Довольно часто встречаются задачи, в которых фигурирует некоторое значение, но при некоторых условиях значение отсутствует. Для таких ситуаций предлагается использовать конструкцию OptionalValue, см. код ниже. Код каждого метода очень простой, поэтому лучше посмотреть сам код. Ниже буду описывать использование OptionalValue.

Катастрофа при Extract Method

Рассмотрим строчку кода:
string someMethodResult = new SomeClass1().SomeMethod1();
Сделаем обычный Extract Method:
string someMethodResult = ExtractedMethod1().SomeMethod1();

SomeClass1 ExtractedMethod1()
{
    return new SomeClass1();
}

Теперь заменим вызов конструктора "new SomeClass1()" на null.
В первом варианте получаем ошибку компиляции:
string someMethodResult = null.SomeMethod1(); //ошибка компиляции
Во втором варианте получаем ошибку в run time-е:
string someMethodResult = ExtractedMethod1().SomeMethod1(); //ошибка в run time-е

SomeClass1 ExtractedMethod1()
{
    return null;
}

Хочется иметь такую технику кодирования, чтобы Extract Method не переводил ошибки компиляции в ошибки run time-а.

Отсюда вывод: не использовать null в качестве возвращаемого значения! То есть набирать на клавиатуре null только:
  1. для передачи значений в уже имеющиеся методы (например, третий аргумент в методе PropertyInfo.SetValue),
  2. в операторе сравнения (фактически вариант а),
  3. может что-то забыл .
Но не для возврата значения.

Все null-ы мы таким образом не истребим, поскольку
  1. field-ы классов инициализируются null-ами,
  2. оператор "as" возвращает null,
  3. уже имеющиеся библиотеки поставляют null-ы.
Но наша цель более скромная – уменьшить количество null-ов. Кстати, по пунктам a и b еще можно кое-что отвоевать.

Как будет выглядеть приведенный выше пример в случае использования OptionalValue?
Делаем замену вызова конструктора "new SomeClass1()" на Nothing. Добиваемся, чтобы код компилировался, в итоге получаем первый вариант:
string someMethodResult = OptionalValue.Nothing<SomeClass1>().Process(
    value => value.SomeMethod1(),
    () => "[Экземпляр класса SomeClass1 не существует]");
второй вариант:
string someMethodResult = ExtractedMethod1().Process(
    value => value.SomeMethod1(),
    () => "[Экземпляр класса SomeClass1 не существует]");

IOptionalValue<SomeClass1> ExtractedMethod1()
{
    return OptionalValue.Nothing<SomeClass1>();
}
Таким образом, оба варианта ведут себя одинаково -- если код компилируется, то он работает.

Схожесть с System.Nullable

Да, OptionalValue очень похож на System.Nullable. Отличия:
  1. OptionalValue работает и для reference и для value типов, System.Nullable только для value типов,
  2. у OptionalValue generic параметр ковариантный.

The Maybe Monad

Да, OptionalValue является Maybe монадой. Но использование двух перегруженных методов ProcessValue часто оказывается удобнее использования разноименных монадных методов SelectMany и Select.

Query comprehension syntax

Методы SelectMany и Select позволяют использовать query comprehension syntax для OptionalValue.

Интересные ссылки на эту тему

Изобретатель null reference называет свое изобретение ошибкой на миллион долларов:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time ...
http://en.wikipedia.org/wiki/C._A._R._Hoare


Общественность уже понимает необходимость разделения nullable типов и не nullable типов. Но пока в майнстримовых языка/платформах это не реализовано.

A more elegant solution that will never come true is to apply value type syntax for reference types: all references are implicitly not nulls for the compiler, but a question mark after a type (ie: «string?» or «Customer?») allows you to set null to the variable.
http://codevanced.net/post/One-Annotations-Way-Resharper.aspx

1.0 Non-Null Types Many errors in modern programs manifest themselves as null-dereference errors, suggesting the importance of a programming language providing the ability to discriminate between expressions that may evaluate to null and those that are sure not to (for some experimental evidence, see [24, 22]). In fact, we would like to eradicate all null dereference errors.
http://stackoverflow.com/questions/1943465/avoiding-null-reference-exceptions


В Википеии это описано под термином Option type.

Source code of OptionalValue

Методы GetValue/HasValue можно поменять местами с методом интерфеса IOptionalValue.Process, т.е. GetValue/HasValue будут в интерфейсе, а Process будет extension методом, который будет выражаться через GetValue/HasValue. В этом случае GetValue можно сделать свойством Value. Я этого не делаю, вот по какой причине. Не хочется иметь в базовом интерфейсе свойство, кидающее исключение. Метод GetValue желательно дергать как можно реже.

    public interface IOptionalValue<out TValue>
    {
        TResult Process<TResult>(Func<TValue, TResult> existFunc, Func<TResult> notExistFunc);
    }

    public static class OptionalValue
    {
        public static IOptionalValue<TValue> Nothing<TValue>()
        {
            return NotExistOptionalValue<TValue>.Instance;
        }

        public static IOptionalValue<TValue> AsOptionalValue<TValue>(
            this TValue value)
        {
            return new ExistOptionalValue<TValue>(value);
        }

        public static IOptionalValue<TTarget> ProcessValue<TValue, TTarget>(
            this IOptionalValue<TValue> optionalValue,
            Func<TValue, TTarget> func)
        {
            return optionalValue.ProcessValue(value => func(value).AsOptionalValue());
        }

        public static IOptionalValue<TTarget> ProcessValue<TValue, TTarget>(
            this IOptionalValue<TValue> optionalValue,
            Func<TValue, IOptionalValue<TTarget>> func)
        {
            return optionalValue.Process(func, Nothing<TTarget>);
        }

        public static void Process<TValue>(
            this IOptionalValue<TValue> optionalValue, 
            Action<TValue> existAction, 
            Action notExistAction)
        {
            optionalValue.Process(existAction.ToFunc(), notExistAction.ToFunc());
        }

        public static void ProcessValue<TValue>(
            this IOptionalValue<TValue> optionalValue, 
            Action<TValue> existAction)
        {
            Process(optionalValue, existAction, () => { });
        }

        public static IOptionalValue<TTarget> Select<TValue, TTarget>(
            this IOptionalValue<TValue> optionalValue,
            Func<TValue, TTarget> func)
        {
            return optionalValue.ProcessValue(func);
        }

        public static IOptionalValue<TTarget> SelectMany<TValue, TTarget>(
            this IOptionalValue<TValue> optionalValue,
            Func<TValue, IOptionalValue<TTarget>> func)
        {
            return optionalValue.ProcessValue(func);
        }

        public static IOptionalValue<T2> SelectMany<TValue, T1, T2>(
            this IOptionalValue<TValue> optionalValue,
            Func<TValue, IOptionalValue<T1>> func1,
            Func<TValue, T1, T2> func2)
        {
            return optionalValue.SelectMany(
                value => func1(value).Select(
                    value1 => func2(value, value1)
                         )
                );
        }

        public static bool HasValue<TValue>(
            this IOptionalValue<TValue> optionalValue)
        {
            return optionalValue.Process(delegate { return true; }, () => false);
        }

        public static TValue GetValue<TValue>(
            this IOptionalValue<TValue> optionalValue)
        {
            return optionalValue.Process(
                value => value,
                () =>
                    {
                        throw new InvalidOperationException(
                            string.Format("Optional value of '{0}' type has no value.", typeof (TValue)));
                    }
                );
        }

        public static TValue GetValueOrDefault<TValue>(
            this IOptionalValue<TValue> optionalValue)
        {
            return optionalValue.Process(value => value, () => default(TValue));
        }

        public static IOptionalValue<TValue> ToOptionalValue<TValue>(
            this TValue value) where TValue : class
        {
            return value == null ? Nothing<TValue>() : value.AsOptionalValue();
        }

        private class NotExistOptionalValue<TValue> : IOptionalValue<TValue>
        {
            public static readonly IOptionalValue<TValue> Instance = new NotExistOptionalValue<TValue>();

            private NotExistOptionalValue()
            {
            }

            public TResult Process<TResult>(
                Func<TValue, TResult> existFunc, Func<TResult> notExistFunc)
            {
                return notExistFunc();
            }
        }

        private class ExistOptionalValue<TValue> : IOptionalValue<TValue>
        {
            private readonly TValue value;

            public ExistOptionalValue(TValue value)
            {
                this.value = value;
            }

            public TResult Process<TResult>(
                Func<TValue, TResult> existFunc, Func<TResult> notExistFunc)
            {
                return existFunc(value);
            }
        }
    }

http://propertyexpression.codeplex.com/SourceControl/changeset/view/66209#1409174
http://propertyexpression.codeplex.com/SourceControl/changeset/view/66209#1409173
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.