Усиливаем контроль типов: где в типичном C#-проекте присутствует непрошеный элемент слабой типизации?

    Проблема


    Мы привыкли говорить о языках вроде C# как строго и статически типизированных. Это, конечно, правда, и во многих случаях тип, указываемый нами для некоторой языковой сущности хорошо выражает наше представление о ее типе. Но есть широко распространенные примеры, когда мы по привычке («и все так делают») миримся с не совсем верным выражением «желаемого типа» в «объявленном типе». Самый яркий — ссылочные типы, безальтернативно оснащенные значением «null».
    В моем текущем проекте за год активной разработки не было ни одного NullReferenceException. Могу не без оснований полагать, что это следствие применения описанных ниже техник.

    Рассмотрим фрагмент кода:

    public interface IUserRepo 
    {
    	User Get(int id);
    	User Find(int id);
    }
    

    Этот интерфейс требует дополнительного комментария: «Get возвращает всегда не null, но кидает Exception в случае ненахождения объекта; а Find, не найдя, возвращает null». «Желаемые», подразумеваемые автором типы возврата у этих методов разные: «Обязательно User» и «Может быть, User». А «объявленный» тип — один и тот же. Если язык не заставляет нас явно выражать эту разницу, то это не означает, что мы не можем и не должны делать это по собственной инициативе.

    Решение


    В функциональных языках, например, в F#, существует стандартный тип FSharpOption<T>, который как раз и представляет для любого типа контейнер, в котором может либо быть одно значение T, либо отсутствовать. Рассмотрим, какие возможности хотелось бы иметь от такого типа, чтобы им было удобно пользоваться, в том числе приверженцами разных стилей кодирования с разной степенью знакомства с функциональными языками.
    С учетом этого гипотетического типа можно переписать наш репозиторий в таком виде:

    public interface IUserRepo 
    {
    	User Get(int id);
    	Maybe<User> Find(int id);
    }
    

    Сразу оговоримся, что первый метод все еще может вернуть null. Простого способа запретить это на уровне языка — нет. Однако, можно это сделать хотя бы на уровне соглашения в команде разработки. Успех такого начинания зависит от людей; в моем проекте такое соглашение принято и успешно соблюдается.
    Конечно, можно пойти дальше и встроить в процесс сборки проверки на наличие ключевого слова null в исходном коде (с оговоренными исключениями из этого правила). Но в этом пока не было потребности, хватает просто внутренней дисциплины.
    А вообще можно пойти и еще дальше, например, принудительно внедрить во все подходящие методы Contract.Ensure(Contract.Result<T>() != null) через какое-нибудь AOP-решение, например, PostSharp, в таком случае даже члены команды с низкой дисциплиной не смогут вернуть злосчастный null.

    В новой версии интерфейса явно декларируется, что Find может и не найти объект, и в этом случае вернет значение Maybe<User>.Nothing. В этом случае никто не сможет по забывчивости не проверить результат на null. Пофантазируем далее об использовании такого репозитория:

    // забывчивый разработчик забыл проверить на null
    var user = repo.Find(userId); // возвращает теперь не User, а Maybe<User>
    var userName = user.Name; // не компилируется, у Maybe нет Name
    
    var maybeUser = repo.Find(userId); // зато код ниже компилируется,
    string userName;
    if (maybeUser.HasValue) // таким образом нас заставили НЕ забыть проверить на наличие объекта
    {
    	var user = maybeUser.Value;
    	userName = user.Name;
    }
    else 
    	userName = "unknown";
    

    Этот код аналогичен тому, что мы бы написали с проверкой null, просто условие в if выглядит несколько иначе. Однако, постоянное повторение подобных проверок, во-первых, захламляет код, делая суть его операций менее явно заметной, во-вторых, утомляет разработчика. Поэтому было бы крайне удобно иметь для большинства стандартных операций готовые методы. Вот предыдущий код в fluent-стиле:

    string userName = repo.Find(userId).Select(u => u.Name).OrElse("unknown");
    

    Для тех же, кому близки функциональные языки и do-нотация, может быть поддержан совсем «функциональный» стиль:

    string userName = (from user in repo.Find(userId) select user.Name).OrElse("unknown");
    

    Или, пример посложнее:

    (
     from roleAProfile in provider.FindProfile(userId, type: "A")
     from roleBProfile in provider.FindProfile(userId, type: "B")
     from roleCProfile in provider.FindProfile(userId, type: "C")
     where roleAProfile.IsActive() && roleCProfile.IsPremium()
     let user = repo.GetUser(userId)
     select user
    ).Do(HonorAsActiveUser);
    

    с его императивным эквивалентом:

    var maybeProfileA = provider.FindProfile(userId, type: "A");
    if (maybeProfileA.HasValue)
    {
    	var profileA = maybeProfileA.Value;
    	var maybeProfileB = provider.FindProfile(userId, type: "B");
    	if (maybeProfileB.HasValue)
    	{
    		var profileB = maybeProfileB.Value;
    		var maybeProfileC = provider.FindProfile(userId, type: "C");
    		if (maybeProfileC.HasValue)
    		{
    			var profileC = maybeProfileC.Value;
    			if (profileA.IsActive() && profileC.IsPremium())
    			{
    				var user = repo.GetUser(userId);
    				HonorAsActiveUser(user);
    			}
    		}
    	}
    }
    

    Также требуется интеграция Maybe<T> с его достаточно близким родственником — IEnumerable<T>, как минимум в таком виде:

    var admin = users.MaybeFirst(u => u.IsAdmin); // вместо FirstOrDefault(u => u.IsAdmin);
    Console.WriteLine("Admin is {0}", admin.Select(a => a.Name).OrElse("not found"));
    

    Из приведенных выше «мечтаний» ясно, что хочется иметь в типе Maybe
    • доступ к информации о наличии значения
    • и к самому значению, если оно доступно
    • набор удобных методов (или методов-расширений) для потокового стиля вызовов
    • поддержка синтаксиса LINQ-выражений
    • интеграция с IEnumerable<T> и другими компонентами, при работе с которыми часто возникают ситуации отсутствия значения


    Рассмотрим, какие решения может предложить нам Nuget для быстрого включения в проект и сравним их по приведенным выше критериям:

    Название пакета Nuget и тип типа HasValue Value FluentAPI Поддержка LINQ Интеграция с IEnumerable Примечания и исходный код
    Option, class есть нет, только pattern-matching минимальное нет нет github.com/tejacques/Option
    Strilanc.Value.May, struct есть нет, только pattern-matching богатое есть есть Принимает null как допустимое значение в May
    github.com/Strilanc/May
    Options, struct есть есть среднее есть есть Также предлагается тип Either
    github.com/davidsidlinger/options
    NeverNull, class есть есть среднее нет нет github.com/Bomret/NeverNull
    Functional.Maybe, struct есть есть богатое есть есть github.com/AndreyTsvetkov/Functional.Maybe
    Maybe, нет типа минимальное нет методы расширения работают с обычным null
    github.com/hazzik/Maybe
    upd: а вот скринкаст от mezastel с аналогичным подходом и подробным объяснением: www.techdays.ru/videos/4448.html
    WeeGems.Options, struct есть есть минимальное нет нет Также есть другие функциональные полезности: мемоизация, частичное применение функций
    bitbucket.org/MattDavey/weegems

    Так сложилось, что у меня в проекте вырос свой пакет, он есть среди вышеперечисленных.

    Из этой таблицы видно, что самое «легкое», минимально инвазивное решение — это Maybe от hazzik, которое не требует никак менять API, а просто добавляет пару методов-расширений, позволяющих избавиться от одинаковых if-ов. Но, увы, никак не защищает забывчивого программиста от получения NullReferenceException.

    Самые богатые пакеты — Strilanc.Value.Maybe (тут автор объясняет, в частности, почему он решил что (null).ToMaybe() не то же самое, что Maybe.Nothing), Functional.Maybe, Options.

    Выбирайте на вкус. А вообще, хочется, конечно, стандартного решения от Microsoft, а еще функциональных типов в C#, кортежей и т.п :). Поживем — увидим.

    UPD: товарищ aikixd написал оппонирующую статью.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 63
    • +3
      • 0
        Спасибо за ссылки на библиотеки. У меня есть несколько дежурных экстеншн методов проверки на null, но зачем использовать велосипед, попробую вашу библиотеку :) Maybe — хорошее и выразительное название для функции.
        • 0
          Пожалуйста :) Исходные коды его на githube, если в процессе использования родятся какие-то еще общеполезные расширения — пушьте, включим в новые версии пакета.
        • +2
          > первый метод все еще может вернуть null. Простого способа запретить это на уровне языка — нет.

          Есть. И вы сами же упомянули контракты. Тем более что это стандартная фича.
          • 0
            Ну, я имею в виду так, чтобы глобально, например, для целого проекта запретить. На уровне метода — да, контракты. Ну а на уровне проекта — тоже контракты, но с AOP, я об этом написал ниже :)
            • 0
              А как это может выглядеть на уровне проекта (особенно не трогая код конкретных интерфейсов)? Что-то слабо представляю. Все равно как-то придется отмечать методы, а это суть — контракты (вопрос реализации).
              • 0
                Почему же, если на уровне проекта мы заявляем: «методы никогда не возвращают null», то, соответственно, нужно внедрить в код каждого метода, возвращающего ссылочный тип, вышеуказанный контракт. Это работа для AOP.

                При этом подразумевается, что повсеместно (где необходимо) передается/возвращается Maybe. Точки сочленения с внешним миром оборачиваются, если необходимо, тоже в Maybe. И — вуаля, «ошибка на миллион долларов» исправлена :)
                • 0
                  Ну по сути получается обратный подход по сравнению с контрактами. Если там мы считаем что по умолчанию все могут вернуть null и ограничиваем нужные без null, то тут — по умолчанию все не могут возвращать null, но выделяем кто может.
          • +12
            Мы с этой же целью используем стандартные решарперовские атрибуты [CanBeNull], [NotNull], а статический анализатор подскажет, что нужна проверка на нуль еще во время написания кода. И не нужны никакие соглашения в команде.
            • 0
              Добавлю, что можно сделать принудительную элевацию (в т.ч. в масштабах solution'а) уровня проверок на PossibleNullReferenceException в R# с Warning до Error. Таким образом даже сборка станет невозможной, пока в коде присутствуют подобные скользкие места.
              • 0
                Мне кажется, что ошибки, обнаруженные Resharper никак не влияют на способность компилятора собрать проект, или я ошибаюсь и это можно где-нибудь включить?
                Было бы, кстати, весьма неприятно, так как Resharper иногда перестает видеть типы из других сборок и тогда половина проекта становится красной, но на компиляцию это, к счастью, не влияет.
                • 0
                  На способности csc.exe конечно не влияют, а вот на способности Visual Studio — да. И если R# говорит, что есть ошибка, то она есть и сборки не произойдет. Не надо путать Hint, Suggestion, Warning и Error — все решарперовское, но вот только Error'ы, которые генерирует ReSharper вполне себе Visual Studio'йные.
                  • 0
                    Ой, картинка неверно загрузилась, в следующем комментарии.
                    • 0


                      Что я делаю не так? Всё прекрасно собирается, несмотря на ошибки, замеченные Resharper. Так было всегда на моей памяти — пользуюсь Resharper уже много лет.
                      • 0
                        Насчет Error — верно, он никак не может влиять на сборку. Однако можно попробовать использовать ReSharper Command line tools www.jetbrains.com/resharper/features/command-line.html, настроив там проверку на вышеуказанный Error, и встроить результаты анализа в билд скрипт, которые при наличии таких ошибок в отчете будет валить сборку.
                        • 0
                          Да, такая возможность была бы полезно, но у нас в компании не всем нравится решарпер и, думаю, было бы весьма сложно убедить их в том, что он нужен на билд-контроллере. А на своей машине я и так вижу результаты анализа, к тому же в моём коде нашлось всего одно такое место, и то не критичное.
                          • 0
                            Ну нравится/не нравится Решарпер — это скорее вопрос относящийся к IDE, command line tools не требует наличия решарпера у всех разработчиков, плюс к этому он бесплатный, насколько я знаю(надо уточнить у коллег)
              • +2
                Моё правило в разработке — методы поиска объектов не возвращают null, а генерируют исключение при отсутствии объекта.
                Если факт отсутствия нужного объекта допускается бизнес-логикой приложения, то метод поиска пишется в bool TryGet(T key, out value) стиле — в этом случае выражение if получается автоматически.
                Т.е. работаем по принципу Dictionary — можно попробовать нагло получить значение, если уверены в том, что оно обязано быть или попробовать получить с проверкой на наличие. При этом сам метод очевиден, понятен, не требует оборачивания в дополнительные классы.
                NullReference попадаются, но практически всегда это в данных, получаемых из сторонних компонентов или через ServiceReference.
                • +1
                  Весьма спорно, при поиске кидать исключение. Поиск подразумевает возможность получения пустого списка, а вот операция получения конкретного объекта — уже как раз может кидать исключение. Тоесть все методы в наименовании которых есть Search* или Find* — никогда не кинут исключение о том что ничего не нашли, а вот Get* — кинет. Но это имхо)
                  • 0
                    Уже после публикации сообщения заметил, что написал «поиск» вместо «получения». Понятно, что методы поиска, которые могут вернуть несколько объектов возвращают IEnumerable. На IEnumerable у меня есть свой Extension.

                    	public static bool AnySafe<T>(this IEnumerable<T> collection, Func<T,bool> predicate = null)
                    	{
                    		return collection != null && (predicate == null ? collection.Any() : collection.Any(predicate));
                    	}
                    


                    Если коллекция не была создана в текущем методе, а была получена откуда-то (т.е. я не уверен что она не Null), то я всегда пользуюсь AnySafe.
                    • 0
                      Моё имхо:

                      Для решения вопроса генерить или нет исключения, применяется простое правило: если метод не выполнил действие, которое его просили выполнить, то надо генерить исключение.

                      Если говорят «дай!» (get), а он не даёт, то генерится исключение.
                      Если говорят «найди!» (find) а он не находит, то генерится исключение.

                      Наверное вы путаете английские слова find (найти, найди!) и search (seek, look for), (искать, ищи!)

                      Короче, find должен или находить, или генерить исключение,
                      а search (seek, look for) должен искать и сообщать о результате поиска,
                      который может быть отрицательным (не нашёл) или положительным (нашёл, что нашёл).

                      Такого принципа последнее время придерживаются и в Microsoft (см. например Find из Linq).
                      • 0
                        Именно. Методы должны делать то, что следует из их названия — это путь к предсказуемости API.
                        Сколько я намучился с кодом предыдущих разработчиков, которые писали ядро системы. Они написали его так, что при явном поиске конкретного объекта по индексатору через this[short key] возвращается null, если объекта с key не существует. Хуже всего в этой ситуации то, что возвращенный null может стрельнуть в непредсказуемом месте, где-нибудь через десяток строк от места получения или даже в другом методе.
                        Например Calculate(..., Context[Key], ...). И в этом случае начинается увлекательный DEBUG.
                        • 0
                          Это не поиск по индексатору, а явное обращение к конкретному объекту через индексатор. По сути — GetByKey(short key).

                          PS. Еще по поводу поведения Find:
                          В LINQ Find — msdn.microsoft.com/en-us/library/x0b5b5bc.aspx
                          The first element that matches the conditions defined by the specified predicate, if found; otherwise, the default value for type T.
                          • 0
                            Согласен, неудачно выбранный термин. «Получение объектов по индексатору».
                            Хотя никто не мешает написать даже так:

                            		public IEnumerable<object> this[int key]
                            		{
                            			get
                            			{
                            


                            Что позволяет особо одаренным программистам реализовывать через индексаторы любую логику, которая придёт в голову.
                            • 0
                              Пример такой реализации — HttpContext ;)
                            • 0
                              otherwise, the default value for type T.

                              Удивлён. Поражён. Перепутал с First, который генерит исключение, если не нашёл ничего по заданному критерию, т.е. фактически выполняет поиск. Я его (First) всегда и использую для поиска…
                              • 0
                                Ну First подразумевает, что в коллекции есть хоть 1 элемент) А вот FirstOrDefault — нет)
                          • 0
                            Уж если апелировать к MS то в EF Find возвращает null: msdn.microsoft.com/en-us/library/gg696418(v=vs.113).aspx

                            При поиске(как Search так и Find) не гарантируется, что будет что-то найдено. При явном запросе — Get — вы априори считаете что такой объект уже есть, и отсутствие такового — исключение.
                      • +5
                        • +1
                          кортежей и т.п

                          А это не оно разве?
                          • 0
                            Конечно, это оно, да и Func<T, ...> тоже есть, как функциональный тип.

                            Но и то, и другое можно поудобнее сделать.

                            Например, поддержать анонимный функциональный тип вида (int -> string), автоматически приводимый к любому подходящему делегату, как Func<int, string> так и какому-нибудь sting MyCustomDelegate(int a).

                            То же и с кортежами:
                            Сейчас так:

                            var tuple = IntegerDivide(5, 3);
                            var div = tuple.Item1;
                            var mod = tuple.Item2; 
                            

                            А можно было бы так:
                            var (div, mod) = IntegerDivide(5, 3); 
                            
                            • 0
                              Есть анонимные типы, хотя с ними тоже в одну строку не получится, но всё же:

                              var tuple = IntegerDivide(5, 3);
                              var anon = new { div = tuple.Item1, mod = tuple.Item2 };

                              Хотя я не совсем понимаю зачем перекладывать значения в локальные переменные, если они уже лежат в самом Tuple. Разве что для повышения читабельности кода, но тогда Tuple лучше не использовать вообще, а завести мелкую структуру или класс.
                              • 0
                                Вот эти Item1… ItemN IMHO большой минус для Tuple. Без доки или изучения кода вызываемого метода фиг поймешь что в них.
                                • 0
                                  Потому и пользуюсь Tuple в случаях крайней нехватки времени или для совсем уж маловажных глубоко спрятанных private кусков.
                                • +1
                                  Только и разницы, что вместо одной строчки две. Никто не умрет, но суть немного затуманивается. Чем такого меньше, тем лучше.
                            • 0
                              Мне кажется удобнее использовать нультипы. Во всех враперах можно все равно запросить значние если его нет, и это ситуация, также порождает ексцепшен. Т.е. синтаксически ничем не отличается от проверки на null.
                              • 0
                                Нультипы — это Nullable? Их нельзя использовать с ссылочными типами, т.е. к текущей проблеме это не имеет отношения — Int или структура всё равно не может быть null.
                                • 0
                                  Нет, это тип твоего объекта но содержащий спец информацию, отладочную или пишуший лог. т.е. FindUser будет всегда находить объект, если юзера нет, то вернется спец объект типа User. И проверять нужно не на Null а на, например, User.Empty
                                  Вот например string.Epmty, Guid.Empty это оно и есть.
                                  • 0
                                    EventArgs.Empty и т.д. Пользовался, но не знал что это нультипами называется. Мне кажется что это не всегда применимо — типы бывают достаточно тяжелыми и жестко связанными с окружением. Ну и плюс никак не гарантируется компилятором, что некто будет этот нультип не то, что использовать, а даже знать о нём. Как проверял на null, так и будет проверять.
                                • +1
                                  Я так понял, что вы имеете в виду шаблон Null Object. Это вполне хорошее решение, когда оно на своем месте. Однако, на мой взгляд, на роль универсального избавителя от null оно не подходит. Этот шаблон относится к моделированию предметной области: если у нас есть семейство объектов (например, разные типы Начислений в финансовом приложении), и для них можно (для унификации обработки) говорить также об Отсутствующем Начислении (если это имеет смысл с точки зрения той модели, в которой мыслят пользователи и проектировщики приложения), то это хорошее место для применения Null Object.

                                  Однако, если у меня есть коллекция users и мне нужно сделать следующее:

                                  var defaultAdmin = ...;
                                  var admin = users.FirstMaybe(u => u.IsAdmin).OrElse(defaultAdmin); 
                                  


                                  То тут вводить NullUser, на мой взгляд, неуместно, так как он не будет представлять никакую существенную концепцию в модели предметной области.

                                  Что же касается возможности вызвать maybe.Value при отсутствии значения, она, хоть и приведет к исключению, все же является явной операцией. Вы прицеливаетесь и четко стреляете себе в ногу, сие есть ваше неотъемлемое право. Мы лишь гарантируем, чтобы пистолет не выстрелил без нажатия на курок :)
                                • +1
                                  А на столько ли страшен null, как его малюют? В нормально спроектированной системе null может вернуться только из поисковых запросов. Если активно используется linq, то .First() и .Single() решают проблему полностью. В других случаях, кроме как вредительством возвращение null не назовешь.

                                  Сам активно использую реализацию Maybe от mezastel, заметил, что кроме linq она у меня встречается только в вариантах, с linq или где нельзя поставить ??
                                  • –2
                                    По личному опыту поиска багов в чужом коде, могу сказать, что 90% NullReferenceException во время исполнения — результаты .First() и .Single().
                                    • +1
                                      Странный у Вас опыт — результатом First или Single не может быть null, разве что null был элементом массива, но тогда это не беда First или Single, а беда логики, анализирующей результаты поиска.
                                      • +1
                                        Точно. Недопроснулся. InvalidOperationException
                                        • +1
                                          Это совсем не то же самое, что NullReference, и за эту ошибку нужно точно бить по рукам — разработчик не понимает бизнес-логики своего приложения.
                                          Этими методами нужно пользоваться только тогда, когда уверен в наличии нужного элемента и когда его отсутствие свидетельствует только о каких-то серьезных проблемах, при которых продолжать дальнейшую работу с приложением не только бессмысленно, но и чревато нарушениями каких-то бизнес-процессов, поэтому все First желательно оборачивать в try...catch с генерацией осмысленной ошибки.
                                          • +1
                                            Либо оставить и не париться, если это очевидное место кода, например — вызов API. Скажем какой-нибудь
                                            class ProductController
                                            {
                                              //...
                                            
                                              public int Buy(int userId, int productId)
                                              {
                                                 this._uow.Users.Single(u => u.Id == userId);
                                                 //...
                                              }
                                            }
                                            


                                            Можно, конечно, лучше детализировать ошибку, но если это внутренние API и так сойдет.
                                            • +1
                                              Пользователя уже могли успеть удалить или перевести в архивное состояние. Если есть время поставить try...catch — лучше это сделать — потом сам себе спасибо скажешь.
                                              Но в целом согласен — иногда приходится делать и ...First().Action() просто потому что, если First ничего не вернул, то дальше и разговаривать не о чем, а обработчики писать некогда.
                                              Сейчас решил побаловаться монадами с проверкой на null — воткнул в пару мест по коду. Кажется ничего так получается — вполне читабельно и удобно.
                                            • 0
                                              я о том же
                                              • 0
                                                >все First желательно оборачивать в try...catch с генерацией осмысленной ошибки.
                                                А какую можно сгенерировать ошибку, более осмысленную чем InvalidOperation со стектрейсом, показывающим прямо на вызов First/Single?
                                                • +1
                                                  Например «Товара с кодом '100' нет в прайсе. Возможно он был удалён или ....». Не знаю каков у Вас опыт сопровождения продуктов, но InvalidOperation со стектрейсом для конечного пользователя — не просто китайская грамота — это как красная тряпка для быка. А InvalidOperation со стектрейсом аккуратненько уходит в лог в виде InnerException.
                                                  • 0
                                                    Да, и в одном методе может быть не один Linq.First, как разработчику узнать чего именно не оказалось?
                                                    • 0
                                                      По номеру строки, например.
                                                      • 0
                                                        В Release версии? Ну-ну — удачи.
                                                        • 0
                                                          Если приложение не с секретными какими технологиями, то я не вижу причин не класть pdb-шки в релизную версию.
                                                          • 0
                                                            У серьезного приложения котчами должны быть охвачены основные уязвимые места, особенно если конечный пользователь после этого сможет самостоятельно разобраться с её причиной или хотя бы понять что не так, вместо чтения ненужных ему стеков.
                                                            • +1
                                                              Идея сделать ошибки user-frendly — она вроде как правильная по-идее. Но на практике для юзера все равно сколько там страшных букв будет, а для программиста или админа — настоящая причина ошибки лучше, чем то, что там другие программисты подумали могло случиться.

                                                              Так что я в серьезных приложениях стараюсь делать только top-level перехват исключений для логирования, и общей устойчивости.

                                                              Кстати советую убедиться что у тебя логируются innerException-ы. Многие логгеры без дополнительных приседаний этого не делают, log4net, например.
                                        • 0
                                          Если так хочется строгости и не смущает Maybe, то можно изобрести Always:

                                          public interface IUserRepo 
                                          {
                                              Always<User> Get(int id);
                                              Maybe<User> Find(int id);
                                          }
                                          
                                          • 0
                                            Проблему того, то что в C#-е зачем-то придумали иметь null-значение для всех value-типов — никакие Maybe не решают. Они ее лишь усугубляют — вместо одного null, получается три: null, Nothing и Maybe(null).

                                            От всяких LINQ и extension-методов плюсы наоборот видны хорошо. Но их можно делать и поверх object-ов.
                                            • +1
                                              При условии, что:
                                              • null запрещен (одним из вышеперечисленных способов) и
                                              • выбранная реализация Maybe не разрешает Maybe(null) (или приравнивает его Nothing),

                                              ваш первый тезис становится неверным.
                                              • 0
                                                Я не вижу способа запретить null совсем — он все равно будет прилетать из библиотек, будет в полях пока их не инициализировали (или забыли). Любые штатные средства — XmlSerializer или какой-нибудь EF не поймут Maybe, и там тоже придется как-то оборачивать null туда-сюда. Null часто нужен в алгоритмах как начальное или промежуточное состояние какой-нибудь переменной.

                                                В результате Maybe принесет больше разброда и WTF-а, нежели пользы.

                                                Думаю прагматичным подходом будет договориться что может возвращать null, а что — нет. И как-то с этим жить — тот же LINQ-стайл только поверх object-а, например, может быть в тему.
                                                • 0
                                                  Вполне могу согласиться с таким подходом, лишь бы команда его принимала и использовала единоообразно.

                                                  Хотя, при желании, вызовы библиотек прекрасно оборачиваются ToMaybe, об использовании неинициализированных переменных и полей предупреждает компилятор или решарпер, а в алгоритмах вполне можно как обходиться без null, так и «разрешить» его локальное использование особым способом (зависящим от того, каким методом null был запрещен; например, специальным комментарием).
                                            • 0
                                              Я-то по названию решил, что новая статья про неявную динамическую типизацию в шарпе в некоторых случаях, а получилась обычная пропаганда монад…
                                              • 0
                                                Вместо Get и Find, я бы предложил Get и TryToGet аналогично Parse :-)

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.