Как стать автором
Обновить

Комментарии 63

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

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

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


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

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

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

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

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

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

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


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

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

При поиске(как Search так и Find) не гарантируется, что будет что-то найдено. При явном запросе — Get — вы априори считаете что такой объект уже есть, и отсутствие такового — исключение.
Конечно, это оно, да и 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); 
Есть анонимные типы, хотя с ними тоже в одну строку не получится, но всё же:

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

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

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

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


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

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

Сам активно использую реализацию Maybe от mezastel, заметил, что кроме linq она у меня встречается только в вариантах, с linq или где нельзя поставить ??
По личному опыту поиска багов в чужом коде, могу сказать, что 90% NullReferenceException во время исполнения — результаты .First() и .Single().
Странный у Вас опыт — результатом First или Single не может быть null, разве что null был элементом массива, но тогда это не беда First или Single, а беда логики, анализирующей результаты поиска.
Точно. Недопроснулся. InvalidOperationException
Это совсем не то же самое, что NullReference, и за эту ошибку нужно точно бить по рукам — разработчик не понимает бизнес-логики своего приложения.
Этими методами нужно пользоваться только тогда, когда уверен в наличии нужного элемента и когда его отсутствие свидетельствует только о каких-то серьезных проблемах, при которых продолжать дальнейшую работу с приложением не только бессмысленно, но и чревато нарушениями каких-то бизнес-процессов, поэтому все First желательно оборачивать в try...catch с генерацией осмысленной ошибки.
Либо оставить и не париться, если это очевидное место кода, например — вызов API. Скажем какой-нибудь
class ProductController
{
  //...

  public int Buy(int userId, int productId)
  {
     this._uow.Users.Single(u => u.Id == userId);
     //...
  }
}


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

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

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

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

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

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

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

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

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

Публикации

Истории