Довольно часто встречаются задачи, в которых фигурирует некоторое значение, но при некоторых условиях значение отсутствует. Для таких ситуаций предлагается использовать конструкцию OptionalValue, см. код ниже. Код каждого метода очень простой, поэтому лучше посмотреть сам код. Ниже буду описывать использование OptionalValue.
Катастрофа при Extract Method
Рассмотрим строчку кода:
string someMethodResult = new SomeClass1().SomeMethod1();
string someMethodResult = ExtractedMethod1().SomeMethod1(); //ошибка в run time-е
SomeClass1 ExtractedMethod1()
{
return null;
}
Хочется иметь такую технику кодирования, чтобы Extract Method не переводил ошибки компиляции в ошибки run time-а.
Отсюда вывод: не использовать null в качестве возвращаемого значения! То есть набирать на клавиатуре null только: для передачи значений в уже имеющиеся методы (например, третий аргумент в методе PropertyInfo.SetValue),
в операторе сравнения (фактически вариант а),
может что-то забыл . Но не для возврата значения.
Все null-ы мы таким образом не истребим, поскольку field-ы классов инициализируются null-ами,
оператор "as" возвращает null,
уже имеющиеся библиотеки поставляют 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. Отличия: OptionalValue работает и для reference и для value типов, System.Nullable только для value типов,
у OptionalValue generic параметр ковариантный.
The Maybe Monad
Да, OptionalValue является Maybe монадой. Но использование двух перегруженных методов ProcessValue часто оказывается удобнее использования разноименных монадных методов SelectMany и Select.
Query comprehension syntax
Методы SelectMany и Select позволяют использовать query comprehension syntax для OptionalValue.
Интересные ссылки на эту тему
Изобретатель null reference называет свое изобретение ошибкой на миллион долларов:
Общественность уже понимает необходимость разделения 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
Методы 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);
}
}
}
Здравствуйте, Alexander Polyakov, Вы писали:
AP>Довольно часто встречаются задачи, в которых фигурирует некоторое значение, но при некоторых условиях значение отсутствует. Для таких ситуаций предлагается использовать конструкцию OptionalValue, см. код ниже. Код каждого метода очень простой, поэтому лучше посмотреть сам код. Ниже буду описывать использование OptionalValue.
Это случайно не Null Object Pattern называется?
Если нам не помогут, то мы тоже никого не пощадим.
Re[2]: Optional Value. Уменьшение количества null reference-
Здравствуйте, IT, Вы писали:
IT>Это случайно не Null Object Pattern называется?
Скорее нет, чем да. Если пользоваться описаниями ссылка1, ссылка2, то Null Object Pattern предлагает делать кастомный интерфейс и кастомную реализацию Null Object-а для каждого случая. A OptionalValue один для всех .
Наиболее близкую ссылку я уже привел в первом посте Option type. Оттуда можно перейти, например, к реализации в Scala.
Здравствуйте, Alexander Polyakov, Вы писали:
IT>>Это случайно не Null Object Pattern называется? AP>Скорее нет, чем да. Если пользоваться описаниями ссылка1, ссылка2, то Null Object Pattern предлагает делать кастомный интерфейс и кастомную реализацию Null Object-а для каждого случая. A OptionalValue один для всех .
Это всё элементарно решается с помощью дженериков.
AP>Наиболее близкую ссылку я уже привел в первом посте Option type. Оттуда можно перейти, например, к реализации в Scala.
Или в Немерле. Но без ПМ этим пользоваться не очень удобно.
Если нам не помогут, то мы тоже никого не пощадим.
Re: Optional Value. Уменьшение количества null reference-ов.
Проблема этого интерфейса что он не энфорсит контракт, это при том что кроме Some\Just и None реализаций не нужно. Но я могу сколько угодно своих реализаций подсовывать.
Вот мой вариант, завалявшихся со стародавних времен:
public abstract class Option<T>
{
class Just:Option<T>
{
T value;
public Just(T value)
{
this.value = value;
}
public override bool HasValue
{
get { return true; }
}
public override T Value
{
get { return this.value; }
}
}
class NoneClass:Option<T>
{
public override bool HasValue
{
get { return false; }
}
public override T Value
{
get { throw new InvalidOperationException("Value is empty"); }
}
}
public abstract T Value { get; }
public abstract bool HasValue { get; }
private Option()
{
}
public static readonly Option<T> None = new NoneClass();
public static Option<T> Create(T value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
return new Just(value);
}
public static implicit operator Option<T>(T value)
{
if (value != null)
{
return Create(value);
}
else
{
return None;
}
}
public static explicit operator T(Option<T> option)
{
return option.Value;
}
}
public static class Option
{
public static Option<T> None<T>()
{
return Option<T>.None;
}
public static Option<T> ToOption<T>(this T value)
{
return value;
}
public static Option<V> SelectMany<T,V>(this Option<T> value, Func<T, Option<V>> selector)
{
return value.HasValue ? selector(value.Value) : None<V>();
}
public static Option<V> SelectMany<T,K,V>(this Option<T> value, Func<T, Option<K>> selector, Func<T,K,V> resultSelector)
{
return value.SelectMany(left => selector(left).SelectMany(right => resultSelector(left, right).ToOption()));
}
public static Option<V> Select<T,V>(this Option<T> value, Func<T, V> selector)
{
return value.SelectMany(v => selector(v).ToOption());
}
public static Option<T> Where<T>(this Option<T> value, Func<T, bool> predicate)
{
return value.SelectMany(v => predicate(v) ? v.ToOption() : None<T>());
}
public static V Process<T,V>(this Option<T> value, Func<T, V> success, Func<V> fail)
{
return value.HasValue ? success(value.Value) : fail();
}
}
class Program
{
static void Main(string[] args)
{
var z1 = from c1 in 2.ToOption()
from c2 in 3.ToOption()
select c1 + c2;
Console.WriteLine(z1.HasValue);
Console.WriteLine(z1.Value);
var z2 = from c1 in 2.ToOption()
from c2 in 3.ToOption()
where c2 > 3
select c1 + c2;
Console.WriteLine(z2.HasValue);
}
}
Re[4]: Optional Value. Уменьшение количества null reference-
Здравствуйте, IT, Вы писали:
IT>Здравствуйте, Alexander Polyakov, Вы писали:
IT>Или в Немерле. Но без ПМ этим пользоваться не очень удобно.
Приведенное решение хорошо как раз тем, что использовать его можно без ПМ. Именно за без ПМ я и поставил оценку.
Кстати, проще было бы реализовать его именно через ПМ (то есть через тест типа)
Здравствуйте, IT, Вы писали:
IT>>>Это случайно не Null Object Pattern называется? AP>>Скорее нет, чем да. Если пользоваться описаниями ссылка1, ссылка2, то Null Object Pattern предлагает делать кастомный интерфейс и кастомную реализацию Null Object-а для каждого случая. A OptionalValue один для всех . IT>Это всё элементарно решается с помощью дженериков.
В описании паттерна четко фигурирует кастомный интерфейс (или абстрактный класс) с кастомным набором методов.
A Linked list is either a head element and a tail which is a list or empty (i.e. Null). Thus is makes sense to model an abstract linked list with a class List which has two methods getTail and accept as per the Visitor pattern [GHJV95, page 331]. http://www.cs.oberlin.edu/~jwalker/nullObjPattern/
Каким боком ты сюда дженерики вставишь? Уберешь этот кастомный интерфейс? Тогда это будет совсем не тот паттерн.
Такое ощущение, что ты ориентируешься только на слова в названии паттерна, попробуй почитать описание паттерна.
IT>Это всё элементарно решается с помощью дженериков.
Еще бы неплохо type inference более менее продвинутый иметь. Попробуй реализуй на Java-е.
IT>Но без ПМ этим пользоваться не очень удобно.
Пример кода с использование ПМ можно?
Re[5]: Optional Value. Уменьшение количества null reference-
Здравствуйте, Qbit86, Вы писали:
Q>В C# для этих целей использую Option из Elevate Нет методов Process, нет перегруженных методов ProcessValue. А это ценно.
Нет ковариантности generic параметра. Поэтому нельзя пользоваться полиморфизмом по generic параметру, что очень печально.
Метод Select по правилам должен называться SelectMany.
Нет методов SelectMany и Select для поддержки query comprehension syntax.
Метод Some должен быть extension методом.
Используйте OptionalValue из Property Expression . Там это даже раньше появилось. По результатам обсуждения на форумах смерджу последнюю версию в Trunk, и выпущу новый релиз.
Здравствуйте, gandjustas, Вы писали:
G>Как-то слишком многословно
Где ты увидел многословность? Объем совпадает с твоим, просто у тебя отсутствуют некоторые полезные методы.
G>и к тому же не компилится, не хватает ToFunc.
Это мелочи, к тому же там была ссылка на сорци полного солюшена. Там все просто. Вот без ToFunc:
public static void Process<TValue>(
this IOptionalValue<TValue> optionalValue,
Action<TValue> existAction,
Action notExistAction)
{
optionalValue.Process(
value =>
{
existAction(value);
return new {};
},
() =>
{
notExistAction();
return new {};
});
}
G>Вот мой вариант, завалявшихся со стародавних времен: G>
G>public abstract class Option<T>
Главный недостаток твоего варианта -- нет ковариантности generic параметра. Поэтому нельзя пользоваться полиморфизмом по generic параметру, что очень печально.
G>Проблема этого интерфейса что он не энфорсит контракт, это при том что кроме Some\Just и None реализаций не нужно. Но я могу сколько угодно своих реализаций подсовывать.
Это замечание незначительно само по себе, а с учетом отсутствия ковариантности generic параметра в альтернативном (твоем) варианте значимость этого замечания пропадает вовсе.
Re[3]: Optional Value. Уменьшение количества null reference-
S>Да, мне это ближе!
Как я уже писал в первом посте. Метод Process и методы GetValue/HasValue выражаются друг через друга в обе стороны, ситуация симметричная. Да, можно ввести в интерфейс IOptionalValue свойства Value и HasValue, а метод Process наоборот перенести в extension методы (в моем текущем проекте именно так). Мои аргументы в пользу варианта из первого поста: Не хочется иметь в базовом интерфейсе свойство, кидающее исключение.
В случае если не срабатывает type inference, то для extension метода Process придется явно прописывать значения обоих generic параметров. А если Process является методом интерфейса, то прописывать придется значение только одного generic параметра.
Вы считаете эти аргументы незначительными?
По производительности вариант со свойствами Value/HasValue, наверное, лучше.
Re: Optional Value. Уменьшение количества null reference-ов.
Здравствуйте, Alexander Polyakov, Вы писали:
AP>Хочется иметь такую технику кодирования, чтобы Extract Method не переводил ошибки компиляции в ошибки run time-а.
Правильное решение этой проблемы — использование контрактов и статической верификации. Если бы оно ешё работало...
AP> оба варианта ведут себя одинаково -- если код компилируется, то он работает.
Да. Но, вместо того, чтобы решать проблему в её источнике — методе, вы передаёте ответственность по соблюдению контракта всему остальному коду. NullReferenceException хотя бы гарантировал, что при ошибке в вашем коде программа упадёт. OptionalValue маскирует исходную проблему и источник её возникновения: "ой, у нас тут значения нет — фиг с ним, подсунем что-нить"
Re[4]: Optional Value. Уменьшение количества null reference-
S>>Да, мне это ближе!
Ближе собственно тем, что при работе с АТД используется один неполиморфный метод.
AP>Как я уже писал в первом посте. Метод Process и методы GetValue/HasValue выражаются друг через друга в обе стороны, ситуация симметричная. Да, можно ввести в интерфейс IOptionalValue свойства Value и HasValue, а метод Process наоборот перенести в extension методы (в моем текущем проекте именно так). AP>Мои аргументы в пользу варианта из первого поста: AP> AP>Не хочется иметь в базовом интерфейсе свойство, кидающее исключение. AP>В случае если не срабатывает type inference, то для extension метода Process придется явно прописывать значения обоих generic параметров. А если Process является методом интерфейса, то прописывать придется значение только одного generic параметра. AP>
1. Не нужны эти свойства, если пойти через тест типа аки в ПМ (здесь
2. Можно пример?
AP>Вы считаете эти аргументы незначительными?
нет.
AP>По производительности вариант со свойствами Value/HasValue, наверное, лучше.
вряд ли намного лучше. Копейки тут выжимать нет смысла, т.к. вся эта кухня куда дороже (по производительности) чем проверка на null.
З.Ы. В некоторых случаях может оказаться удобнее OptionalValue, построенный не на АТД, а на кортеже флага и значения. Такая штука шикарно передается в качестве параметров по умолчанию. Описано у desco.
Re[3]: Optional Value. Уменьшение количества null reference-
Здравствуйте, Alexander Polyakov, Вы писали:
AP>Здравствуйте, gandjustas, Вы писали:
G>>Как-то слишком многословно AP>Где ты увидел многословность? Объем совпадает с твоим, просто у тебя отсутствуют некоторые полезные методы.
Твой код в полтора раза больше.
Какие например?
У меня еще есть Where комбинатор.
G>>Вот мой вариант, завалявшихся со стародавних времен: G>>
G>>public abstract class Option<T>
AP>Главный недостаток твоего варианта -- нет ковариантности generic параметра. Поэтому нельзя пользоваться полиморфизмом по generic параметру, что очень печально.
Приведи пример где ковариантность необходима.
G>>Проблема этого интерфейса что он не энфорсит контракт, это при том что кроме Some\Just и None реализаций не нужно. Но я могу сколько угодно своих реализаций подсовывать. AP>Это замечание незначительно само по себе,
Да ну? Сломать контракт это фигня?
AP>а с учетом отсутствия ковариантности generic параметра в альтернативном (твоем) варианте значимость этого замечания пропадает вовсе.
Приведи пример где ковариантность необходима.
Re[2]: Optional Value. Уменьшение количества null reference-
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, Alexander Polyakov, Вы писали:
AP>>Хочется иметь такую технику кодирования, чтобы Extract Method не переводил ошибки компиляции в ошибки run time-а. S>Правильное решение этой проблемы — использование контрактов и статической верификации. Если бы оно ешё работало...
AP>> оба варианта ведут себя одинаково -- если код компилируется, то он работает. S>Да. Но, вместо того, чтобы решать проблему в её источнике — методе, вы передаёте ответственность по соблюдению контракта всему остальному коду. NullReferenceException хотя бы гарантировал, что при ошибке в вашем коде программа упадёт. OptionalValue маскирует исходную проблему и источник её возникновения: "ой, у нас тут значения нет — фиг с ним, подсунем что-нить"
Ну это неверно. Maybe монада и была придумана, как средство описать частичные вычисления, которые могут не возвращать результат в некоторых случаях. Правда имеет смысл как можно дольше оставаться в этой самой монаде, а метод Process такой способностью не обладает.
Re[3]: Optional Value. Уменьшение количества null reference-
Здравствуйте, gandjustas, Вы писали:
G>Ну это неверно. Maybe монада и была придумана, как средство описать частичные вычисления, которые могут не возвращать результат в некоторых случаях. Правда имеет смысл как можно дольше оставаться в этой самой монаде, а метод Process такой способностью не обладает.
Не спорю. Только топикстартер её использует для других целей:
Хочется иметь такую технику кодирования, чтобы Extract Method не переводил ошибки компиляции в ошибки run time-а.
Re: Optional Value. Уменьшение количества null reference-ов.
Здравствуйте, Alexander Polyakov, Вы писали:
AP>Отсюда вывод: не использовать null в качестве возвращаемого значения! То есть набирать на клавиатуре null только: AP>Но наша цель более скромная – уменьшить количество null-ов. Кстати, по пунктам a и b еще можно кое-что отвоевать.
Не очень понятен смысл неиспользования null-ов. Проблема, я так понимаю, в том, что NRE упадёт слишком поздно?
AP>Как будет выглядеть приведенный выше пример в случае использования OptionalValue? AP>Делаем замену вызова конструктора "new SomeClass1()" на Nothing. Добиваемся, чтобы код компилировался, в итоге получаем первый вариант: AP>
AP>string someMethodResult = OptionalValue.Nothing<SomeClass1>().Process(
AP> value => value.SomeMethod1(),
AP> () => "[Экземпляр класса SomeClass1 не существует]");
AP>
второй вариант: AP>
AP>string someMethodResult = ExtractedMethod1().Process(
AP> value => value.SomeMethod1(),
AP> () => "[Экземпляр класса SomeClass1 не существует]");
AP>IOptionalValue<SomeClass1> ExtractedMethod1()
AP>{
AP> return OptionalValue.Nothing<SomeClass1>();
AP>}
AP>Таким образом, оба варианта ведут себя одинаково -- если код компилируется, то он работает.
Не очень понял, что будет происходить при возврате null из SomeMethod1(). Будет выброшен NRE? Или в консоль уедет "[Экземпляр класса SomeClass1 не существует]"?
AP>
The Maybe Monad
Да, OptionalValue является Maybe монадой. Но использование двух перегруженных методов ProcessValue часто оказывается удобнее использования разноименных монадных методов SelectMany и Select.
AP>Общественность уже понимает необходимость разделения nullable типов и не nullable типов. Но пока в майнстримовых языка/платформах это не реализовано. AP>
AP>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.
AP>http://codevanced.net/post/One-Annotations-Way-Resharper.aspx
AP>
AP>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.
AP>http://stackoverflow.com/questions/1943465/avoiding-null-reference-exceptions
AP>В Википеии это описано под термином Option type.
AP>
Source code of OptionalValue
Методы GetValue/HasValue можно поменять местами с методом интерфеса IOptionalValue.Process, т.е. GetValue/HasValue будут в интерфейсе, а Process будет extension методом, который будет выражаться через GetValue/HasValue. В этом случае GetValue можно сделать свойством Value. Я этого не делаю, вот по какой причине. Не хочется иметь в базовом интерфейсе свойство, кидающее исключение. Метод GetValue желательно дергать как можно реже.
Не вижу ничего плохого в выбросе исключения. Вопрос только в том, когда именно исключение кидать:
1. При компиляции, при попытке засунуть null в non-nullable объект => вроде бы невозможно на текущем уровне развития компилятора
2. При исполнении, при попытке прочитать null в non-nullable объекта => то же самое, что сейчас делает среда
3. При исполнении, при попытке засунуть null в non-nullable объект => вот это было бы полезно, но в ваших примерах я этого не вижу.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.