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

Ste*_*ser 269 c# generics

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

public void MyMethod<T>(T myArgument)
Run Code Online (Sandbox Code Playgroud)

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

if (myArgument == default(T))
Run Code Online (Sandbox Code Playgroud)

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

if (myArgument.Equals(default(T)))
Run Code Online (Sandbox Code Playgroud)

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

if (myArgument == null || myArgument.Equals(default(T)))
Run Code Online (Sandbox Code Playgroud)

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

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

Mar*_*ell 539

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

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}
Run Code Online (Sandbox Code Playgroud)

这将匹配:

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

  • 哇,多么令人愉快的晦涩难懂!这绝对是可行的方式,不仅如此. (25认同)
  • 很棒的答案!更好的是为这行代码添加扩展方法,以便你可以去obj.IsDefaultForType() (13认同)
  • 我无法确定这个答案是否会让我远离或接近精神错乱.+1 (4认同)
  • @Jordan 这不是拳击的意思;您刚刚描述的是“按值传递”,是的:结构会发生这种情况。但是装箱是将值类型视为“对象”(或接口,例如“IEquatable”);这包括将值类型作为 `object` 类型的参数传递,这就是 `object.Equals` 所拥有的。当你这样做时,值被“装箱”到一个对象中,这意味着:一个新的托管堆对象被分配,它封装了一个值的副本。当您将两个整数装箱时:没有人会在意。但是当这是在热路径循环中时,您可以分配**批次**,**非常非常快** - 不好。 (3认同)
  • 在'Person`的情况下@nawfal,`p1.Equals(p2)`将取决于它是否在公共API上实现`IEquatable <Person>`,或者通过显式实现 - 即编译器是否可以看到公共`Equals(人其他)`方法.然而; 在*generics*中,相同的IL用于所有`T`; 碰巧实现`IEquatable <T1>`的`T1`需要被处理成与没有的`T2`相同 - 所以不,它不会**发现`Equals(T1 other)`方法,甚至如果它在运行时存在.在这两种情况下,还有"null"要考虑(对象).因此,对于泛型,我会使用我发布的代码. (2认同)
  • @Jordan 前三个词:“避免拳击”。`Object.Equals` 会导致装箱 (2认同)
  • @Jordan [这是 IL](https://gist.github.com/mgravell/7c4134374b73610238b1ea0d300e700b) - 如您所见,它清楚地装箱。现在,拳击是否是*问题*是上下文相关的。由于程序会立即退出,因此您的示例显然无关紧要。然而,每个盒子都是一个分配。如果您处理任何非平凡的事情,则始终建议避免*不必要的* 分配。这些分配是不必要的。 (2认同)
  • @MarcGravell 我已经使用了好几年了(感谢顺便说一句),我最近在我的项目中启用了 Nullable 引用类型,并且知道我收到一条警告,默认(T)可能为空(CS8604),你知道还有其他方法吗检查 null,避免装箱和使用泛型? (2认同)

Ken*_*art 117

这个怎么样:

if (object.Equals(myArgument, default(T)))
{
    //...
}
Run Code Online (Sandbox Code Playgroud)

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

  • 是的,它通常是,但可能不依赖于上下文.可能有一个实例Equals()方法,它接受两个参数.我倾向于使用类名显式地为所有静态调用添加前缀,如果只是为了使代码更容易阅读. (13认同)
  • 需要注意的是,它会导致拳击,在某些情况下,它可能很重要 (8认同)
  • 您甚至可以删除“对象”。部分,因为它是多余的。如果(Equals(myArgument,default(T))) (2认同)
  • 对我来说,当使用已经装箱的整数时,这不起作用。因为它将成为一个对象,而object的默认值为null而不是0。 (2认同)

Eri*_*ver 27

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

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

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

public class Test<T> where T : Exception
Run Code Online (Sandbox Code Playgroud)

如果已知类型是值类型,则根据使用的确切类型执行特定值相等性测试.这里没有好的"默认"比较,因为参考比较对值类型没有意义,并且编译器无法知道要发出哪个特定值比较.编译器可以发出对ValueType.Equals(Object)的调用,但是此方法使用反射,并且与特定值比较相比效率很低.因此,即使您要在T上指定值类型约束,编译器也无法在此处生成:

public class Test<T> where T : struct
Run Code Online (Sandbox Code Playgroud)

在您提供的情况下,编译器甚至不知道T是值还是引用类型,同样没有任何生成可以对所有可能类型有效的内容.参考比较对于值类型无效,对于不重载的引用类型,某种值比较会出乎意料.

这是你可以做的......

我已经验证了这两种方法都可以用于参考和值类型的通用比较:

object.Equals(param, default(T))
Run Code Online (Sandbox Code Playgroud)

要么

EqualityComparer<T>.Default.Equals(param, default(T))
Run Code Online (Sandbox Code Playgroud)

要与"=="运算符进行比较,您需要使用以下方法之一:

如果T的所有情况都来自已知的基类,您可以让编译器知道使用泛型类型限制.

public void MyMethod<T>(T myArgument) where T : MyBase
Run Code Online (Sandbox Code Playgroud)

编译器然后识别如何执行操作,MyBase并且不会抛出"运算符'=='不能应用于现在看到的'T'和'T'类型的操作数"错误.

另一种选择是将T限制为任何实现的类型IComparable.

public void MyMethod<T>(T myArgument) where T : IComparable
Run Code Online (Sandbox Code Playgroud)

然后使用IComparable接口CompareTo定义的方法.

  • "这种行为是设计使然,没有一个简单的解决方案可以使用可能包含值类型的类型参数." 其实微软错了.有一个简单的解决方案:MS应该扩展ceq操作码以作为按位运算符在值类型上运行.然后他们可以提供一个简单地使用这个操作码的内在函数,例如object.BitwiseOrReferenceEquals <T>(值,默认(T)),它只使用ceq.对于值和引用类型,这将检查值*的按位相等*(但对于引用类型,引用按位相等与object.ReferenceEquals相同) (4认同)

ang*_*son 18

试试这个:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))
Run Code Online (Sandbox Code Playgroud)

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

  • 1)你有没有尝试过,2)你比较对象的对象是什么?`IEqualityComparer`的`Equals`方法有两个参数,两个要比较的对象,所以不,它不是多余的. (2认同)

Joe*_*orn 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();
}
Run Code Online (Sandbox Code Playgroud)

还有一件事:有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));
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 它确实"工作"作为扩展方法.这很有意思,因为即使你在o为空时你说o.IsDefault <object>()它也有效.吓人=) (3认同)

Nic*_*ina 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;
    }
Run Code Online (Sandbox Code Playgroud)

  • FYI:如果T变成值类型,那么与null的比较将被抖动视为始终为false. (2认同)
  • 有关不涉及装箱等的替代方案,请参阅EqualityComparer <T>答案 (2认同)

wch*_*ard 5

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

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }
Run Code Online (Sandbox Code Playgroud)

用法:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }
Run Code Online (Sandbox Code Playgroud)

与 null 交替以简化:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }
Run Code Online (Sandbox Code Playgroud)

用法:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }
Run Code Online (Sandbox Code Playgroud)