316

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

public void MyMethod<T>(T myArgument)

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

if (myArgument == default(T))

但这不能编译,因为我不能保证 T 会实现 == 运算符。所以我将代码切换为:

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

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

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

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

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

4

14 回答 14

673

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

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

这将匹配:

  • 类为空
  • 空(空)为Nullable<T>
  • 其他结构的零/假/等
于 2009-05-14T18:11:23.613 回答
121

这个怎么样:

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

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

于 2008-09-15T18:30:54.180 回答
31

我能够找到一篇详细讨论此问题的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定义的方法。

于 2008-09-15T18:26:37.110 回答
21

试试这个:

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

那应该编译,并做你想做的事。

于 2008-09-15T18:33:29.090 回答
7

(已编辑)

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));
    }
}
于 2009-05-14T16:14:00.807 回答
6

要处理所有类型的 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;
    }
于 2009-05-14T16:16:09.427 回答
4

基于已接受答案的扩展方法。

   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;
   }
于 2020-02-18T12:50:58.073 回答
3

这里会有问题——

如果您要允许它适用于任何类型,则 default(T) 对于引用类型将始终为 null,对于值类型将始终为 0(或 struct full of 0)。

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

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

于 2009-05-14T16:16:55.697 回答
1

我认为您可能需要将此逻辑分成两部分并首先检查 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 方法中,我们正在检查字符串的特殊情况。对于所有其他类型,我们将值(已经知道为空)与它的默认值进行比较,对于所有引用类型,默认值都是空的,对于值类型,通常是某种形式的零(如果它们是整数)。

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

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) ...
}
于 2009-03-24T12:23:07.600 回答
0

我用:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}
于 2018-07-08T23:55:16.147 回答
0

只是一个骇人听闻的答案,并作为对自己的提醒。但我发现这对我的项目很有帮助。我这样写的原因是因为如果值为 0,我不希望默认整数 0 被标记为 null

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));
}
于 2021-02-23T04:59:18.890 回答
-1

不知道这是否符合您的要求,但您可以将 T 限制为实现 IComparable 等接口的类型,然后使用该接口中的 ComparesTo() 方法(IIRC 支持/处理空值)像这样:

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

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

于 2008-09-15T18:32:35.080 回答
-2

@ilitirit:

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 设计一个解决方案,但实际上这最终会导致更多的代码行并大大增加复杂性。

于 2008-09-15T18:37:39.437 回答
-3

我想你很接近。

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

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

您只需要反转调用 equals 的对象即可获得优雅的 null 安全方法。

default(T).Equals(myArgument);
于 2011-07-28T10:56:46.303 回答