[英]Extending the C# Coalesce Operator
在我解釋我想要做什么之前,如果你看下面的代碼,你會明白它應該做什么嗎? (更新 - 見下文)
Console.WriteLine(
Coalesce.UntilNull(getSomeFoo(), f => f.Value) ?? "default value");
C# 已經有一個空合並運算符,它在簡單對象上工作得很好,但如果您需要訪問該 object 的成員,則無濟於事。
例如
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 是多余的,因為您可以只使用現有的? 運營商。
這是修改后的 class:
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");
是的,我會理解的。 是的,coalesce 是個好名字。 是的,如果 C# 有一個像 Groovy 和其他一些語言這樣的空安全解引用運算符會更好:)
更新
C# 6 就有這樣一個運算符——null 條件運算符, ?.
例如:
var street = customer?.PrimaryAddress?.Street;
如果您仍需要默認值,請將其與原始空合並運算符一起使用。 例如:
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"
,這不是案例(null 測試在原始實例上)。
注意沒有括號會更清楚嗎?
f => f.Value
你實際上在做什么類似於Select
- 所以像SafeSelect
這樣的名字會是一個好名字,IMO(但可能不完全是......)。
甚至只是簡單的Dereference
,只要參數名稱(等)清楚地說明第二個參數的用途。
這也可以很容易地擴展:
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
看起來足夠可讀,雖然它仍然有點笨拙。
不過,這似乎是實現null object 模式的絕佳機會。
考慮:
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);
很棒的帖子。 我更新了您的代碼以支持返回 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>();
}
}
如果你經常在代碼庫中使用它,我認為它很好,因為它在第一次閱讀時並不太難理解,並且減少了代碼的大小——這樣可以幫助我從樹上看到木頭。
但是 if 只使用 1 或 2 次,我認為“in line” if會更好,因為我第一次看到它時不會考慮“if”的含義。
“in line” if - 我的意思是一個正常的 if 語句,它沒有隱藏在單獨的方法中。
為什么不寫下正常的coalesce function,那么你可以這樣使用它:
coalesce(something, something_else, "default");
換句話說——你需要 lambdas 做什么?
六年后, 空條件運算符出現了:
有時代碼往往會淹沒在空檢查中。 空條件運算符允許您僅在接收者不為空時訪問成員和元素,否則提供 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
空條件運算符表現出短路行為,其中緊隨其后的成員訪問鏈、元素訪問和調用僅在原始接收者不是 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 的簡單且線程安全的方法。 它是線程安全的原因是該功能僅評估左側一次,並將其保存在臨時變量中。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.