【问题标题】:Null or default comparison of generic argument in C#C#中泛型参数的空值或默认比较
【发布时间】:2008-09-15 18:17:59
【问题描述】:

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

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) 部分。有没有更好的方法来解决这个问题?

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

【问题讨论】:

  • C# 现在支持Null Conditional Operators,这是您给出的最后一个示例的语法糖。您的代码将变为if (myArgument?.Equals( default(T) ) != null )
  • @wizard07KSU 这对值类型不起作用,即在任何情况下都计算为true,因为Equals 将始终被调用为值类型,因为myArgument 在此不能是null大小写和Equals(布尔值)的结果永远不会是null
  • 同样有价值的几乎重复(所以不投票结束):Can't operator == be applied to generic types in C#?

标签: c# generics


【解决方案1】:

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

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

这将匹配:

  • 类为空
  • Nullable&lt;T&gt; 为空(空)
  • 其他结构的零/假/等

【讨论】:

  • 哇,多么令人愉快的晦涩难懂!这绝对是要走的路,荣誉。
  • 很好的答案!更好的是为这行代码添加一个扩展方法,这样你就可以去 obj.IsDefaultForType()
  • @nawfal 在 Person 的情况下,p1.Equals(p2) 将取决于它是在公共 API 上实现 IEquatable&lt;Person&gt;,还是通过显式实现 - 即编译器能否看到公共 Equals(Person other)方法。然而;在generics中,相同的IL用于所有T;恰好实现 IEquatable&lt;T1&gt;T1 需要与不实现 T2T2 同等对待 - 所以不,它不会发现 Equals(T1 other) 方法,即使它存在于运行。在这两种情况下,还需要考虑null(任一对象)。所以对于泛型,我会使用我发布的代码。
  • 我无法确定这个答案是否让我远离或接近了精神错乱。 +1
  • @Jordan 这不是拳击的意思;您刚刚描述的是“按值传递”,是的:结构会发生这种情况。但是装箱是当您将值类型视为object(或接口,例如IEquatable)时;这包括将值类型作为object 类型的参数传递,这是object.Equals 所具有的。当您这样做时,该值被“装箱”到一个对象中,这意味着:分配了一个新的托管堆对象,该对象封装了该值的副本。当您将两个整数装箱时:没人会在意。但是当它处于热路径循环中时,您可以分配 lots非常非常快 - 不好。
【解决方案2】:

这个怎么样:

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

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

【讨论】:

  • 您甚至可以删除“对象”。部分,因为它是多余的。 if (Equals(myArgument, default(T)))
  • 是的,通常是这样,但可能不取决于上下文。可能有一个带有两个参数的实例 Equals() 方法。我倾向于使用类名显式地为所有静态调用添加前缀,这只是为了使代码更易于阅读。
  • 需要注意的是会导致拳击,在某些情况下可能很重要
  • 对我来说,这在使用已经装箱的整数时不起作用。因为它将是一个对象,并且对象的默认值为 null 而不是 0。
【解决方案3】:

我找到了详细讨论此问题的Microsoft Connect article

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

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

【讨论】:

  • “这种行为是设计使然,没有一个简单的解决方案可以使用可能包含值类型的类型参数。”其实微软错了。有一个简单的解决方案:MS 应该将 ceq 操作码扩展为在值类型上作为按位运算符运行。然后他们可以提供一个简单地使用这个操作码的内在函数,例如仅使用 ceq 的 object.BitwiseOrReferenceEquals(value, default(T))。对于值和引用类型,这将检查 值的位相等(但对于引用类型,引用位相等与 object.ReferenceEquals 相同)
  • 我认为您想要的 Microsoft Connect 链接是 connect.microsoft.com/VisualStudio/feedback/details/304501/…
【解决方案4】:

试试这个:

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

应该编译,然后做你想做的事。

【讨论】:

  • default(T) 不是多余的吗? EqualityComparer<t>.Default.Equals(myArgument)</t> 应该可以解决问题。
  • 1) 你试过了吗,2) 你在比较什么,比较器对象? IEqualityComparerEquals 方法接受两个参数,两个对象进行比较,所以不,不是多余的。
  • 这甚至比接受的答案恕我直言,因为它处理装箱/拆箱和其他类型。请参阅此“作为骗子关闭”问题的答案:stackoverflow.com/a/864860/210780
【解决方案5】:

(已编辑)

Marc Gravell 给出了最好的答案,但我想发布一个简单的代码 sn-p 来演示它。只需在一个简单的 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));
    }
}

【讨论】:

  • 它作为一种扩展方法“工作”。这很有趣,因为即使您在 o 为空时说 o.IsDefault() 也有效。可怕 =)
【解决方案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;
    }

【讨论】:

  • 请注意,该函数已更改为接受 Func 并返回 T,我认为是在提问者的代码中意外省略了。
  • 似乎 ReSharper 惹恼了我。没有意识到它关于值类型和 null 之间可能比较的警告不是编译器警告。
  • 仅供参考:如果 T 原来是一个值类型,那么与 null 的比较将被抖动视为始终为假。
  • 有道理 - 运行时会将指针与值类型进行比较。然而,Equals() 检查在这种情况下确实有效(有趣的是,说 5.Equals(4) 确实编译似乎非常动态语言)。
  • 查看 EqualityComparer 答案以获得不涉及拳击等的替代方案
【解决方案7】:

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

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

【讨论】:

  • 啊,是的,那个奇怪的小细节,你可以完美地调用空对象的扩展方法而不会出现空指针异常。老实说,在可能为 null 的 obj 上调用 obj.ExecuteSomething 的语法看起来真的很吓人。
【解决方案8】:

这里会有问题-

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

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

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

【讨论】:

    【解决方案9】:

    我认为您可能需要将此逻辑分成两部分并首先检查是否为空。

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

    【讨论】:

      【解决方案10】:

      我用:

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

      【讨论】:

        【解决方案11】:

        只是一个粗俗的答案,并作为对我自己的提醒。 但我发现这对我的项目很有帮助。 我这样写的原因是因为如果值为 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));
        }
        

        【讨论】:

          【解决方案12】:

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

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

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

          【讨论】:

          • OP 对 NullReferenceException 感到担心,而您也向他保证同样如此。
          【解决方案13】:

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

          【讨论】:

            【解决方案14】:

            我认为你很接近。

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

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

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

            default(T).Equals(myArgument);
            

            【讨论】:

            • 我也在想同样的事情。
            • 引用类型的默认(T)为 null 并导致有保证的 NullReferenceException。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2021-09-11
            • 2018-09-12
            • 2022-07-24
            • 1970-01-01
            • 2011-06-15
            相关资源
            最近更新 更多