扩展C#的Coalesce运算符

14

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

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

C# 已经有了一个空值合并运算符,对于简单对象已经有很好的效果,但是如果您需要访问该对象的成员,则无法使用该运算符。

例如:

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");

1
哇!我知道扩展方法基本上是“thiscall”静态方法,但我仍然认为当在空实例上调用时它们会抛出Null引用异常(因为非虚拟实例方法实际上也是这样工作的)...不过无论如何,这样做更有意义。 - vgru
谢谢您的评论!这正是我正在寻找的 - 或者说不想寻找的 ;) API 应该遵循“最小惊奇原则” - 所以我猜使用扩展方法不是一个好主意 - 我会重新考虑我的设计! - laktak
看看我的更新 - C# 6 可能有你想要的 :) - Jon Skeet
谢谢,我已经在这里偶然发现了它 https://github.com/dotnet/roslyn/wiki/New-Language-Features-in-C%23-6#null-conditional-operators ;) - laktak
8个回答

11

是的,我会理解它。是的,coalesce是一个好名字。如果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的结果如果x的值为null,则为null;否则它将是x.y的结果。噢,而且您还可以将其用于条件方法调用:

possiblyNull?.SomeMethod();

7

这已经让我感到困惑了...通常,我们认为coalesce是作用于它的 - 我想象中的是第一个非空的(f) => f.Value"默认值"会被返回,但实际情况并非如此(null测试在原始实例上进行)。

注意,如果去掉括号会更清晰吗?

f => f.Value

你实际上正在做类似于“Select”的事情,因此我认为“SafeSelect”之类的名称会很好,但可能不完全是那样。甚至可以简单地使用“Dereference”,只要参数名称等清楚第二个参数的用途即可。

感谢-已删除括号。我认为SafeSelect会建议一个枚举类型,但它并不是...在Dereference时我不确定...最难的部分始终是给事物命名 :) - laktak
我必须同意Marc的观点,你的扩展方法在代码示例中并不像“??”运算符那样工作。 - Samuel

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

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

这迫使不可变性,尽管(至少是冰棒不可变性),并且与“default(T)”等不兼容。此外,它更普遍地假定您控制所涉及的类型(这并不总是正确的)。 - Marc Gravell

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

1

如果在代码库中经常使用它,我认为它很好,因为第一次阅读时不会太难理解,还可以减小代码的大小——这有助于我看到整体而不是细节。

然而,如果只用1或2次,我认为“内联”if更好,因为我第一次看到它时不需要思考“if”的含义。

所谓“内联”if——是指一个普通的if语句,没有隐藏在单独的方法中。


确实如此;有些情况下只有“内联if”才能发挥作用,比如在为LINQ-to-[某个数据库]构建lambda表达式时。 - Marc Gravell
你是说“内联if”的话,是指三元运算符吗? - laktak
抱歉,我只是指一个普通的“if”语句,并没有隐藏在一个单独的方法中。 - Ian Ringrose

0
为什么不编写一个普通的合并函数,然后您可以像这样使用它:
coalesce(something, something_else, "default");

换句话说——你需要lambda做什么?

他需要使用lambda表达式,因为something_elsesomething的一个属性。将属性解引用包装在lambda表达式中,可以延迟评估它,直到他知道这样做是安全的。 - Sean Devlin
1
这是错误的方法,因为你把那些应该是黑盒的原始操作暴露给了外部。如果你想要懒加载,只需将属性包装在懒加载类中,并将此类混合到方法中(例如 coalesce)。 否则,你最终会得到 coalesce、coalesce_lazy 等等。 - greenoldman

0

六年后,Null-conditional operators终于来了:

Sometimes code tends to drown a bit in null-checking. The null-conditional operator lets you access members and elements only when the receiver is not-null, providing a null result otherwise:

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

The null-conditional operator is conveniently used together with the null coalescing operator ??:

int length = customers?.Length ?? 0; // 0 if customers is null

The null-conditional operator exhibits short-circuiting behavior, where an immediately following chain of member accesses, element accesses and invocations will only be executed if the original receiver was not null:

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

This example is essentially equivalent to:

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

Except that customers is only evaluated once. None of the member accesses, element accesses and invocations immediately following the ? are executed unless customers has a non-null value.

Of course null-conditional operators can themselves be chained, in case there is a need to check for null more than once in a chain:

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

Note that an invocation (a parenthesized argument list) cannot immediately follow the ? operator – that would lead to too many syntactic ambiguities. Thus, the straightforward way of calling a delegate only if it’s there does not work. However, you can do it via the Invoke method on the delegate:

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

We expect that a very common use of this pattern will be for triggering events:

PropertyChanged?.Invoke(this, args);

This is an easy and thread-safe way to check for null before you trigger an event. The reason it’s thread-safe is that the feature evaluates the left-hand side only once, and keeps it in a temporary variable.


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接