Расширение оператора С# 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");

Другими словами - для чего вам нужны лямбды?