运算符==不能应用于C#中的泛型类型吗?


326

根据MSDN中==运营商的文档,

对于预定义的值类型,相等运算符(==)如果其操作数的值相等,则返回true,否则返回false。对于字符串以外的引用类型,如果==的两个操作数引用同一对象,则==返回true。对于字符串类型,==比较字符串的值。用户定义的值类型可能会使==运算符重载(请参阅运算符)。用户定义的引用类型也可以,尽管 默认情况下==的行为与上述预定义和用户定义的引用类型相同。

那么,为什么此代码片段无法编译?

bool Compare<T>(T x, T y) { return x == y; }

我收到错误运算符'=='不能应用于类型'T'和'T'的操作数。我不知道为什么,因为据我所知,该==操作符是为所有类型预定义的?

编辑:谢谢大家。起初我没有注意到该声明仅与引用类型有关。我还认为为所有值类型提供了逐位比较,现在我知道这是正确的。

但是,如果我使用的是引用类型,==操作员会使用预定义的引用比较吗?如果定义了一个类型,它会使用操作员的重载版本吗?

编辑2:通过反复试验,我们了解到,==当使用无限制的泛型类型时,运算符将使用预定义的引用比较。实际上,编译器将使用它可以为受限类型参数找到的最佳方法,但是不再赘述。例如,true即使Test.test<B>(new B(), new B())被调用,下面的代码也将始终print :

class A { public static bool operator==(A x, A y) { return true; } }
class B : A { public static bool operator==(B x, B y) { return false; } }
class Test { void test<T>(T a, T b) where T : A { Console.WriteLine(a == b); } }

再次查看我的答案以获取后续问题的答案。
乔瓦尼·加尔博

理解即使没有泛型,在某些==相同类型的两个操作数之间也不允许某些类型的类型可能是有用的。对于struct不重载的类型(“预定义”类型除外),这是正确的operator ==。作为一个简单的示例,请尝试以下操作:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
Jeppe Stig Nielsen

继续我自己的旧评论。例如(请参阅其他线程),使用var kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;,您将无法检查,kvp1 == kvp2因为它KeyValuePair<,>是一个结构,它不是C#的预定义类型,并且不会重载operator ==。举一个var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;无法使用的示例e1 == e2(这里有一个嵌套结构List<>.Enumerator"List`1+Enumerator[T]"由运行时调用),它不会重载==)。
Jeppe Stig Nielsen

RE:“那么为什么这个代码片段无法编译?” boolvoid
-......

1
@ BrainSlugs83感谢您捕获一个10岁的bug!
Hosam Aly

Answers:


143

“ ...默认情况下,==的行为对于预定义和用户定义的引用类型都如上所述。”

类型T不一定是引用类型,因此编译器无法进行此假设。

但是,这将编译,因为它更加明确:

    bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }

继续回答另一个问题:“但是,如果我使用的是引用类型,==运算符将使用预定义的引用比较,还是如果一个类型定义了该类型,它将使用运算符的重载版本吗?”

我本以为泛型中的==会使用重载版本,但是下面的测试进行了演示。有趣的...我很想知道为什么!如果有人知道,请分享。

namespace TestProject
{
 class Program
 {
    static void Main(string[] args)
    {
        Test a = new Test();
        Test b = new Test();

        Console.WriteLine("Inline:");
        bool x = a == b;
        Console.WriteLine("Generic:");
        Compare<Test>(a, b);

    }


    static bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }
 }

 class Test
 {
    public static bool operator ==(Test a, Test b)
    {
        Console.WriteLine("Overloaded == called");
        return a.Equals(b);
    }

    public static bool operator !=(Test a, Test b)
    {
        Console.WriteLine("Overloaded != called");
        return a.Equals(b);
    }
  }
}

输出量

内联:重载==已调用

通用:

按任意键继续 。。。

跟进2

我确实要指出,将我的比较方法更改为

    static bool Compare<T>(T x, T y) where T : Test
    {
        return x == y;
    }

导致重载的==运算符被调用。我想如果不指定类型(如where),编译器就无法推断它应该使用重载运算符...尽管我认为即使不指定类型,编译器也将具有足够的信息来做出该决定。


谢谢。我没有注意到该语句仅涉及引用类型。
Hosam Aly

4
回复:跟进2:实际上,编译器将链接它找到的最佳方法,在本例中为Test.op_Equal。但是,如果您有一个从Test派生并覆盖该运算符的类,则仍将调用Test的运算符。
Hosam Aly

4
我想指出的一个好习惯是,您应该始终在覆盖的Equals方法(而不是在==运算符中)内进行实际比较。
2009年

