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

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

110390 次浏览

我能够找到一个详细讨论这个问题的Microsoft Connect文章:

不幸的是,这种行为是经过设计的,并且没有一个简单的解决方案来支持使用可能包含值类型的with类型参数。

如果已知类型是引用类型,则对象上定义的默认重载将测试变量的引用相等性,尽管类型可以指定其自己的自定义重载。编译器根据变量的静态类型决定使用哪个重载(这个决定不是多态的)。因此,如果你改变你的例子,将泛型类型参数T约束为非密封引用类型(例如Exception),编译器可以确定要使用的特定重载,并编译以下代码:

public class Test<T> where T : Exception

如果已知类型为值类型,则基于所使用的确切类型执行特定的值相等性测试。没有好的“违约”;这里的比较是因为引用比较对值类型没有意义,而且编译器不知道要发出哪个特定的值比较。编译器可以发出对valuettype . 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接口. 0方法定义的CompareTo方法。

这个怎么样:

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

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

不知道这是否适用于您的需求,但您可以将T约束为实现IComparable等接口的类型,然后从该接口(IIRC支持/处理空值)使用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'类型的操作数

如果不进行显式空测试,然后调用Equals方法或对象,我想不出有什么方法可以做到这一点。如上所述等于。

您可以使用System设计解决方案。比较,但这最终会导致更多的代码行,并大幅增加复杂性。

我认为您可能需要将这个逻辑分为两部分,并首先检查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对象根据定义不能为空的事实,因此如果value恰好是派生自ValueType的类,我们已经知道它不是空的。另一方面,如果它不是一个值类型,那么我们可以将值转换为对象与null进行比较。我们可以通过直接转换为object来避免对ValueType的检查,但这意味着值类型将被装箱,这是我们可能想要避免的,因为这意味着在堆上创建了一个新对象。

在IsNullOrEmpty方法中,我们检查字符串的特殊情况。对于所有其他类型,我们将值(已经知道是 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) ...
}

(编辑)

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

要处理所有类型的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)对于引用类型总是空的,对于值类型总是0(或充满0的结构体)。

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

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

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

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

这将匹配:

  • 类为空
  • Nullable<T>为null(空)
  • 对于其他结构体为0 /false/etc

我觉得你很接近了。

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

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

您只需要反向调用等号的对象,就可以实现一种优雅的零安全方法。

default(T).Equals(myArgument);

我使用:

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

基于公认答案的扩展方法。

   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

,我不希望默认整数0被标记为空
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));
}