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

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

Null or default comparison of generic argument in C#

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

1
public void MyMethod<T>(T myArgument)

我要做的第一件事是检查myArgument的值是否是该类型的默认值,如下所示:

1
if (myArgument == default(T))

但这并不能编译,因为我还不能保证t将实现==运算符。所以我把代码换成了:

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

现在编译,但如果myArgument为空(这是我测试的一部分),它将失败。我可以像这样添加一个显式的空检查:

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

现在我觉得这是多余的。Resharper甚至建议我将myArgument==null部分更改为myArgument==default(t),这是我开始的地方。有没有更好的方法来解决这个问题?

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


为了避免拳击,最好的方法是与EqualityComparer.Default比较泛型是否平等。这尊重EDOCX1(不带拳击)和object.Equals,处理所有Nullable的"提升"细微差别。因此:

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

这将匹配:

  • 类为空
  • Nullable为空
  • 其他结构为零/假/等

这个怎么样?

1
2
3
4
if (object.Equals(myArgument, default(T)))
{
    //...
}

使用static object.Equals()方法可以避免您需要自己进行null检查。根据您的上下文,可能不需要显式限定使用object.的调用,但我通常会在static调用前面加上类型名,以使代码更易理解。


我找到了一篇详细讨论此问题的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:

1
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:

1
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.

这是你能做的…

我已经验证了这两种方法都适用于引用类型和值类型的一般比较:

1
object.Equals(param, default(T))

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

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

如果t的所有情况都是从已知的基类派生的,那么可以使用泛型类型限制让编译器知道。

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

然后,编译器识别如何在MyBase上执行操作,并且不会抛出"operator"=="cannot be applied to operands of type‘t’and‘t’"错误,您现在看到的是。

另一种选择是将t限制为实现IComparable的任何类型。

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

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


试试这个:

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

它应该编译,并做您想要做的事情。


(编辑)

MarcGravell有最好的答案,但我想发布一个简单的代码片段来演示它。只需在一个简单的C控制台应用程序中运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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年,我很想知道这是否可以。

编辑:以下是如何使其作为扩展方法工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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是基元类型,需要使用两种比较方法进行编译:

1
2
3
4
5
6
7
8
9
    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;
    }


这里会有问题的-

如果您要允许它适用于任何类型,那么对于引用类型,默认值(T)将始终为空,对于值类型,默认值(T)将始终为0(或结构完全为0)。

不过,这可能不是你所追求的行为。如果您希望它以通用的方式工作,您可能需要使用反射来检查t的类型,并处理不同于引用类型的值类型。

或者,您可以在上面放置一个接口约束,并且接口可以提供一种检查类/结构默认值的方法。


我认为您可能需要将这个逻辑分成两部分,并首先检查是否为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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对象不能为空,因此如果值恰好是从ValueType派生的类,我们已经知道它不是空的。另一方面,如果它不是一个值类型,那么我们可以将值强制转换与一个对象的值进行比较。我们可以通过直接转到强制转换对象来避免对value type的检查,但这意味着值类型将被装箱,这可能是我们想要避免的,因为它意味着在堆上创建了一个新对象。

在isNullOrEmpty方法中,我们正在检查字符串的特殊情况。对于所有其他类型,我们将值(已经知道不是空值)与默认值(对于所有引用类型,默认值为空值)进行比较,对于值类型,默认值通常是某种形式的零(如果它们是整数)。

使用这些方法,以下代码的行为与您预期的一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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) ...
}

我使用:

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

不知道这是否适用于您的需求,但是您可以将t约束为实现接口(如IComparable)的类型,然后从该接口(IIRC支持/处理空值)使用comparesto()方法,如下所示:

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

可能还有其他接口,您可以使用以及iequitable等。


@伊利提利:

1
2
3
4
5
6
7
8
9
public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

运算符"=="不能应用于"t"和"t"类型的操作数

如果没有显式的空测试,然后调用equals方法或object.equals(如上所述),我想不出一种方法来实现这一点。

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


我觉得你很亲近。

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

现在编译,但如果myArgument为空,这将失败,这是我测试的一部分。我可以像这样添加一个显式的空检查:

您只需要反转对其调用equals的对象,就可以使用一种优雅的空安全方法。

1
default(T).Equals(myArgument);


推荐阅读