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


288

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

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

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


C#现在支持Null Conditional Operators,这是您最后给出的示例的语法糖。您的代码将变为if (myArgument?.Equals( default(T) ) != null )
Wizard07KSU'3

1
@ wizard07KSU不适用于值类型,即true在任何情况下都求值为,因为将Equals始终为值类型调用,因为在这种情况下myArgument不能调用,null并且Equals(boolean)的结果永远不会null
碧玉'18

同样有价值的几乎是重复的(因此不赞成关闭):运算符==不能应用于C#中的泛型类型吗?
Gserg

Answers:


582

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

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

这将匹配:

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

28
哇,令人难以置信!当然,这肯定是要走的路。
尼克·法里纳

1
绝对是最佳答案。重写后可以在我的代码中使用简洁的代码行以使用此解决方案。
内森·里德利2009年

13
好答案!更好的做法是为此代码行添加扩展方法,以便可以使用obj.IsDefaultForType()
rikoe 2010年

2
@nawfal在的情况下Personp1.Equals(p2)将取决于它IEquatable<Person>是在公共API上实现还是通过显式实现-即编译器能否看到公共Equals(Person other)方法。然而; 在泛型中,全部使用相同的IL T; 一个T1恰好执行IEquatable<T1>需要被同等对待的T2不-所以没有,也不会发现一个Equals(T1 other)方法,即使它存在在运行时。在两种情况下,都null必须考虑(两个对象)。因此,对于泛型,我将使用我发布的代码。
Marc Gravell

5
我无法确定这个答案是否使我远离或接近精神错乱。+1
史蒂文·里肯斯

118

这个怎么样:

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

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


2
您甚至可以删除“对象”。部分,因为它是多余的。if(Equals(myArgument,default(T)))
Stefan Moser

13
是的,通常是,但是可能不取决于上下文。可能有一个带有两个参数的实例Equals()方法。我倾向于用类名显式地为所有静态调用添加前缀,如果这样做只是为了使代码更易于阅读。
肯特·布加亚特

8
需要注意的是,这将导致拳击,并且在某些情况下可能很重要
nightcoder 2010年

2
对我来说,当使用已经装箱的整数时,这不起作用。因为它将然后是一个对象和对象的默认为空而不是0
riezebosch

28

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

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

如果已知类型是引用类型,则对象上定义的默认重载会测试变量是否具有引用相等性,尽管类型可以指定自己的自定义重载。编译器根据变量的静态类型确定要使用的重载(确定不是多态的)。因此,如果您更改示例以将通用类型参数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定义的方法。


4
“这种行为是设计使然,没有一个简单的解决方案可以使用带有可能包含值类型的类型参数。” 其实微软是错的。有一个简单的解决方案:MS应该将ceq操作码扩展为按位运算符对值类型起作用。然后,他们可以提供仅使用此操作码的内部函数,例如,仅使用ceq的object.BitwiseOrReferenceEquals <T>(value,default(T))。对于值和引用类型,这将检查值的
位相等性


18

尝试这个:

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

应该可以编译,并做您想要的。


<code> default(T)</ code>不是多余的吗?<code> EqualityComparer <T> .Default.Equals(myArgument)</ code>应该可以解决问题。
Joshcodes 2013年

2
1)您是否尝试过,2)然后您要与比较器对象进行比较?该Equals方法IEqualityComparer有两个参数,这两个要比较的对象,所以,不,它不是多余的。
Lasse V. Karlsen 2013年

这甚至比恕我直言更好,因为它处理装箱/拆箱和其他类型。看到这个“封闭为

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

3
作为扩展方法,它可以“工作”。这很有趣,因为即使o为null时,即使您说o.IsDefault <object>()也可以使用。吓人=)
尼克·法里纳

6

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

1
请注意,该函数已更改为接受Func <T>并返回T,我认为这是从发问者的代码中意外省略的。
尼克·法里纳

似乎ReSharper惹我了。没有意识到关于值类型和null之间可能比较的警告不是编译器警告。
内森·里德利

2
仅供参考:如果T证明是一种值类型,那么与null的比较将被抖动视为始终为false。
埃里克·利珀特

很有道理-运行时将比较指针与值类型。但是,Equals()检查确实可以在这种情况下工作(有趣的是,因为说5.Equals(4)看起来很动态,但确实可以编译)。
尼克·法里纳

2
请参见EqualityComparer <T>答案,以了解不涉及拳击的其他方法
马克·格雷夫

2

这里会有一个问题-

如果您要允许它适用于任何类型,则引用类型的default(T)始终为null,而值类型的default(T)始终为0(或结构全为0)。

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

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


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方法中,我们正在检查字符串的特殊情况。对于所有其他类型,我们正在将值(已经知道不为空)与它的默认值(默认值对于所有引用类型为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

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

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

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

-1

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

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

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


OP担心NullReferenceException,并且您保证他也是如此。
nawfal

-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'的操作数

如果没有显式的null测试后再调用Equals方法或object.Equals,我想不出一种方法,如上所述。

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


-3

我想你很亲密。

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

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

您只需要反转在其上调用equals的对象,即可使用一种优雅的null安全方法。

default(T).Equals(myArgument);

我在想同样的事情。
克里斯·盖斯勒

6
引用类型的default(T)为null,并保证NullReferenceException。
Stefan Steinegger,2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.