簡體   English   中英

擴展 C# Coalesce 運算符

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM