C#中泛型参数的空值或者默认比较

时间:2020-03-05 18:54:10  来源:igfitidea点击:

我有这样定义的通用方法:

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),这是我开始的地方。有解决这个问题的更好方法吗?

我需要同时支持引用类型和值类型。

解决方案

回答

我能够找到一篇Microsoft Connect文章,其中详细讨论了此问题:

Unfortunately, this behavior is by design and there is not an easy solution to enable use of with type parameters that may contain value types.
  
  If the types are known to be reference types, the default overload of defined on object tests variables for reference equality, although a type may specify its own custom overload. The compiler determines which overload to use based on the static type of the variable (the determination is not polymorphic). Therefore, if you change your example to constrain the generic type parameter T to a non-sealed reference type (such as Exception), the compiler can determine the specific overload to use and the following code would compile:
public class Test<T> where T : Exception
If the types are known to be value types, performs specific value equality tests based on the exact types used. There is no good "default" comparison here since reference comparisons are not meaningful on value types and the compiler cannot know which specific value comparison to emit. The compiler could emit a call to ValueType.Equals(Object) but this method uses reflection and is quite inefficient compared to the specific value comparisons. Therefore, even if you were to specify a value-type constraint on T, there is nothing reasonable for the compiler to generate here:
public class Test<T> where T : struct
In the case you presented, where the compiler does not even know whether T is a value or reference type, there is similarly nothing to generate that would be valid for all possible types. A reference comparison would not be valid for value types and some sort of value comparison would be unexpected for reference types that do not overload.

这是我们可以做的...

我已经验证了这两种方法都可以对引用类型和值类型进行一般比较:

object.Equals(param, default(T))

或者

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

要与" =="运算符进行比较,我们将需要使用以下方法之一:

如果T的所有情况都来自已知的基类,则可以使用泛型类型限制让编译器知道。

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 (object.Equals(myArgument, default(T)))
{
    //...
}

使用static object.Equals()方法避免了我们需要对自己进行null检查。根据上下文,可能不一定需要使用object.来明确限定调用,但我通常会在static调用之前加上类型名称,以使代码更易于理解。

回答

不知道这是否符合要求,但是我们可以将T约束为实现诸如IComparable之类的接口的Type,然后像这样从该接口(IIRC支持/处理null)使用ComparesTo()方法:

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

我们可能还可以使用IEquitable等其他接口。

回答

试试这个:

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

应该可以编译,然后做我们想要的。

回答

@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,我想不出一种方法,如上所述。

我们可以使用System.Comparison设计解决方案,但实际上这将最终导致更多的代码行并大大增加复杂性。

回答

我认为我们可能需要将此逻辑分为两部分,并首先检查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派生的类,我们已经知道它不是null。另一方面,如果它不是值类型,那么我们可以将对象的值强制转换为null。我们可以直接对对象进行强制转换来避免对ValueType的检查,但这意味着将对值类型进行装箱,这是我们可能要避免的事情,因为这意味着在堆上创建了一个新对象。

在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) ...
}

回答

(已编辑)

Marc Gravell的答案是最好的,但是我想发布一个我努力演示的简单代码段。只需在一个简单的Cconsole应用程序中运行它即可:

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,我们需要使用两种比较方法进行编译:

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

回答

这里会有一个问题-

如果我们要允许它适用于任何类型,则对于引用类型,default(T)始终为null,对于值类型,其值始终为0(或者结构全为0)。

但是,这可能不是我们追求的行为。如果希望此方法以通用方式工作,则可能需要使用反射来检查T的类型,并处理与引用类型不同的值类型。

或者,我们可以对此施加接口约束,并且该接口可以提供一种方法来检查类/结构的默认值。

回答

为了避免装箱,比较泛型是否相等的最佳方法是使用EqualityComparer &lt;T> .Default。这尊重" IEquatable <T>"(不带拳击)以及" object.Equals",并处理所有" Nullable <T>""提升"的细微差别。因此:

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

这将匹配:

  • 类的null
  • Nullable <T>的null(空)
  • 零/假/等其他结构