понедельник, 2 июня 2014 г.

Борьба с "нулевыми" ссылками в C#

UPDATE: интересно продолжение этой статьи? Читайте: “Контракты vs. Монады?”.

Вступление

В моих черновиках уже больше года лежит статья, в которой я хотел рассказать о проблеме разыменовывания пустых ссылок (null reference dereferencing), с подходами в разных языках и платформах. Но поскольку у меня все никак не доходили руки, а в комментариях к прошлой статье ("Интервью с Бертраном Мейером") была затронута эта тема в контексте языка C#, то я решил к ней все-таки вернуться. Пусть получилось не столь фундаментально как я хотел изначально, но букв и так получилось довольно много.

Ошибка на миллиард долларов?

В марте 2009-го года сэр Тони Хоар (C.A.R. Hoare) выступил на конференции Qcon в Лондоне с докладом на тему "Нулевые ссылки: ошибка на миллиард долларов" (Null References: The Billion Dollar Mistake), в котором признался, что считает изобретение нулевых указателей одной из главных своих ошибок, стоившей индустрии миллиарды долларов.

“Я называю это своей ошибкой на миллиард долларов. Речь идет о изобретении нулевых ссылок (null reference) в 1965 году. В то время, я проектировал комплексную систему типов для ссылок в объекто-ориентированном языке программирования ALGOL W. Я хотел гарантировать, что любое использование всех ссылок будет абсолютно безопасным, с автоматической проверкой этого компилятором. Но я не смог устоять перед соблазном добавить нулевую ссылку (null reference), поскольку реализовать это было столь легко. Это решение привело к бесчисленному количеству ошибок, дыр в безопасности и падений систем, что привело, наверное, к миллиардным убыткам за последние 40 лет. В последнее время, ряд анализаторов, таких как PREfix и PREfast корпорации Microsoft были использованы для проверки валидности ссылок и выдачи предупреждений в случае, если они могли быть равными null. Совсем недавно появился ряд языков программирования, таких как Spec# с поддержкой ненулевых ссылок (non-null references). Это именно то решение, которое я отверг в 1965-м.”

Сегодня уже поздно говорить, чтоб бы было, если бы Хоар когда-то принял иное решение и более разумно посмотреть, как эти проблемы решаются в разных языках и, в частности, в языке C#.

