代码之家  ›  专栏  ›  技术社区  ›  Nathan Ridley

如何将泛型类型与其默认值进行比较?[复制品]

  •  46
  • Nathan Ridley  · 技术社区  · 15 年前

    我有一个定义如下的通用方法:

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

    我需要支持 二者都 引用类型和值类型。

    0 回复  |  直到 9 年前
        1
  •  521
  •   Sergey Kalinichenko    11 年前

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

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

    这将匹配:

    • 类为空
    • 空的 可为空<t>
    • 其他结构的0/false/etc
        2
  •  114
  •   Tim Cooper    13 年前

    这个怎么样:

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

    使用 static object.Equals() 方法避免您需要执行 null 检查一下你自己。显式限定调用 object. 根据上下文的不同,可能不需要,但我通常在 static 使用类型名的调用只是为了使代码更易于理解。

        3
  •  26
  •   increddibelly    10 年前

    我找到了一个 Microsoft Connect article 详细讨论了这个问题:

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

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

    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 并且不会抛出“operator”=“cannot be applied to operates of type't'and't'”错误,您现在看到的是。

    另一个选择是将t限制为实现 IComparable .

    public void MyMethod<T>(T myArgument) where T : IComparable
    

    然后使用 CompareTo 方法定义为 IComparable interface .

        4
  •  18
  •   Lasse V. Karlsen    16 年前

    试试这个:

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

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

        5
  •  7
  •   Andrew Hare    15 年前

    (编辑)

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

    还有一件事:有人能将它作为一种扩展方法吗?我在这里停留在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));
        }
    }
    
        6
  •  6
  •   George Stocker NotMe    11 年前

    要处理所有类型的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;
        }
    
        7
  •  2
  •   Reed Copsey    15 年前

    这里会有问题的-

    如果要允许对任何类型执行此操作,则对于引用类型,默认值(t)将始终为空,对于值类型,默认值(t)将始终为0(或充满0的结构)。

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

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

        8
  •  1
  •   Damian Powell    15 年前

    我认为您可能需要将这个逻辑分成两部分,并首先检查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,所以如果值恰好是从ValueType派生的类,我们就知道它不是NULL。另一方面,如果它不是一个值类型,那么我们只可以将值CAST与对象对NULL进行比较。我们可以直接转到cast to object来避免对value type的检查,但这意味着值类型将被装箱,这可能是我们希望避免的,因为它意味着在堆上创建了一个新对象。

    在isnullorempty方法中,我们检查字符串的特殊情况。对于所有其他类型,我们正在比较 空)与它的默认值(对于所有引用类型来说都是空的,对于值类型来说通常是某种形式的零(如果它们是整数)。

    使用这些方法,以下代码的行为与您预期的一样:

    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) ...
    }
    
        9
  •  0
  •   kofifus    6 年前

    我使用:

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

    不知道这是否符合您的需求,但可以将t约束为实现接口(如icomparable)的类型,然后使用该接口(iirc支持/处理空值)中的compareSto()方法,如下所示:

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

    可能还有其他接口,你也可以使用iequitable等。

        11
  •  -2
  •   cfeduke    16 年前

    @伊利提利:

    public class Class<T> where T : IComparable
    {
        public T Value { get; set; }
        public void MyMethod(T val)
        {
            if (Value == val)
                return;
        }
    }
    

    运算符“=”不能应用于“t”和“t”类型的操作数

    如果没有显式的空测试,然后调用Error方法或对象,我想不出这样的方法。

    你可以设计一个使用系统比较的解决方案,但实际上,这会以更多的代码行来结束,并且大大增加了复杂性。

        12
  •  -3
  •   nawfal Donny V.    11 年前

    我觉得你很亲近。

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

    现在,这将编译,但是如果 myArgument 是空的,这是我测试的一部分。我可以像这样添加一个显式的空校验:

    您只需要反向调用equals的对象,就可以使用一种优雅的空安全方法。

    default(T).Equals(myArgument);