Расширение оператора С# Coalesce
Прежде чем я объясню, что я хочу сделать, если вы посмотрите на следующий код, вы бы поняли, что он должен делать? (обновлено - см. ниже)
Console.WriteLine(
Coalesce.UntilNull(getSomeFoo(), f => f.Value) ?? "default value");
У С# уже есть оператор с косой связью, который хорошо работает на простых объектах, но не помогает, если вам нужно получить доступ к члену этого объекта.
например.
Console.WriteLine(getSomeString()??"default");
работает очень хорошо, но вам это не поможет:
public class Foo
{
public Foo(string value) { Value=value; }
public string Value { get; private set; }
}
// this will obviously fail if null was returned
Console.WriteLine(getSomeFoo().Value??"default");
// this was the intention
Foo foo=getSomeFoo();
Console.WriteLine(foo!=null?foo.Value:"default");
Поскольку это то, что я часто встречаю, я думал об использовании метода расширения (старая версия):
public static class Extension
{
public static TResult Coalesce<T, TResult>(this T obj, Func<T, TResult> func, TResult defaultValue)
{
if (obj!=null) return func(obj);
else return defaultValue;
}
public static TResult Coalesce<T, TResult>(this T obj, Func<T, TResult> func, Func<TResult> defaultFunc)
{
if (obj!=null) return func(obj);
else return defaultFunc();
}
}
Что позволяет мне написать:
Console.WriteLine(getSomeFoo().Coalesce(f => f.Value, "default value"));
Итак, считаете ли вы, что этот код доступен для чтения? Является ли Coalesce хорошим именем?
Изменить 1: удалили скобки, как было предложено Marc
Update
Мне очень понравились предложения от lassevk и отзывы Groo. Поэтому я добавил перегрузки и не реализовал его как метод расширения. Я также решил, что defaultValue избыточен, потому что вы можете просто использовать существующие? для этого.
Это измененный класс:
public static class Coalesce
{
public static TResult UntilNull<T, TResult>(T obj, Func<T, TResult> func) where TResult : class
{
if (obj!=null) return func(obj);
else return null;
}
public static TResult UntilNull<T1, T2, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, TResult> func2) where TResult : class
{
if (obj!=null) return UntilNull(func1(obj), func2);
else return null;
}
public static TResult UntilNull<T1, T2, T3, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, TResult> func3) where TResult : class
{
if (obj!=null) return UntilNull(func1(obj), func2, func3);
else return null;
}
public static TResult UntilNull<T1, T2, T3, T4, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, T4> func3, Func<T4, TResult> func4) where TResult : class
{
if (obj!=null) return UntilNull(func1(obj), func2, func3, func4);
else return null;
}
}
Использование образца:
Console.WriteLine(
Coalesce.UntilNull(getSomeFoo(), f => f.Value) ?? "default value");
Еще один пример:
public class Bar
{
public Bar Child { get; set; }
public Foo Foo { get; set; }
}
Bar bar=new Bar { Child=new Bar { Foo=new Foo("value") } };
// prints "value":
Console.WriteLine(
Coalesce.UntilNull(bar, b => b.Child, b => b.Foo, f => f.Value) ?? "null");
// prints "null":
Console.WriteLine(
Coalesce.UntilNull(bar, b => b.Foo, f => f.Value) ?? "null");
Ответы
Ответ 1
Шесть лет спустя и Операторы с нулевым условием находятся здесь:
Иногда код имеет тенденцию заглушить бит при нулевой проверке. null-условный оператор позволяет вам только доступ к элементам и элементам когда приемник не равен-null, в противном случае будет иметь нулевой результат:
int? length = customers?.Length; // null if customers is null Customer
first = customers?[0]; // null if customers is null
Оператор с нулевым условием удобно использовать вместе с нулевой коалесцирующий оператор:
int length = customers?.Length ?? 0; // 0 if customers is null
Оператор с нулевым условием проявляет короткое замыкание, где сразу следующая цепочка доступа к элементам, элемент обращения и вызовы будут выполняться только в том случае, если оригинал приемник не был нулевым:
int? first = customers?[0].Orders.Count();
Этот пример по существу эквивалентен:
int? first = (customers != null) ? customers[0].Orders.Count() : null;
За исключением того, что клиенты оцениваются только один раз. Ни один из участников доступа, доступа к элементам и вызовов, непосредственно следующих за? выполняются, если у клиентов нет ненулевого значения.
Конечно, нуль-условные операторы сами могут быть связаны цепью, в В этом случае необходимо проверить null в цепочке не более одного раза:
int? first = customers?[0].Orders?.Count();
Обратите внимание, что вызов (список аргументов в скобках) не может немедленно следовать? оператора - это приведет к слишком большому количеству синтаксические двусмысленности. Таким образом, простой способ делегат, только если его там не работает. Однако вы можете сделать это через метод Invoke для делегата:
if (predicate?.Invoke(e) ?? false) { … }
Мы ожидаем, что очень распространенное использование этого шаблона будет триггерные события:
PropertyChanged?.Invoke(this, args);
Это простой и потокобезопасный способ проверить наличие null перед вами инициировать событие. Причина его поточно-безопасной в том, что функция оценивает левую сторону только один раз и удерживает ее во временном переменная.
Ответ 2
Да, я бы это понял. Да, coalesce - хорошее имя. Да, было бы лучше, если бы у С# был нуль-безопасный оператор разыменования, например Groovy и некоторые другие языки:)
Обновление
С# 6 имеет такой оператор - нулевой условный оператор, ?.
Например:
var street = customer?.PrimaryAddress?.Street;
Используйте его в сочетании с исходным оператором с нулевым коалесцированием, если по-прежнему требуется значение по умолчанию. Например:
var street = customer?.PrimaryAddress?.Street ?? "(no address given)";
Или, основываясь на исходном коде в вопросе:
Console.WriteLine(getSomeFoo()?.Value ?? "default");
Отмечая, конечно, что предоставление дефолта таким образом работает, только если это нормально использовать это значение по умолчанию, даже когда конечное значение свойства доступно, но по какой-то причине оно имеет значение null.
Результат выражения x?.y
равен null
, если x
имеет значение null; в противном случае это результат x.y
. О, и вы можете использовать его для вызова условного метода тоже:
possiblyNull?.SomeMethod();
Ответ 3
Это меня уже смутило... как правило, вы думаете о совместном действии на его значениях - я думал, что будет возвращено первое ненулевое значение (f) => f.Value
и "default value"
, что не так ( null test находится в исходном экземпляре).
Заметьте, что было бы проще, без скобок?
f => f.Value
То, что вы на самом деле делаете, похоже на Select
- поэтому что-то вроде SafeSelect
будет хорошим именем, IMO (но, возможно, не совсем так).
Или даже просто Dereference
, если имя аргумента (и т.д.) дает понять, для чего нужен второй аргумент.
Ответ 4
Это также можно было бы легко расширить:
public static TResult Coalesce<T, TResult>(this T obj, Func<T, TResult> func, TResult defaultValue)
{
if (obj == null)
return defaultValue;
return func(obj);
}
public static TResult Coalesce<T1, T2, TResult>(this T1 obj, Func<T1, T2> func1, Func<T2, TResult> func2, TResult defaultValue)
{
if (obj == null)
return defaultValue;
T2 obj2 = func1(obj);
if (obj2 == null)
return defaultValue;
return func2(obj2);
}
public static TResult Coalesce<T1, T2, T3, TResult>(this T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, TResult> func3, TResult defaultValue)
{
if (obj == null)
return defaultValue;
T2 obj2 = func1(obj);
if (obj2 == null)
return defaultValue;
T3 obj3 = func2(obj2);
if (obj3 == null)
return defaultValue;
return func3(obj3);
}
public static TResult Coalesce<T1, T2, T3, T4, TResult>(this T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, T4> func3, Func<T4, TResult> func4, TResult defaultValue)
{
if (obj == null)
return defaultValue;
T2 obj2 = func1(obj);
if (obj2 == null)
return defaultValue;
T3 obj3 = func2(obj2);
if (obj3 == null)
return defaultValue;
T4 obj4 = func3(obj3);
if (obj4 == null)
return defaultValue;
return func4(obj4);
}
Что можно использовать следующим образом:
BinaryTreeNode node = LocateNode(someKey);
BinaryTreeNode grandFatherNode = node.Coalesce(n1 => n1.Parent, n2 => n2.Parent, null);
Что заменит:
BinaryTreeNode grandFatherNode = node.Parent.Parent; // or null if none
Ответ 5
Кажется читаемым достаточно, хотя он все еще немного неуклюжий.
Это кажется прекрасной возможностью реализовать нулевой шаблон объекта.
Рассмотрим:
public class Foo
{
public Foo(string value) { Value=value; }
public string Value { get; private set; }
private static Foo nullFoo = new Foo("default value");
public static Foo NullFoo { get { return nullFoo; } }
}
Затем getSomeFoo() возвращает Foo.NullFoo вместо null. Это требует немного дополнительной мысли, но, как правило, делает более приятным код.
Обновление в ответ на комментарии:
Скажем, вы не контролируете Foo, вы все еще можете (часто) это делать (что больше того, как вы хотели бы реализовать его независимо):
public class NullFoo : Foo
{
private NullFoo() : base("default value") { }
private static NullFoo instance = new NullFoo();
public static Foo Instance { get { return instance; } }
}
Затем верните NullFoo.Instance из getSomeFoo(). Если вы не контролируете getSomeFoo(), у вас еще есть возможность сделать это:
Console.WriteLine((getSomeFoo() ?? NullFoo.Instance).Value);
Ответ 6
Если вы часто используете его в базе кода, я думаю, что это хорошо, поскольку его не так сложно понять при первом чтении и уменьшить размер кода - так помогает мне видеть дерево с деревьев.
Однако, если используется только 1 или 2 раза, я думаю, что "в строке", если будет лучше, поскольку я не вижу, чтобы думать о значении "если" в первый раз, когда я его вижу.
Под "в строке", если - я имею в виду нормальный оператор if, который не скрывается в отдельном методе.
Ответ 7
Отличный пост! Я обновил ваш код, чтобы поддерживать возврат Nullable, потому что Nullable реализован как структура.
public static class Coalesce
{
public static TResult UntilNull<T, TResult>(T obj, Func<T, TResult> func) where TResult : class
{
if (obj != null) return func(obj);
else return null;
}
public static TResult UntilNull<T1, T2, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, TResult> func2) where TResult : class
{
if (obj != null) return UntilNull(func1(obj), func2);
else return null;
}
public static TResult UntilNull<T1, T2, T3, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, TResult> func3) where TResult : class
{
if (obj != null) return UntilNull(func1(obj), func2, func3);
else return null;
}
public static TResult UntilNull<T1, T2, T3, T4, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, T4> func3, Func<T4, TResult> func4) where TResult : class
{
if (obj != null) return UntilNull(func1(obj), func2, func3, func4);
else return null;
}
public static Nullable<TResult> UntilNull<T, TResult>(T obj, Func<T, Nullable<TResult>> func) where TResult : struct
{
if (obj != null) return func(obj);
else return new Nullable<TResult>();
}
public static Nullable<TResult> UntilNull<T1, T2, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, Nullable<TResult>> func2) where TResult : struct
{
if (obj != null) return UntilNull(func1(obj), func2);
else return new Nullable<TResult>();
}
public static Nullable<TResult> UntilNull<T1, T2, T3, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, Nullable<TResult>> func3) where TResult : struct
{
if (obj != null) return UntilNull(func1(obj), func2, func3);
else return new Nullable<TResult>();
}
public static Nullable<TResult> UntilNull<T1, T2, T3, T4, TResult>(T1 obj, Func<T1, T2> func1, Func<T2, T3> func2, Func<T3, T4> func3, Func<T4, Nullable<TResult>> func4) where TResult : struct
{
if (obj != null) return UntilNull(func1(obj), func2, func3, func4);
else return new Nullable<TResult>();
}
}
Ответ 8
Почему бы не записать нормальную функцию coalesce, тогда вы можете использовать ее следующим образом:
coalesce(something, something_else, "default");
Другими словами - для чего вам нужны лямбды?