Зло использование Модификации Maybe и методов расширения в С#?
изменить 2015 Этот вопрос и его ответы больше не актуальны. Он был задан до появления С# 6, который имеет нулевой передатчик (?.), Который устраняет хакерские обходные пути, обсуждаемые в этом вопросе и последующих ответах. Начиная с 2015 года, в С# вы должны теперь использовать Form.ActiveForm?.ActiveControl?.Name.
Я думал о проблеме распространения NULL в .NET, что часто приводит к уродливому, повторяемому коду, подобному этому:
Попытка # 1 обычный код:
string activeControlName = null;
var activeForm = Form.ActiveForm;
if (activeForm != null)
{
var activeControl = activeForm.ActiveControl;
if(activeControl != null)
{
activeControlname = activeControl.Name;
}
}
Было несколько обсуждений в StackOverflow о возможностях <T> monad, или используя какой-либо метод расширения "если не нуль":
Попытка # 2, метод расширения:
// Usage:
var activeControlName = Form.ActiveForm
.IfNotNull(form => form.ActiveControl)
.IfNotNull(control => control.Name);
// Definition:
public static TReturn IfNotNull<TReturn, T>(T instance, Func<T, TReturn> getter)
where T : class
{
if (instance != null ) return getter(instance);
return null;
}
Я думаю, что это лучше, однако, есть немного синтаксической беспорядка с повторяющимися "IfNotNull" и лямбдами. Я сейчас рассматриваю этот дизайн:
Попытка # 3, Maybe <T> с расширением
// Usage:
var activeControlName = (from window in Form.ActiveForm.Maybe()
from control in window.ActiveControl.Maybe()
select control.Name).FirstOrDefault();
// Definition:
public struct Maybe<T> : IEnumerable<T>
where T : class
{
private readonly T instance;
public Maybe(T instance)
{
this.instance = instance;
}
public T Value
{
get { return instance; }
}
public IEnumerator<T> GetEnumerator()
{
return Enumerable.Repeat(instance, instance == null ? 0 : 1).GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
public static class MaybeExtensions
{
public static Maybe<T> Maybe<T>(this T instance)
where T : class
{
return new Maybe<T>(instance);
}
}
Мой вопрос: это злое злоупотребление методами расширения? Это лучше, чем старые обычные нулевые проверки?
Ответы
Ответ 1
Интересно, что так много людей самостоятельно выбирают имя IfNotNull
, для этого в С# - это должно быть самое разумное имя!:)
Самое раннее, что я нашел на SO: Возможные подводные камни использования этой (основанной на расширении) стенографии
Мой (в незнании выше): Труба вперед в С#
Еще один недавний пример: Как проверить нули в глубоком лямбда-выражении?
Существует несколько причин, по которым метод расширения IfNotNull
может быть непопулярным.
-
Некоторые люди категорически заявляют, что метод расширения должен генерировать исключение, если его параметр this
равен null
. Я не согласен с тем, что имя метода дает понять.
-
Расширения, которые применяются слишком широко, будут иметь тенденцию загромождать меню автоматического завершения. Этого можно избежать путем правильного использования пространств имен, чтобы они не раздражали людей, которые их не хотят.
Я также играл с подходом IEnumerable
, также как и эксперимент, чтобы увидеть, сколько вещей я мог бы перекрутить, чтобы соответствовать ключевым словам Linq, но я думаю, что конечный результат менее читабельен, чем цепочка IfNotNull
или необработанный императивный код.
Я закончил с простым автономным классом Maybe
с одним статическим методом (а не с методом расширения), и это работает очень хорошо для меня. Но затем я работаю с небольшой командой, и мой следующий старший коллега интересуется функциональным программированием и лямбдами и т.д., Поэтому он не откладывается им.
Ответ 2
Как я поклонник методов расширения, я не думаю, что это действительно полезно. У вас все еще есть повторение выражений (в монадической версии), и это просто означает, что вы должны объяснить Maybe
всем. В этом случае добавленная кривая обучения не имеет достаточной пользы.
Версия IfNotNull
по крайней мере справляется с тем, чтобы избежать повторения, но я думаю, что она все еще слишком длинна, не будучи на самом деле более ясной.
Возможно, однажды мы получим нуль-безопасный оператор разыменования...
Так же, как и в стороне, мой любимый метод расширения полу-зла:
public static void ThrowIfNull<T>(this T value, string name) where T : class
{
if (value == null)
{
throw new ArgumentNullException(name);
}
}
Это позволяет вам включить это:
void Foo(string x, string y)
{
if (x == null)
{
throw new ArgumentNullException(nameof(x));
}
if (y == null)
{
throw new ArgumentNullException(nameof(y));
}
...
}
в
void Foo(string x, string y)
{
x.ThrowIfNull(nameof(x));
y.ThrowIfNull(nameof(y));
...
}
Там все еще неприятное повторение имени параметра, но по крайней мере оно более аккуратное. Конечно, в .NET 4.0 я бы использовал Code Contracts, о чем я сейчас хочу писать... Qaru - отличное предотвращение работы;)
Ответ 3
Если вы хотите, чтобы метод расширения уменьшал вложенное, если у вас есть, вы можете попробовать что-то вроде этого:
public static object GetProperty(this object o, Type t, string p)
{
if (o != null)
{
PropertyInfo pi = t.GetProperty(p);
if (pi != null)
{
return pi.GetValue(o, null);
}
return null;
}
return null;
}
поэтому в коде, который вы только что сделали:
string activeControlName = (Form.ActiveForm as object)
.GetProperty(typeof(Form),"ActiveControl")
.GetProperty(typeof(Control),"Name");
Я не знаю, хочу ли я часто использовать его из-за медленности отражения, и я действительно не думаю, что это намного лучше, чем альтернатива, но он должен работать, независимо от того, null по пути...
(Примечание: я мог бы перепутать эти типы):)
Ответ 4
В случае, если вы имеете дело с С# 6.0/VS 2015 и выше, теперь у них есть встроенное решение для нулевого распространения:
string ans = nullableString?.Length.ToString(); // null if nullableString == null, otherwise the number of characters as a string.
Ответ 5
Исходный образец работает и проще всего прочитать с первого взгляда. Есть ли необходимость улучшить это?
Ответ 6
Решение IfNotNull является лучшим (пока команда С# не предоставит нам нуль-безопасный оператор разыменования, то есть).
Ответ 7
Я не слишком без ума от решения. Что не так с оригинальной версией оригинала:
string activeControlName = null;
if (Form.ActiveForm != null)
if (Form.ActiveForm.ActivControl != null) activeControlname = activeControl.Name;
Если это не так, то я бы посмотрел на запись объекта NotNullChain или FluentNotNull, чем можно связать несколько не нулевых тестов в строке. Я согласен с тем, что метод расширения IfNotNull, действующий на нуль, кажется немного странным - хотя методы расширения - это просто синтаксический сахар.
Я думаю, что ответ Марка Синовека мог бы сделать общий.
ИМХО, я думаю, что основная команда С# должна смотреть на эту "проблему", хотя я думаю, что есть большие проблемы, которые нужно решать.
Ответ 8
Конечно, исходный 2-вложенный IF намного читабельнее других. Но предлагая вам более широко решить проблему, вот еще одно решение:
try
{
var activeForm = Form.ActiveForm; assumeIsNotNull(activeForm);
var activeControl = activeForm.ActiveControl; assumeIsNotNull(activeControl);
var activeControlname = activeControl.Name;
}
catch (AssumptionChainFailed)
{
}
где
class AssumptionChainFailed : Exception { }
void assumeIsNotNull(object obj)
{
if (obj == null) throw new AssumptionChainFailed();
}