簡體   English   中英

Null 或 C# 中通用參數的默認比較

[英]Null or default comparison of generic argument in C#

我有一個這樣定義的通用方法:

public void MyMethod<T>(T myArgument)

我要做的第一件事是檢查 myArgument 的值是否是該類型的默認值,如下所示:

if (myArgument == default(T))

但這不會編譯,因為我不能保證 T 會實現 == 運算符。 所以我將代碼切換為:

if (myArgument.Equals(default(T)))

現在可以編譯,但如果 myArgument 是 null(這是我正在測試的一部分),則會失敗。 我可以像這樣添加一個明確的 null 檢查:

if (myArgument == null || myArgument.Equals(default(T)))

現在這對我來說是多余的。 ReSharper 甚至建議我將 myArgument == null 部分更改為我開始的 myArgument == default(T) 部分。 有沒有更好的方法來解決這個問題?

我需要同時支持引用類型和值類型。

為避免裝箱,比較 generics 是否相等的最佳方法是使用EqualityComparer<T>.Default 這尊重IEquatable<T> (沒有裝箱)以及object.Equals ,並處理所有Nullable<T> “提升”的細微差別。 因此:

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

這將匹配:

  • null 用於類
  • null(空)為Nullable<T>
  • 其他結構的零/假/等

這個怎么樣:

if (object.Equals(myArgument, default(T)))
{
    //...
}

使用static object.Equals()方法可以避免您自己進行null檢查。 使用object. 根據您的上下文,可能沒有必要,但我通常在static調用前加上類型名稱,只是為了使代碼更易於溶解。

我能夠找到一篇詳細討論此問題的Microsoft Connect 文章

不幸的是,這種行為是設計使然,並且沒有一個簡單的解決方案可以使用可能包含值類型的類型參數。

如果已知類型是引用類型,則在 object 上定義的默認重載會測試變量的引用相等性,盡管類型可以指定自己的自定義重載。 編譯器根據變量的 static 類型確定使用哪個重載(確定不是多態的)。 因此,如果您更改示例以將泛型類型參數 T 約束為非密封引用類型(例如 Exception),編譯器可以確定要使用的特定重載,以下代碼將編譯:

public class Test<T> where T : Exception

如果已知類型是值類型,則根據使用的確切類型執行特定的值相等測試。 這里沒有好的“默認”比較,因為引用比較對值類型沒有意義,並且編譯器不知道要發出哪個特定的值比較。 編譯器可以發出對 ValueType.Equals(Object) 的調用,但此方法使用反射並且與特定值比較相比效率很低。 因此,即使您要在 T 上指定值類型約束,編譯器在此處生成的內容也不合理:

public class Test<T> where T : struct

在您介紹的情況下,編譯器甚至不知道 T 是值類型還是引用類型,類似地,不會生成任何對所有可能類型都有效的內容。 引用比較對值類型無效,並且對於不重載的引用類型,某種值比較將是意外的。

這是您可以做的...

我已經驗證這兩種方法都適用於引用和值類型的通用比較:

object.Equals(param, default(T))

或者

EqualityComparer<T>.Default.Equals(param, default(T))

要使用“==”運算符進行比較,您需要使用以下方法之一:

如果 T 的所有情況都源自已知的基礎 class ,則可以使用泛型類型限制讓編譯器知道。

public void MyMethod<T>(T myArgument) where T : MyBase

然后編譯器會識別如何在MyBase上執行操作,並且不會拋出您現在看到的“運算符 '==' 不能應用於類型為 'T' 和 'T' 的操作數”錯誤。

另一種選擇是將 T 限制為實現IComparable的任何類型。

public void MyMethod<T>(T myArgument) where T : IComparable

然后使用IComparable 接口定義的CompareTo方法。

嘗試這個:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

那應該編譯,並做你想做的事。

(已編輯)

Marc Gravell 給出了最好的答案,但我想發布一個簡單的代碼片段來演示它。 只需在一個簡單的 C# 控制台應用程序中運行它:

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

還有一件事:有 VS2008 的人可以試試這個作為擴展方法嗎? 我在這里堅持使用 2005 年,我很想知道這是否會被允許。


編輯:以下是如何讓它作為擴展方法工作:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}

要處理所有類型的 T,包括其中 T 是原始類型,您需要在兩種比較方法中進行編譯:

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }

基於已接受答案的擴展方法。

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

用法:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }

與 null 交替使用以簡化:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

用法:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }

這里會有問題——

如果您要允許它適用於任何類型,則默認(T)對於引用類型將始終為 null,對於值類型將始終為 0(或結構為 0)。

不過,這可能不是您所追求的行為。 如果您希望它以通用方式工作,您可能需要使用反射來檢查 T 的類型,並處理與引用類型不同的值類型。

或者,您可以對此設置接口約束,並且該接口可以提供一種方法來檢查類/結構的默認值。

我認為您可能需要將此邏輯分成兩部分,然后首先檢查 null。

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

在 IsNull 方法中,我們依賴於 ValueType 對象不能是 null 的事實,因此如果 value 恰好是派生自 ValueType 的 class,我們已經知道它不是 Z37A6259CC0C1DAE299ZA786 另一方面,如果它不是值類型,那么我們可以將轉換為 object 的值與 null 進行比較。 我們可以通過直接轉換為 object 來避免對 ValueType 的檢查,但這意味着值類型會被裝箱,這是我們可能想要避免的,因為這意味着在堆上創建了一個新的 object。

在 IsNullOrEmpty 方法中,我們正在檢查字符串的特殊情況。 對於所有其他類型,我們將值(已經知道為空)與其默認值進行比較,對於所有引用類型,默認值是 null,對於值類型,通常是某種形式的零(如果它們是整數)。

使用這些方法,以下代碼的行為可能與您預期的一樣:

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}

我用:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}

只是一個駭人聽聞的答案,並作為對自己的提醒。 但我發現這對我的項目很有幫助。 我這樣寫的原因是因為我不希望默認 integer 0 被標記為 null 如果值為 0

private static int o;
public static void Main()
{
    //output: IsNull = False -> IsDefault = True
    Console.WriteLine( "IsNull = " + IsNull( o ) + " -> IsDefault = " + IsDefault(o)); 
}

public static bool IsNull<T>(T paramValue)
{
  if( string.IsNullOrEmpty(paramValue + "" ))
    return true;
  return false;
}

public static bool IsDefault<T>(T val)
{
  return EqualityComparer<T>.Default.Equals(val, default(T));
}

不知道這是否符合您的要求,但您可以將 T 限制為實現 IComparable 等接口的類型,然后使用該接口(IIRC 支持/處理空值)中的 ComparesTo() 方法,如下所示:

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

您可能還可以使用其他接口 IEquitable 等。

@ilitirit:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

運算符“==”不能應用於“T”和“T”類型的操作數

如果沒有明確的 null 測試然后調用 Equals 方法或 object.Equals ,我想不出一種方法,如上所述。

您可以 devise 使用 System.Comparison 解決方案,但實際上這將導致更多的代碼行並大大增加復雜性。

我想你很接近。

if (myArgument.Equals(default(T)))

現在可以編譯,但如果myArgument是 null(這是我正在測試的一部分),則會失敗。 我可以像這樣添加一個明確的 null 檢查:

您只需要反轉 object ,在其上調用 equals 即可獲得優雅的 null 安全方法。

default(T).Equals(myArgument);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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