为什么在不装箱的情况下不能重写值类型的Equals()方法?

9

我知道我可以通过添加自己的Equals实现来避免装箱。

public struct TwoDoubles
{
    public double m_Re;
    public double m_Im;

    public TwoDoubles(double one, double two)
    {
        m_Re = one;
        m_Im = two;
    }

    public override bool Equals(object ob)
    {
           return Equals((TwoDoubles)ob);
    }
    public bool Equals(TwoDoubles ob)
    {
        TwoDoubles c = ob;
        return m_Re == c.m_Re && m_Im == c.m_Im;
    }
}

我不太能称其为重写(override),倒更像是重载(overload)。在运行时的神奇作用下,根据调用者类型正确地调用Equals()实现。
为什么我不能重写并将参数类型更改为TwoDoubles,让运行时的强制装箱按需发生?这是因为C#不支持参数逆变(如果是这个原因,那为什么不支持呢...从object o = new TwoDoubles()看来只是一个小小的步骤)?
更新 澄清一下,object是结构的继承层次结构的一部分。为什么我们不能指定更具体的类型作为参数来重写来自更抽象类型的实现?这样我们就可以编写:
 public override bool Equals(TwoDoubles ob)
 {
        TwoDoubles c = ob;
        return m_Re == c.m_Re && m_Im == c.m_Im;    
 }

即使将变量装箱为对象类型,当变量为TwoDouble时应调用哪个函数。

1
如果ob在重载函数中不是TwoDoubles,则会抛出异常。在另一个函数中检查类型是没有意义的。 - Magnus
1
@Magnus - 已修复。我匆忙出门时疏忽了。 - P.Brian.Mackey
4
请注意,由于你覆盖了 Equals(object) 方法,你也应该覆盖 GetHashCode() 方法。 - Jeff Mercado
@Magnus - 异常情况怎么可能出现呢?任何非双倍变量都不可能调用twodoubles的Equals方法吧? - P.Brian.Mackey
由于您对问题进行了更改,它现在无法实现。 - Magnus
你的问题基本上是“为什么Object类中没有一个通用的Equals方法,它的当前Equals方法会调用同一类型的方法”? - Magnus
3个回答

13

为什么我不能覆盖并将参数类型更改为TwoDoubles?

因为那样不安全!

class B
{
  public virtual void M(Animal animal) { ... }
}
class D : B
{
  public override void M(Giraffe animal) { ... }
}

B b = new D();
b.M(new Tiger());

现在你把老虎传给了一个只接受长颈鹿的方法!

在你的情况下也是一样。你正在使用一个仅能接受结构体的方法来覆盖一个可以接受任何对象的方法,这是不安全的类型转换。

这是因为 C# 不支持参数类型逆变吗?

不是,这是因为你要求参数类型的协变,而这是不安全的类型转换。

C# 也不支持参数类型逆变,但这不是你所要求的。


在您的博客中,您解释了协变/逆变/不变是指映射而非类型。那么,在这种情况下,映射是什么?http://blogs.msdn.com/b/ericlippert/archive/2009/11/30/what-s-the-difference-between-covariance-and-assignment-compatibility.aspx - P.Brian.Mackey
2
@P.Brian.Mackey:好问题!考虑关系“具有参数类型X的方法可以合法地覆盖具有参数类型Y的方法”和关系“类型Y与类型X可赋值兼容”。如果第一个关系每次都满足第二个关系,则第一个关系在参数类型上是逆变的。更准确地说:投影是从“类型T”到“具有参数类型T的方法”的投影,而正是这个投影这两个关系方面是逆变的。 - Eric Lippert
1
@P.Brian.Mackey:稍微正式一些:假设我们在集合T上有一个关系R1;也就是说,一个函数R1(T, T)-->bool。假设我们在集合M上有一个关系R2,R2(M, M)-->bool。并且假设我们有一个投影P,它是P(T)-->M。如果R1(x, y)为真意味着R2(P(y), P(x))也为真,则P是逆变的。在这种情况下,R1是“类型之间的赋值兼容性”,R2是“一个方法覆盖另一个方法的合法性”,而P是“取一个类型并创建一个具有该类型参数的方法”。 - Eric Lippert
请原谅我的无知,正式证明难道不是加强了我们正在处理逆变而不是协变的想法吗?如果是,类型安全问题仍然存在吗? - P.Brian.Mackey
2
@P.Brian.Mackey:那不是一个证明,而是对逆变性的更精确定义。如果你的问题是“使用参数类型逆变的虚方法重写是否类型安全?”,答案是是的,它将是类型安全的。只是它不被CLR或C#(或者说C++)支持。C#支持两种参数类型逆变:首先,泛型委托类型转换的逆变,其次,将方法组转换为委托类型时参数类型的逆变。这两种情况都要求变化的类型为引用类型。 - Eric Lippert

2
您可以更改Equals的参数(重载),就像您所做的那样,必要时会进行装箱(即每当调用Equals(object)时)。
因为所有内容都继承(或隐式地通过装箱)自object,您无法阻止人们在您的类型上使用Equals(object)。但是,这样做会得到一个装箱调用。
如果您控制调用代码,则始终使用您的新重载选项,即Equals(TwoDouble) 请注意,正如一个评论者已经说过的那样,您的代码有些不正确,请改用以下代码:
public override bool Equals(object ob)
{
  if (ob is TwoDoubles)
    return Equals((TwoDoubles)ob);
  else
    return false;
}
public bool Equals(TwoDoubles c)
{
  return m_Re == c.m_Re && m_Im == c.m_Im;
}

编辑根据明智的建议,您应该使用IEquatable接口来完成相同的任务,在这种情况下是IEquatable<TwoDouble>(请注意,与我们所做的签名匹配,无需更改代码)


1
第二个等号上的转换并不是必要的。 - digEmAll
6
如果你想要一个通用的 Equals 方法,我建议你实现 IEquatable<T> 接口。 - Magnus
这修复了一个实现问题。对于那个类型,问题已更新。它并没有解决我的问题。 - P.Brian.Mackey

2
如果按照您的建议进行操作,只需要public override bool Equals(TwoDoubles c) { return m_Re == c.m_Re && m_Im == c.m_Im; }就足以覆盖Equals(object)方法,那么这段代码会做什么呢?
TwoDoubles td = new TwoDoubles();
object o = td;
bool b = o.Equals(new object()); // Equals(object) overridden with Equals(TwoDouble)

第三行应该怎么处理?

  • 它应该调用 Equals(TwoDouble) 吗?如果是,参数是什么?使用所有零的默认 TwoDouble 将违反有关相等性的规则。
  • 它应该抛出类型转换异常吗?我们正确遵循了方法签名,所以不应该发生这种情况。
  • 它应该总是返回 false 吗?现在编译器必须意识到 Equals 方法的含义,并且必须对其进行与其他方法不同的处理。

没有好的方法来实现它,它很快会引起更多问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接