it-swarm.cn

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

我有一个像这样定义的泛型方法:

public void MyMethod<T>(T myArgument)

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

if (myArgument == default(T))

但是这不能编译,因为我没有保证T将实现==运算符。所以我把代码改为:

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

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

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

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

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

252
Stefan Moser

为了避免装箱,比较泛型的最佳方法是使用EqualityComparer<T>.Default。这尊重IEquatable<T>(没有装箱)以及object.Equals,并处理所有Nullable<T>“提升”的细微差别。因此:

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

这将匹配:

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

这个怎么样:

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

使用static object.Equals()方法可以避免自己进行null检查。根据你的上下文,可能没有必要使用object.显式地限定调用,但我通常使用类型名称为static调用前缀,以使代码更易于解析。

113
Kent Boogaart

我找到了一个 Microsoft Connect文章 它详细讨论了这个问题:

不幸的是,这种行为是设计上的,并没有一个简单的解决方案来启用可能包含值类型的类型参数。

如果已知类型是引用类型,则对象上定义的默认重载测试变量以引用相等,尽管类型可以指定自己的自定义重载。编译器根据变量的静态类型确定要使用的重载(确定不是多态的)。因此,如果您更改示例以将泛型类型参数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的所有情况都来自已知的基类,您可以让编译器知道使用泛型类型限制。

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方法

24
Eric Schoonover

试试这个:

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

应该编译,并做你想要的。

18

(编辑)的

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));
    }
}
7
Joel Coehoorn

要处理所有类型的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;
    }
6
Nick Farina

这里会出现问题 -

如果您要允许它适用于任何类型,则默认(T)对于引用类型始终为null,对于值类型为0(或者结构为0)。

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

或者,您可以对此设置接口约束,并且接口可以提供检查类/结构的默认值的方法。

2
Reed Copsey

我想你可能需要将这个逻辑分成两部分并首先检查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方法中,我们正在检查字符串的特殊情况。对于所有其他类型,我们将值(已知为 not null)与其默认值进行比较,对于所有引用类型,该值为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) ...
}
1
Damian Powell

我用:

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