题
我有一个如下定义的通用方法:
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),这就是我开始的地方。有没有更好的方法来解决这个问题?
我需要支持 两个都 引用类型和值类型。
解决方案
为了避免装箱,比较泛型是否相等的最佳方法是 EqualityComparer<T>.Default
. 。这尊重 IEquatable<T>
(没有拳击)以及 object.Equals
, ,并处理所有 Nullable<T>
“提升”细微差别。因此:
if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
return obj;
}
这将匹配:
- 类为空
- null(空)对于
Nullable<T>
- 其他结构的零/假/等
其他提示
这个怎么样:
if (object.Equals(myArgument, default(T)))
{
//...
}
使用 static object.Equals()
方法避免了您需要执行的操作 null
自行检查。显式限定调用 object.
根据您的上下文,可能没有必要,但我通常会添加前缀 static
使用类型名称进行调用只是为了使代码更易于理解。
我能够找到一个 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
然后使用 CompareTo
方法定义的 I可比接口.
尝试这个:
if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))
应该编译,并做你想做的事。
(已编辑)
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) 将始终为 null,对于值类型,默认值将始终为 0(或结构体全为 0)。
但这可能不是您想要的行为。如果您希望它以通用方式工作,您可能需要使用反射来检查 T 的类型,并处理与引用类型不同的值类型。
或者,您可以对此施加接口约束,并且该接口可以提供一种方法来检查类/结构的默认值。
我认为您可能需要将此逻辑分为两部分并首先检查 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)反对它的默认值,对于所有引用类型都是 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) ...
}
我用:
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 等接口的 Type,然后使用该接口(IIRC 支持/处理空值)中的 ComparesTo() 方法,如下所示:
public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))
您可能还可以使用其他接口 IEquitable 等。
@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 设计一个解决方案,但实际上这会导致更多的代码行并大大增加复杂性。
我想你很接近。
if (myArgument.Equals(default(T)))
现在可以编译了,但是如果 myArgument
为空,这是我正在测试的一部分。我可以像这样添加显式空检查:
您只需要反转调用 equals 的对象即可实现优雅的空安全方法。
default(T).Equals(myArgument);