11
重载解析发生在编译时。因此,当我们==在泛型类型T和之间T进行操作时,考虑到所受到的约束T(存在一个特殊的规则,即永远不会为此填充值类型(这将给出无意义的结果)),可以找到最佳的重载。某些保证它是引用类型的约束)。在“ 跟进2”中,如果您引入了DerivedTest对象,并且DerivedTest派生自对象,Test但又引入了新的重载==,您将再次遇到“问题”。哪个重载被调用,在编译时被“烧录”到IL中。
Jeppe Stig Nielsen

1
奇怪的是,这似乎适用于常规引用类型(您希望此比较基于引用相等),但是对于字符串,它似乎也使用引用相等-因此,您最终可以比较2个相同的字符串并具有==(在具有类约束的通用方法)说它们是不同的。
JonnyRaa

291

就像其他人所说的那样,它仅在将T约束为引用类型时才有效。没有任何约束,您可以与null进行比较,但只能与null进行比较-对于不可为null的值类型,该比较将始终为false。

最好不要使用Equals,而最好使用IComparer<T>--如果没有更多信息,EqualityComparer<T>.Default则是一个不错的选择:

public bool Compare<T>(T x, T y)
{
    return EqualityComparer<T>.Default.Equals(x, y);
}

除了其他以外,这避免了装箱/铸造。


谢谢。我试图编写一个简单的包装器类,所以我只想将操作委托给实际的包装器成员。但是了解EqualityComparer <T> .Default无疑为我增加了价值。:)
Hosam Aly

小一点,乔恩;您可能想在我的帖子中注明关于re pobox vs yoda的评论。
Marc Gravell

4
使用EqualityComparer <T>的不错提示
chakrit 2011年

1
+1指出它可以与null进行比较,并且对于非null值类型,它将始终为false
Jalal Said

@BlueRaja:是的,因为对于与空文字的比较有特殊的规则。因此,“在没有任何约束的情况下,您可以比较null,但只能比较null”。已经在答案中了。那么,为什么这不正确呢?
乔恩·斯基特

41

通常,EqualityComparer<T>.Default.Equals应该使用任何已实现的IEquatable<T>或具有明智Equals实现的方法来完成这项工作。

但是,如果==Equals是出于某种原因实现方式不同,那么我的工作一般运营商应该是有用的; 它支持以下版本的操作员版本:

  • 等于(T值1,T值2)
  • NotEqual(T值1,T值2)
  • 大于(T值1,T值2)
  • 小于(T值1,T值2)
  • GreaterThanOrEqual(T值1,T值2)
  • LessThanOrEqual(T值1,T值2)

非常有趣的图书馆!:)(旁注:我可能建议使用www.yoda.arachsys.com的链接,因为该pobox之一被我的工作场所的防火墙阻止了吗?其他人可能也面临同样的问题。)
Hosam Aly

这个想法是,pobox.com/~skeet始终指向我的网站-即使它在其他地方移动。为了后代,我倾向于通过pobox.com发布链接-但是您目前可以代替yoda.arachsys.com。
乔恩·斯基特

pobox.com的问题在于它是基于Web的电子邮件服务(或公司的防火墙说),因此被阻止了。这就是为什么我不能跟随它的链接。
Hosam Aly

“但是,如果由于某些原因==和Equals的实现方式有所不同,” –天哪!真是个好东西!也许我只需要看一个相反的用例,但是具有不同等式语义的库可能会遇到比泛型麻烦更大的问题。
爱德华·布雷

@EdwardBrey你没看错;如果编译器可以执行该命令将很好,但是...
Marc Gravell

31

这么多答案,为什么没有一个解释呢?(乔凡尼明确提出的问题)...

.NET泛型不像C ++模板那样起作用。在C ++模板中,过载解析是在知道实际模板参数之后发生的。

在.NET泛型(包括C#)中,将在不知道实际泛型参数的情况下进行重载解析。编译器可以用来选择要调用的函数的唯一信息来自通用参数的类型约束。


2
但是为什么编译器不能将它们视为通用对象?毕竟==适用于所有类型,无论是引用类型还是值类型。这应该是我不认为您回答的问题。
nawfal

4
@nawfal:实际上不,==并非对所有值类型都有效。更重要的是,对于所有类型,它的含义并不相同,因此编译器不知道如何处理它。
Ben Voigt

1
Ben,哦,是的,我错过了我们可以不用任何方法就可以创建的自定义结构==。您是否也可以在回答中包含这一部分,因为我想这就是重点
nawfal

12

编译器无法知道T不能是结构(值类型)。因此,您必须告诉它它只能是我认为的参考类型:

bool Compare<T>(T x, T y) where T : class { return x == y; }

这是因为,如果T可以是值类型,则在某些情况下x == y可能格式不正确-如果类型没有定义运算符==。同样的事情也会发生,这更加明显:

void CallFoo<T>(T x) { x.foo(); }

那也失败了,因为您可以传递没有函数foo的类型T。C#强制您确保所有可能的类型始终具有函数foo。这是通过where子句完成的。


1
感谢您的澄清。我不知道值类型不支持开箱即用的==运算符。
Hosam Aly

1
Hosam,我使用gmcs(mono)进行了测试,并且它总是比较引用。(即,它不为T使用可选定义的operator ==)
Johannes Schaub-litb

这个解决方案有一个警告:operator ==不能重载;看到这个StackOverflow问题
Dimitri C. 2010年

8

似乎没有类约束:

bool Compare<T> (T x, T y) where T: class
{
    return x == y;
}

应该认识到,虽然class约束Equals==运算符中是从继承的Object.Equals,但结构的继承自ValueType.Equals

注意:

bool Compare<T> (T x, T y) where T: struct
{
    return x == y;
}

还给出了相同的编译器错误。

到目前为止,我还不明白为什么编译器会拒绝进行值类型相等运算符比较。我确实知道,这是可行的:

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}

