扩展C#Coalesce运算符

时间:2009-03-20 10:05:15

标签: c# extension-methods

在我解释我想要做什么之前,如果你看一下下面的代码,你会理解它应该做什么吗? (更新 - 见下文)

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

C#已经有一个null-coalescing运算符,它在简单对象上运行得很好,但如果你需要访问该对象的一个​​成员则无效。

E.g。

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

建议的括号

更新

我真的很喜欢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 个答案:

答案 0 :(得分:11)

是的,我会理解的。是的,合并是个好名字。是的,如果C#有一个像Groovy和其他一些语言的空安全解除引用运算符会更好:)

<强>更新

C#6有这样一个运算符 - 空条件运算符?.例如:

var street = customer?.PrimaryAddress?.Street;

如果您仍需要默认值,请将其与原始的空合并运算符结合使用。例如:

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

或者,根据问题中的原始代码:

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

当然注意到,只有当最终属性值可用但由于某种原因设置为null时,以这种方式提供默认值才能正常工作。

如果x?.y求值为null,则表达式null的结果为x;否则它是x.y的结果。哦,您也可以将它用于条件方法调用:

possiblyNull?.SomeMethod();

答案 1 :(得分:7)

它已经让我困惑了......通常,你会想到合并行为的 - 我想象(f) => f.Value"default value"的第一个非空返回,但情况并非如此(空测试在原始实例上)。

注意没有括号会更清楚吗?

f => f.Value

你实际上做的是和Select类似 - 所以 SafeSelect这样的东西就是一个好名字,IMO(但可能并不完全是......)。

甚至只是Dereference,只要参数名称(等)明确了第二个arg的用途。

答案 2 :(得分:3)

这也很容易扩展:

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

答案 3 :(得分:2)

看起来足够可读,虽然它仍然有点笨拙。

这似乎是实施null object pattern的绝佳机会。

考虑:

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; } }
}

然后从getSomeFoo()返回NullFoo.Instance。如果你不控制getSomeFoo(),你仍然可以选择这样做:

Console.WriteLine((getSomeFoo() ?? NullFoo.Instance).Value);

答案 4 :(得分:1)

如果你经常在代码库中使用它,我认为它很好,因为在第一次阅读时不太难理解并减少代码的大小 - 所以帮助我从树上看到木材。

但是,如果仅使用1或2次,我认为“在线”如果会更好,因为我没有想到“if”第一个的含义我看到的时候了。

通过“in line”if - 我的意思是一个普通的if语句,它没有隐藏在一个单独的方法中。

答案 5 :(得分:1)

很棒的帖子!我更新了代码以支持返回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>();
        }
    }

答案 6 :(得分:0)

为什么不写下正常的合并函数,然后就可以这样使用它了:

coalesce(something, something_else, "default");

换句话说 - 你需要什么lambdas?

答案 7 :(得分:0)

六年后,Null-conditional operators在这里:

  

有时代码往往会在null检查中淹没一点。该   null-conditional运算符只允许您访问成员和元素   当接收者不为null时,否则提供null结果:

int? length = customers?.Length; // null if customers is null Customer
first = customers?[0];  // null if customers is null
     

空条件运算符可以方便地与   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的简单且线程安全的方法   触发事件。它的线程安全的原因是该功能   仅评估左侧一次,并将其保持在临时状态   变量