Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 17.06.2014 | нет комментариев

Задача

Мы привыкли говорить о языках как бы 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);
}

Сразу оговоримся, что 1-й способ все еще может воротить 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
WeeGems.Options, struct есть есть минимальное нет нет Также есть другие функциональные полезности: мемоизация, частичное использование функций
bitbucket.org/MattDavey/weegems

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

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

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

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

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB