Расширение оператора объединения C # - PullRequest
13 голосов
/ 20 марта 2009

Прежде чем я объясню, что я хочу сделать, если вы посмотрите на следующий код, поймете ли вы, что он должен делать? (обновлено - см. Ниже)

Console.WriteLine(
  Coalesce.UntilNull(getSomeFoo(), f => f.Value) ?? "default value");

В C # уже есть оператор объединения нулей, который довольно хорошо работает на простых объектах, но не помогает, если вам нужен доступ к члену этого объекта.

1008 * Е.Г. *

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: убрать скобки, как предложено Марком

Обновление

Мне очень понравились предложения 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");

Ответы [ 8 ]

11 голосов
/ 20 марта 2009

Да, я бы это понял. Да, объединение это хорошее имя. Да, было бы лучше, если бы C # имел нулевой безопасный оператор разыменования, такой как Groovy и некоторые другие языки:)

Обновление

C # 6 имеет такой оператор - нулевой условный оператор, ?. Например:

var street = customer?.PrimaryAddress?.Street;

Используйте его вместе с исходным оператором слияния нулей, если вам все еще требуется значение по умолчанию. Например:

var street = customer?.PrimaryAddress?.Street ?? "(no address given)";

Или, исходя из исходного кода в вопросе:

Console.WriteLine(getSomeFoo()?.Value ?? "default"); 

Конечно, отмечая, что предоставление значения по умолчанию этим способом работает только в том случае, если можно использовать это значение по умолчанию, даже когда конечное значение свойства доступно, но по какой-то причине установлено в ноль.

Результат выражения x?.y равен null, если x оценивается как ноль; в противном случае это результат x.y. О, и вы также можете использовать его для вызова условного метода:

possiblyNull?.SomeMethod();
7 голосов
/ 20 марта 2009

Меня это уже смутило ... обычно вы думаете о слиянии, действующем на его значениях - я представлял себе, что будут возвращены первые ненулевые значения (f) => f.Value и "default value", что не дело не в этом (нулевой тест в исходном экземпляре).

Обратите внимание, что было бы яснее без скобок?

f => f.Value

То, что вы на самом деле делаете, похоже на Select, поэтому что-то вроде SafeSelect будет хорошим именем, ИМО (но, может быть, не совсем это ...).

Или даже просто Dereference, если имя аргумента (и т. Д.) Проясняет, для чего нужен второй аргумент.

3 голосов
/ 20 марта 2009

Это также можно легко расширить:

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
2 голосов
/ 20 марта 2009

Кажется достаточно читабельным, хотя все еще немного неуклюжим.

Кажется, что это прекрасная возможность для реализации шаблона нулевого объекта .

Рассмотрим:

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 вместо нуля. Это требует немного дополнительной мысли, но обычно делает код более приятным.

Обновление в ответ на комментарии:

Допустим, вы не контролируете 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);
1 голос
/ 02 февраля 2010

Отличный пост! Я обновил ваш код для поддержки возврата 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>();
        }
    }
1 голос
/ 20 марта 2009

Если вы часто используете его в кодовой базе, я думаю, что это хорошо, так как это не так сложно понять при первом чтении и уменьшает размер кода - поэтому помогает мне видеть древесину с деревьев.

Однако, если он используется только 1 или 2 раза, я думаю, что "in line" , если будет лучше, так как я не вижу смысла в значении "if" в первый раз Я вижу это.

Под "в строке" if - я имею в виду нормальное выражение if, которое не было скрыто в отдельном методе.

0 голосов
/ 21 июля 2015

Шесть лет спустя и Нулевые операторы здесь:

Иногда код имеет тенденцию немного тонуть при проверке нуля. Нулевой условный оператор позволяет получить доступ только к элементам и элементам когда получатель не равен 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;

За исключением того, что клиенты оцениваются только один раз. Ни один из членов доступы, доступы к элементам и вызовы сразу после? выполняются, если у клиентов нет ненулевого значения.

Конечно, нулевые условные операторы сами могут быть связаны В случае необходимости проверки на ноль более чем один раз в цепочке:

int? first = customers?[0].Orders?.Count();

Обратите внимание, что вызов (список аргументов в скобках) не может сразу следовать? оператор - это привело бы к слишком многим синтаксические неясности. Таким образом, простой способ вызова делегировать только если он там не работает. Тем не менее, вы можете сделать это через метод Invoke для делегата:

if (predicate?.Invoke(e) ?? false) { … }

Мы ожидаем, что очень распространенное использование этого шаблона будет для инициирующие события:

PropertyChanged?.Invoke(this, args);

Это простой и поточно-ориентированный способ проверки на ноль перед вами. вызвать событие. Причиной многопоточности является то, что оценивает левую часть только один раз и сохраняет ее во временном переменная.

0 голосов
/ 26 января 2010

Почему бы не записать обычную функцию объединения, тогда вы можете использовать ее следующим образом:

coalesce(something, something_else, "default");

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

...