你知道我是一个C#新手。但是我认为它失败了,因为编译器不知道该怎么办。由于尚不知道T,因此如果允许值类型,该做什么取决于类型T。对于引用,无论T如何,都将仅比较引用。如果执行.Equals,则仅调用.Equal。
Johannes Schaub-litb

但是,如果对值类型执行==,则不必不必要地实现该值类型。
Johannes Schaub-litb

这很有道理,litb :)用户定义的结构可能不会重载==,因此编译器会失败。
乔恩·林贾普

2
第一个比较方法并没有使用Object.Equals,而是测试参考平等。例如,Compare("0", 0.ToString())将返回false,因为参数将引用不同的字符串,这两个字符串的唯一字符均为零。
supercat

1
最后一个小技巧-您尚未将其限制为结构,因此NullReferenceException可能会发生。
Flynn1179 '17

6

在我的情况下,我想对相等运算符进行单元测试。我需要在相等运算符下调用代码,而无需显式设置泛型类型。建议EqualityComparer作为EqualityComparer被叫Equals方法没有帮助,但对等于运算符没有帮助。

这就是我通过构建一个通用类型来使它工作的方式LINQ。它为==!=运算符调用正确的代码:

/// <summary>
/// Gets the result of "a == b"
/// </summary>
public bool GetEqualityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.Equal(paramA, paramB);
    // compile it
    var invokeEqualityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeEqualityOperator(a, b);
}

/// <summary>
/// Gets the result of "a =! b"
/// </summary>
public bool GetInequalityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.NotEqual(paramA, paramB);
    // compile it
    var invokeInequalityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeInequalityOperator(a, b);
}

4

没有此的MSDN连接进入这里

Alex Turner的回复开始于:

不幸的是,此行为是设计使然,还没有一种简单的解决方案可以将==与可能包含值类型的类型参数一起使用。


4

如果要确保调用自定义类型的运算符,则可以通过反射来实现。只需使用您的通用参数获取类型,然后检索所需运算符的MethodInfo(例如op_Equality,op_Inequality,op_LessThan ...)。

var methodInfo = typeof (T).GetMethod("op_Equality", 
                             BindingFlags.Static | BindingFlags.Public);    

然后使用MethodInfo的Invoke方法执行运算符,并将对象作为参数传递。

var result = (bool) methodInfo.Invoke(null, new object[] { object1, object2});

这将调用您的重载运算符,而不是由通用参数上的约束所定义的运算符。可能不切实际,但是当使用包含几个测试的通用基类时,可以方便地对操作员进行单元测试。


3

我编写了以下函数,以查看最新的msdn。它可以轻松比较两个对象xy

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}

4
您可以摆脱布尔return ((IComparable)(x)).CompareTo(y) <= 0;
运算,

1

bool Compare(T x, T y) where T : class { return x == y; }

上面的方法将起作用,因为在用户定义的引用类型的情况下==会得到照顾。
对于值类型,==可以被覆盖。在这种情况下,也应定义“!=“。

我认为这可能是原因,它不允许使用“ ==”进行一般比较。


2
谢谢。我相信引用类型也可以覆盖运算符。但是故障原因现在很清楚。
Hosam Aly

1
==令牌用于两个不同的运算符。如果对于给定的操作数类型,存在相等运算符的兼容重载,则将使用该重载。否则,如果两个操作数都是彼此兼容的引用类型,则将使用引用比较。请注意,在上述Compare方法中,编译器无法说出第一个含义适用,但是可以说出第二个含义适用,因此,即使重载了equals-check运算符(例如,如果是type ==令牌也将使用后者。TString
超级猫

0

.Equals()同时工作对我来说TKey是一个泛型类型。

public virtual TOutputDto GetOne(TKey id)
{
    var entity =
        _unitOfWork.BaseRepository
            .FindByCondition(x => 
                !x.IsDelete && 
                x.Id.Equals(id))
            .SingleOrDefault();


    // ...
}

x.Id.Equals不是id.Equals。据推测,编译器对的类型有所了解x
Hosam Aly
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.