扩展C#Coalesce运算符

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

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

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

例如

 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(this T obj, Func func, TResult defaultValue) { if (obj!=null) return func(obj); else return defaultValue; } public static TResult Coalesce(this T obj, Func func, Func 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 obj, Func func) where TResult : class { if (obj!=null) return func(obj); else return null; } public static TResult UntilNull(T1 obj, Func func1, Func func2) where TResult : class { if (obj!=null) return UntilNull(func1(obj), func2); else return null; } public static TResult UntilNull(T1 obj, Func func1, Func func2, Func func3) where TResult : class { if (obj!=null) return UntilNull(func1(obj), func2, func3); else return null; } public static TResult UntilNull(T1 obj, Func func1, Func func2, Func func3, Func 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"); 

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

更新

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

 var street = customer?.PrimaryAddress?.Street; 

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

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

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

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

当然注意到,只有当最终属性值可用但由于某种原因设置为null时,才可以使用默认值。

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

 possiblyNull?.SomeMethod(); 

它已经让我困惑了…通常,你会想到合并它的 – 我想象第一个非空的(f) => f.Value"default value"将被返回,这不是case(null测试在原始实例上)。

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

 f => f.Value 

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

或者甚至简单地Dereference ,只要参数名称(等)清楚地表明第二个arg是什么。

这也很容易扩展:

 public static TResult Coalesce(this T obj, Func func, TResult defaultValue) { if (obj == null) return defaultValue; return func(obj); } public static TResult Coalesce(this T1 obj, Func func1, Func func2, TResult defaultValue) { if (obj == null) return defaultValue; T2 obj2 = func1(obj); if (obj2 == null) return defaultValue; return func2(obj2); } public static TResult Coalesce(this T1 obj, Func func1, Func func2, Func 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(this T1 obj, Func func1, Func func2, Func func3, Func 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 

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

这似乎是实现null对象模式的绝佳机会。

考虑:

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

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

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

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

好post! 我更新了代码以支持返回Nullable,因为Nullable是作为结构实现的。

  public static class Coalesce { public static TResult UntilNull(T obj, Func func) where TResult : class { if (obj != null) return func(obj); else return null; } public static TResult UntilNull(T1 obj, Func func1, Func func2) where TResult : class { if (obj != null) return UntilNull(func1(obj), func2); else return null; } public static TResult UntilNull(T1 obj, Func func1, Func func2, Func func3) where TResult : class { if (obj != null) return UntilNull(func1(obj), func2, func3); else return null; } public static TResult UntilNull(T1 obj, Func func1, Func func2, Func func3, Func func4) where TResult : class { if (obj != null) return UntilNull(func1(obj), func2, func3, func4); else return null; } public static Nullable UntilNull(T obj, Func> func) where TResult : struct { if (obj != null) return func(obj); else return new Nullable(); } public static Nullable UntilNull(T1 obj, Func func1, Func> func2) where TResult : struct { if (obj != null) return UntilNull(func1(obj), func2); else return new Nullable(); } public static Nullable UntilNull(T1 obj, Func func1, Func func2, Func> func3) where TResult : struct { if (obj != null) return UntilNull(func1(obj), func2, func3); else return new Nullable(); } public static Nullable UntilNull(T1 obj, Func func1, Func func2, Func func3, Func> func4) where TResult : struct { if (obj != null) return UntilNull(func1(obj), func2, func3, func4); else return new Nullable(); } } 

为什么不写下正常的合并函数,那么你可以像这样使用它:

 coalesce(something, something_else, "default"); 

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

六年后, Null条件运算符在这里:

有时代码往往会在null检查中淹没一点。 空条件运算符只允许您在接收方不为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的简单且线程安全的方法。 它是线程安全的原因是该function仅评估左侧一次,并将其保存在临时变量中。