Void Safety в Eiffel (с примерами на C#)

Одним из первых на выступление Тони Хоара отреагировал Бертран Мейер, гуру ООП и автор языка Eiffel. Предложенное Мейером решение заключается в разделении переменных всех ссылочных типов на две категории: на переменные, допускающие null (nullable references или detach references в терминах Eiffel) и на переменные, не допускающие null (not-nullable references или attach references в терминах Eiffel).

При этом по умолчанию, все переменные стали относится именно к non-nullable категории! Причина такого решения в том, что на самом деле, в подавляющем большинстве случаев нам нужны именно non-nullable ссылки, а nullable-ссылки являются исключением.

С таким разделением компилятор Eiffel может помочь программисту в обеспечении "Void Safety": он свободно позволяет обращаться к любым членам not-nullable ссылок и требует определенного "обряда" для потенциально «нулевых» ссылок. Для этого используется несколько паттернов и синтаксических конструкций, но идея, думаю, понятна. Если переменная может быть null, то для вызова метода x.Foo() вначале требуется проверить, что x != null.

При этом, чтобы решить проблему с многопоточностью (дабы избавиться от гонок), вводится специальная синтаксическая конструкция, следующего вида:

if attached t as l then
  l.f -- здесь обеспечивается Void safety.
end

Если провести параллель между языком Eiffel и C#, то выглядело бы это примерно так. Все переменные ссылочного типа превратились бы в not-nullable переменные, что требовало бы их инициализации в месте объявления валидным (not null) объектом. А доступ к любым nullable переменным требовал бы какой-то магии:

// Где-то в идеальном мире
// s – not-nullable переменная

public void Foo(string
s)
{
   
// Никакие проверки, контркты, атрибуты не нужны
    Console.WriteLine(s.
Length);
}

// str – nullable (detached) переменная.
//string! аналогичен типу Option<string>

public void Boo(string
! str)
{
   
// Ошибка компиляции!
    // Нельзя обращаться к членам "detached" строки!
    // Console.WriteLine(str.Length);
    str.IfAttached((string s) => Console.
WriteLine(s));
   
// Или
    if (str != null
)
       
Console.WriteLine(str.
Length);
}

public void Doo(string
! str)
{
   
Contract.Requires(str != null
);
   
// Наличие предусловия позволяет безопасным образом
    // обращаться к объекте string через ссылку str!
    Console.WriteLine(str.Length);
}

Такие изменения в языке, а также в стандартной библиотеке Eiffel позволили гарантировать отсутствие разыменовывания нулевых ссылок. Но даже с небольшим комьюнити, для внедрения этих изменений потребовалось несколько лет. Поэтому неудивительно, что нам не стоит ждать аналогичных изменений в таком языке как C#.

Более того, реалии таковы, что нам вряд ли стоит ждать появления not-nullable ссылочных типов (о чем не так давно поведал Эрик Липперт в своей статье "C#: Non-nullable Reference Types"), поэтому нам приходится изобретать различного вида велосипеды. Но прежде чем переходить к этим самым велосипедам, интересно посмотреть на подход другого языка платформы .NET – F#.

ПРИМЕЧАНИЕ
Подробнее о Void Safety в Eiffel можно почитать в статье Бертрана Мейера "Avoid a Void: The eradication of null dereferencing", а также в замечательной статье Жени Охотникова "Void safety в языке Eiffel".

Void Safety в F#

Большинство функциональных языков программирования (включая F#) решают проблему разыменовывания нулевых ссылок уже довольно давно, причем способом аналогичным рассмотренному ранее. Так, типы, объявленные в F# по умолчанию не могут быть null (нужно использовать специальный атрибут AllowNullLiteral, да и в этом случае это возможно не всегда). В F# разрешается использование литерала null, но лишь для взаимодействия со сторонним кодом, не столь продвинутым в вопросах Void Safety, написанном на таких языках как C# или VB (хотя и в этом случае иногда могут быть проблемы, см. "F# null trick").

Для указания "отсутствующего" значения в F# вместо null принято использовать специальный тип – Option<T>. Это позволяет четко указать в сигнатуре метода, когда ожидается nullable аргумент или результат, а когда результат не должен быть null:

// Результат не может быть null
let findRecord (id: int) : Record =
    ...


// Результат тоже не может быть null,
// но результат завернут в Option<Record> и может отсутствовать

let tryFindRecord (id: int) : Option<Record> =
    ...

Так, в первом случае возвращаемое значение обязательно (и при его отсутствии будет сгенерировано исключение), а во втором случае результат может отсутствовать.

ПРИМЕЧАНИЕ
Именно этой идиоме следует стандартная библиотека F#. Так, вместо пары методов из LINQ: Single/SingleOrDefault в F# используются методы find/tryFind модулей Seq, List и других.

Поскольку "необязательные" значения может участвовать в целой цепочке событий, любой функциональный язык программирования поддерживает специальный "паттерн", который позволяет "протаскивать" значение через некоторую цепочку операций. Данный паттерн носит название "монада" и является фундаментальным в некоторых функциональных языках программирования. Я не хочу останавливаться на абстрактном описании, поскольку для этого уже есть ряд отличных источников (см. дополнительные источники в конце статьи).

Идея же достаточно простая: нам нужен простой способ работать с "завернутыми" значениями, такими как Option<T>, аналогично тому, как бы мы работали с самим типом T напрямую. Проще всего это показать на примере:

Предположим, у нас есть цепочка операций:

  1. Попытаться найти сотрудника (tryFindEmployee), если он найден, то
  2. взять свойство Department, если свойство не пустое, то
  3. взять свойство BaseSalary, если свойство не пустое, то
  4. вычислить размер заработной платы (computeSalary).
  5. если один из этапов возвращает не содержит результата (None в случае F#), то результат должен отсутствовать (должен быть равен None), в противном случае мы получим вычисленный результат.

Это можно сделать большим набором if-ов, а можно использовать Option.bind:

let salary = 
    tryFindEmployee
42
 
   
|> Option.bind(fun e -> e.
Department)
   
|> Option.bind(fun d -> d.
BaseSalary)
   
|> Option.bind computeSalary

Основная идея метода bind очень простая: если Option<T> содержит значение, тогда метод bind вызывает переданный делегат с "распакованным" значением, в противном случае – просто возвращается None (отсутствие значение в типе Option). При этом делегат, переданный в метод Bind также может вернуть значение, завернутое в Option или None, если значение не получено. Такой подход позволяет легко создавать цепочки операций, не заморачиваясь кучей проверок на null на каждом этапе.

Void Safety в C#

Поскольку популярность функциональных языков существенно возросла, то не удивительно, что многие паттерны функционального программирования начали перекачивать в такие исходно объектно-ориентированные языки, как C# (авторы C# серьезно думают о добавлении в одной из новых версий Pattern Matching-а!). Поэтому не удивительно, что для решения проблемы "нулевых ссылок" очень часто начали использоваться идиомы, заимствованные из функциональных языков.

"Монада" Maybe

Одна из причин появления этой статьи заключается в попытке выяснить, насколько полезно/разумно использовать тип Maybe<T> (аналогичный типу Option<T> из F#) в языке C#. В комментариях к предыдущей заметке было высказано мнение о пользе этого подхода, но хотелось бы рассмотреть "за" и "против" более подробно.

> Используйте монадки, и не нужно париться с null. Накладывайте рестрикшен через тип.

Действительно, строгая типизация и выражение намерений через систему типов – это лучший способ для разработчика передать свои намерения. Так, не зная существующих идиом платформы .NET, очень сложно понять, в чем же разница между методами First и FirstOrDefault в LINQ, или как понять, что будет делать метод GetEmployeeById в случае отсутствия данных по сотруднику: вернет null или сгенерирует исключение? В первом случае суффикс "Default" намекает, что в случае отсутствия элемента мы получим "значение по умолчанию" (т.е. null), но что будет во втором случае – не ясно.

В этом плане подход, принятый в F# выглядит намного более приятным, ведь там методы отличаются не только именем, но и возвращаемым значением: метод find возвращает T, а tryFind возвращает Option<T>. Так почему бы не пользоваться этой же идиомой в языке C#?

Главная причина, останавливающая от повсеместного использования Option<T> в C# заключается в отсутствии повсеместного использования Option<T> в C#. Звучт бредово, но все так и есть: использование Option<T> не является стандартной идиомой для библиотек на языке C#, а значит использование своего "велосипедного" Option<T> не принесет существенной пользы.

Теперь стоит понять, а стоит ли овчинка выделки и будет ли полезным использование Option<T> лишь в своем проекте.

Обеспечивает ли Maybe<T> Void Safety?

Особенность использования Maybe заключается в том, что это лишь смягчает, но не решает полностью проблему "разыменовывания нулевых указателей". Проблема в том, что нам все равно нужен способ сказать в коде, что данная переменная типа Maybe<T> должна содержать значение в определенный момент времени. Нам не только нужен безопасный способ обработать оба возможных варианта значения переменной Maybe, но и сказать о том, что один из них в данный момент не возможен.

Давайте рассмотрим такой пример. При реализации "R# Contract Extensions" мне понадобился класс для добавления предусловий/постусловий в абстрактные методы, и методы интерфейсов. Для тех, кто не очень в курсе библиотеки Code Contracts, контракты для абстрактных методов и интерфейсов задаются отдельным классом, который помечается специальным атрибутом – ContractClassFor:

[ContractClass(typeof (AbstractClassContract))]
public abstract class AbstractClass
{
   
public abstract void Foo(string
str);
}

// Отдельный класс с контрактами абстрактного класса
[ContractClassFor(typeof (AbstractClass))]
abstract class AbstractClassContract : AbstractClass
{
   
public override void Foo(string
str)
    {
       
Contract.Requires(str != null
);
       
throw new System.NotImplementedException();
    }
}

Процесс добавления контракта для абстрактного метода AbstractClass.Foo такой:

  1. Получить "контрактный метод" текущего метода (в нашем случае нужно найти "дескриптор" метода AbstractClassContract.Foo).
  2. Если такого метода еще нет (или нет класса AbstractClassContract), сгенерировать класс контракта с этим методом.
  3. Получить "контрактный метод" снова (теперь мы точно должны получить "дескриптор" метода AbstractClassContract.Foo).
  4. Добавить предусловие/постусловие в "контрактный метод".

В результате метод добавления предусловия для абстрактного метод выглядит так (см. ComboRequiresContextAction метод ExecutePsiTransaction):

var contractFunction = GetContractFunction();
if (contractFunction == null
)
{

    AddContractClass();

   
contractFunction = GetContractFunction();
    Contract.Assert(contractFunction != null
);
}


...

[CanBeNull, System.Diagnostics.Contracts.Pure]
private ICSharpFunctionDeclaration 
GetContractFunction()
{

    return _availability.SelectedFunction.GetContractFunction();
}

Метод GetContractFunction возвращает метод класса-контракта для абстрактного класса или интерфейса (в нашем примере, для метода AbstractClass.Foo этот метод вернет AbstractClassContract.Foo, если контрактный класс существует, или null, если класса-контракта еще нет).

Как бы изменился этот метод, если бы я использовал Maybe<T> или Option<T>? Я бы убрал атрибут CanBeNull и поменял тип возвращаемого значения. Но вопрос в том, как бы использование Maybe мне помогло бы выразить, что "постусловие" этого закрытого метода меняется в зависимости от контекста? Так, при первом вызове этого метода вполне нормально, что контрактного метода еще нет и возвращаемое значение равно null (или None, в случае "монадического" решения). Однако после вызова AddContractClass (т.е. после добавления класса-контракта) возвращаемое значение точно должно быть и метод GetContractFunction обязательно должен вернуть "непустое" значение! Я никак не могу отразить это в системе типов языка C# или F#, поскольку лишь я знаю ожидаемое поведение. (Напомню, что контракты выступают в роли спецификации и задают ожидаемое поведение, отклонение от которого означает наличие багов в реализации. Так и в этом случае, если второй вызов GetContractFunction вернул null, то это значит, что в консерватории что-то сломалось и нужно лезть в код и его чинить).

Этим маленьким примером я хотел показать, что не все проблемы с разыменовыванием нулевых указателей можно решить с помощью класса Maybe, а контракты и аннотации (такие как CanBeNull) вполне может поднять «описательность» методов не меняя тип возвращаемого значения.

Использование "неявной" монады Maybe

Как уже было сказано выше, польза от Maybe будет лишь в том случае, если он будет использован всеми типами приложения. Это значит, что если ваш метод TryFind возвращает Maybe<Person>, то нужно, чтобы все "nullable" свойства класса Person тоже возвращали Maybe<T>.

Добиться повсеместного использования Maybe в любом серьезном приложении, написанном на C#, весьма проблематично, поскольку все равно останется масса "внешнего" кода, который не знает об этой идиоме. Так почему бы не воспользоваться этой же идеей, но без «заворачивания» всех типов в Maybe<T>?

Так, класс Maybe<T> (или Option<T>) является своего рода оболочкой вокруг некоторого значения, со вспомогательным методом Bind (название метода является стандартным для паттерна монада), который "разворачивает коверт" и достает из него реальное значение и передает для обработки. При этом становится очень легко строить pipe line, который мы видели в разделе, посвященном языку F#.

Но почему бы не рассматривать любую ссылку, как некую "оболочку" вокруг объекта, для которого мы добавим метод расширения с тем же смыслом, что и метод Bind класса Option? В результате, мы создадим метод, который будет "разворачивать" нашу ссылку и вызывать метод Bind (который мы переименуем в With) лишь тогда, когда текущее значение не равно null:

public static U With<T, U>(this T callSite, Func<T, U> selector) where T : class
{
    Contract.Requires(selector != null);

   
if (callSite == null)
        return default(U);

   
return selector(callSite);
}

В этом случае, мы сможем использовать достаточно компактный синтаксис для обхода целого дерева, каждый узел которого может вернут null. И тогда, если все "узлы" этого дерева будут не null, мы получим непустой результат, если же на одном из этапов, значение будет отсутствовать, то результат всего выражения будет null.

С методом With наш пример, рассмотренный в предыдущем разделе будет выглядеть так:

var salary = Repository.GetEmployee(42)
   
.With(employee => employee.
Department)
   
.With(department => department.
BaseSalary)
   
.With(baseSalary => ComputeSalary(baseSalary));

В некоторых случаях такой простой "велосипед" может в разы сократить размер кода и здорово улучшить читабельность, особенно при работе с "глубокими" графами объектов, большинство узлов которого может содержать null. Примером такого "глубокого" графа является API Roslyn-а, а также ReSharper SDK. Именно поэтому в своем плагине для ReSharper-а я очень часто использую подобный код:

[CanBeNull]
public static IClrTypeName GetCallSiteType(this IInvocationExpression 
invocationExpression)
{

    Contract.Requires(invocationExpression != null);

   
var type = invocationExpression
        .With(x => x.InvokedExpression)
        .With(x => x as IReferenceExpression)
        .With(x => x.Reference)
        .With(x => x.Resolve())
        .With(x => x.DeclaredElement)
        .With(x => x as IClrDeclaredElement)
        .With(x => x.GetContainingType())
        .Return(x => x.GetClrName());
    return type;
}

Данный тип "расковыривает" выражение вызова метода и достает тип, на котором этот вызов происходит. При этом может возникнуть вопрос с отладкой, когда результат равен null и не понятно, что же конкретно пошло не так:

clip_image002

В этом плане очень полезна новая фича в VS2013 под названием Autos, которая показывает, на каком из этапов мы получили null:

clip_image004

(В данном случае мы видим, что Resolve вернул не нулевой элемент, но DeclaredElement этого значения равен null.)

Null propagating operator ?. в C# 6.0

Поскольку проблема обработки сложных графов объектов с потенциально отсутствующими значениями, столь распространена, разработчики C# решили добавить в C# 6.0 специальный синтаксис в виде оператора "?." (null propagating operator). Основная его идея аналогична методу Bind класса Option<T> и приведенному выше методу расширения With.

Следующие три примера эквивалентны:

var customerName1 = GetCustomer() ?.Name;
var customerName2 = GetCustomer().With(x => x.
Name);

var tempCustomer = GetCustomer();
string customerName3 = tempCustomer != null ? tempCustomer.Name : null;

ПРИМЕЧАНИЕ
Null propagating operator не может заменить приведенные ранее методы расширения With, поскольку справа от оператора ?. может быть лишь обращение к членам текущего объекта, а произвольное выражения использовать нельзя.
Так, в случае с With мы можем сделать следующее:

var result = GetCustomer().With(x => x.Name).With(x => string.Intern(x));

В случае с “?.” нам все же понадобится временная переменная.

Заключение

Так как же можно обеспечить Void Safety в языке C#? Обеспечить полностью? Никак. Если же мы хотим уменьшить эту проблему к минимуму, то тут есть варианты.

Мне правда нравится идея явного использования типа Maybe (или Option) для получения более декларативного дизайна, но меня напрягает неуниверсальность этого подхода: для собственного кода этот подход работает "на ура", но все равно будет непоследовательным, поскольку сторонний код этой идиомой не пользуется.

Для обработки сложных графов объектов мне нравится подход с методами расширения типа With/Return для ссылочных типов + контракты для декларативного описания наличия или отсутствия значения. Мне вообще кажется, что аннотации типа CanBeNull + контракты с соответствующей поддержкой со стороны среды разработки (как в плане редактирования, так и просмотра) могут существенно упростить понимание кода и проблема сейчас именно в отсутствии такой поддержки.

После выхода C# 6.0 и появления оператора “?.” в ряде случаев можно будет отказаться от методов расширения With/Return, но иногда такие велосипеды все равно будут нужны из-за ограничений оператора “?.”.

К сожалению, без полноценных not-nullable типов обеспечить Void Safety полностью просто невозможно. Поэтому сейчас нам остается использовать разные велосипеды и надеяться на усовершенствование средств разработки, которые упростят работу с контрактами и nullable-типами.

Дополнительные ссылки

З.Ы. Понравился пост? Поделись с друзьями! Вам не сложно, а мне приятно;)

19 комментариев:

  1. Ответы
    1. И от сергуна еще https://www.nuget.org/packages/Monads/

      Удалить
  2. Жизнь — боль, и язык с null'ами болен ими же самими. Тоже боремся, но все кончается «монадами»-расширениями, которыми и не все пользоваться-то хотят, да и не всегда к месту. Увы.

    ОтветитьУдалить
    Ответы
    1. Есть такое дело!
      Кстати, в аннонсированном вчера чудо языке от Appke - Swift - есть поддержка not-nullable сылок. Так что, видимо, индустрия таки движется в правильном направлении

      Удалить
  3. "для собствненого кода".
    Спасибо за статью, Сергей.

    ОтветитьУдалить
  4. Вообще-то Functor Maybe и Applicative Maybe обеспечивает всё необходимое не затрагивая тему монад. ((+1) <$> Nothing) == Nothing, ((+1) <$> Just 2) == Just 3. Monad Maybe позволяет просто использовать нотацию "do" в Haskell, которая в случае
    do { x <- foo; y <- bar; return x*y; } :: Maybe Int
    делает что-то вроде (*) <$> foo <*> bar .
    А с использованием Applicative Monad можно писать (Nothing <|> Just 4) == Just 4

    По поводу обёрток типов... Можно ввести value-тип Strict<T> и для With иметь overload который не будет проверять на null. Хотя, сомневаюсь это ускорит что-то. Думаю компилятор вставит свою проверку чтобы кинуть NullPointerException даже если это никогда не произойдёт.
    В Haskell очень популярно вводить обёртки которые добавляют семантику через тип и для этого есть специальный способ объявления newtype который может легко наследовать type-class'ы которые есть у оборачиваемого типа (NewtypeDeriving).

    ОтветитьУдалить
    Ответы
    1. Там не Applicative Monad а Alternative Maybe

      Удалить
    2. Ну вот, пришел Коля и сломал всем мозг:))

      Удалить
  5. >>>Null propagating operator не может заменить приведенные ранее методы расширения When, поскольку справа от оператора ?. может быть лишь обращение к членам текущего объекта, а произвольное выражения использовать нельзя.
    >>>Так, в случае с When мы можем сделать следующее:

    Наверно, оба раза имелось в виду не When, а With?

    ОтветитьУдалить
  6. Этот комментарий был удален автором.

    ОтветитьУдалить
  7. 1) Твой пример с контрактами не сильно подходящий, он не высосан с пальца, но он очень спецыфичный к твоей задаче...

    На самом деле ты вводишь сложность с твоим: "постусловие этого закрытого метода меняется в зависимости от контекста?". Мне твой солюшен попахивает "bool hell". Ведь если ты мыслишь в терминах типов то по сути ты должен оперировать типами, а не состоянием в зависимости от какого - то там пост кондишена. Для примера: Maybe монаду ты можешь заимплиментить на bool флажках (isSome, isNone) с использованием basic contrflow конструкций (if, else) в императивном стиле... Но так же ты можешь опиратся на контейнерные типы Just : Maybe, и Nothing : Maybe - улавливаешь разницу?

    [DebuggerStepThrough]
    private class Nothing : Maybe
    {
    public override bool HasValue
    {
    get { return false; }
    }

    public override TValue Value { get { throw new InvalidOperationException(); } }

    public override Maybe Map(Func map)
    {
    return new Nothing();
    }

    public override Maybe FlatMap(Func> flatMap)
    {
    return new Nothing();
    }
    }


    С таким решением ты мыслишь проджекшинами выстраивая четкий воркфлов - это очень правильно. Неправильно - это когда ты оперируешь всяким хитрым bool флажкам, какому-то неявному состоянию - ты только усложняешь свое решение. Также замечу что чаще всего приходится видеть такой код у людей которые не интересуются функциональщиной или не до конца её поняли. Но есть исключения, например перфоменс.

    "Я никак не могу отразить это в системе типов языка C# или F#, поскольку лишь я знаю ожидаемое поведение."
    - Я не сильно понял как ты это отразил с контрактами?

    Если ты имеешь это ввиду:
    contractFunction = GetContractFunction();
    Contract.Assert(contractFunction != null);

    То чем это отличается от if (contractFunction.isNone).

    Мне кажется что твою задачу можно решить по другому.. По сути тебе нужно ретурнуть два разных типа и все. Есть только прямой флов и ты не можешь с него выпрыгнуть рание не пройдя его полностью. Как ты это сделаешь (добавишь еще один интерфейс который будет возвращать NotNull тип и просунешь его дальше, что б твои клиенты работали только с ним, а не с Maybe.... заиспользуешь композицию... способов туча)... Поинт тут в том что ты не должен опиратся на семантику кода, а говорить все через типы.... Это уже сугубо твой выбор...

    2) Еще раз по поводу контрактов: то что я(и многие из моих френдов) вижу - очень страшно.. на 3 строчки кода я должен добавить просто пачку метаданных... и кому это понравится?? а поддержка, а реффакторинг??? Зачем так жостко захламлять код? Для C# это очень не универсальный инструмент....

    3) "Как уже было сказано выше, польза от Maybe будет лишь в том случае, если он будет использован всеми типами приложения. Это значит, что если ваш метод TryFind возвращает Maybe, то нужно, чтобы все "nullable" свойства класса Person тоже возвращали Maybe." - да так и есть... Хотел добавить что TryFind ваще не должен применятся когда у тебя есть Maybe. когда у тебя есть хоть минимальная причина получить null то лучше сразу возвращать Maybe First() и не добавлять FirstOrDefault();

    ОтветитьУдалить
  8. 4) "Добиться повсеместного использования Maybe в любом серьезном приложении, написанном на C#, весьма проблематично, поскольку все равно останется масса "внешнего" кода, который не знает об этой идиоме. Так почему бы не воспользоваться этой же идеей, но без «заворачивания» всех типов в Maybe?"
    - да ну? да ну зачем ты так говоришь? какое заворачивание всех типов?(может у тебя что-то с дизайном что тебе приходится заворачивать все типы в maybe). Я не люблю когда оперируют абстракциями.. давай вот просто посмотрим реальную модель кода (Person кого хочешь) и ты увидешь что там этих Maybe не так много как ты это хочешь преподнести.... Понятно что есть исключения, например у меня не так давно было много интенсивного WinAPI, у меня был для этого WinAPiService который помагал мне работать с хендлами и тд.. и например у него был метод Maybe GetProcessByName(string name); и да мне приходилось чекать результат на IsSome, но у меня при этом Process оставался чистым.. если я его получил (он не IsNone) то значит что все в порядке.. и дальше я с ним работаю без всяких чеков на null... и также на практике, у тебя чаще будет ситуации когда или целый обджек Null или NotNull.

    Я не вижу примущества использования контрактов в C# для проверок на null(там пачка проблем выползает которые неявные). Еще раз, мы на проекте используем монады и у нас нет проблем.

    ОтветитьУдалить
  9. @Antya: ты не хочешь замутить бэттл на Kiev ALT.NET по этому поводу?
    Мы тут будем переходить по частностям обсуждения, теряя основную мысль, поэтому особенно цепляться к заявлениям в комментариях я не хочу. Я готов прокомментить твою обдуманную статью по этому поводу, но сраться в комментах на комменты смысла не вижу:)

    ОтветитьУдалить
  10. @Sergey Teplyakov по поводу батла даже незнаю... Мне кажется, что лучше просто попробовать решить задачку.... просто смоделировав реаьную ситуацию... и сравнить код....

    "Я готов прокомментить твою обдуманную статью по этому поводу, но сраться в комментах на комменты смысла не вижу:)"
    - это тоже правильно, но мне кажется что все сведется к определенной задачке... так как в коментах ты будешь пытатся решить задачку которую я опишу в посте....

    ОтветитьУдалить
    Ответы
    1. Не совсем. Вот я, например, не вполне пойму, с чем именно ты не согласен. Ты реагируешь на конкретные моменты, но я не понял, с чем ты не согласен по сути. Кажется, ты не согласен с тем, что контракты помогают бороться с проблемой нулевых ссылок, и если это так, то я готов описать это подробнее (кстати, я хочу напомнить, что в я ведь использую микс из подходов: у меня есть монадические расширения и есть контракты;)).

      Ну твою статью я не буду просто комментить, я ведь напишу еще одну статью, в которой постараюсь разложить по полочкам, с чем я согласен, а с чем - нет.

      Если таки интересно, я готов продолжить общение и в комментах, но, как правило, это будет существенно менее интерактивным и более трудоемким с точки зрения донесения мысли.
      Ну и можно выбрать такой подход: одна ветка комментов - одна мысль. Так у нас будет шанс не запутаться в мыслях и таки почерпнуть максимум пользы из нашего общения;)

      > Мне кажется, что лучше просто попробовать решить задачку.... просто смоделировав реаьную ситуацию... и сравнить код....
      Я готов на это:) Но тут есть проблема: будет сложно найти такую задачу, в предметной области которой мы одинаково хорошо разбираемся:) А именно от этого будет строиться дизайн решения.

      В статье я привел несколько примеров из своего плагина, в котором проблема нулевых ссылок очень актуальна. В обычной бизнес задаче для меня эта проблема вообще практически не возникала, поскольку я в своем коде очень и очень редко возвращаю null-ы, а значит любой из предложенных подходов будет абсолютно нормальным и не покажет реальной разницы))

      Если же взять такие вещи, как Розлин или R# API, в котором очень ветвистые графы объектов, и многие узлы этих графов могут отсутствовать, много полиморфных узлов с заранее не понятными типами, то там разница будет. Но, как ты правильно заметил, это не вполне типовая задача (раз), и я с этим доменом немного поработал, а ты, скорее всего, нет (два). Это делает ее использование невозможной.

      В общем, я готов к любому раскладу:)

      Удалить