为什么我的Equals方法没有被调用?

7
我正在学习Kent Beck的《测试驱动开发》这本书,作为一项学术练习,但是我使用MSpec来编写测试。在跟随实际例子时,我喜欢引入一些变化,以便我不能简单地把文字复制下来,我发现这样做我倾向于遇到问题并解决它们,从而学到更多知识。我相信这是其中一个场合。
我已经完成了Kent 'money'示例的部分内容。这是我拥有的类结构: enter image description here 我有以下两个测试环境:
[Subject(typeof(Money), "Equality")]
public class when_comparing_different_classes_for_equality
{
    Because of = () => FiveFrancs = new Franc(5, "CHF");
    It should_equal_money_with_currency_set_to_francs = () => FiveFrancs.Equals(new Money(5, "CHF")).ShouldBeTrue();
    static Franc FiveFrancs;
}

[Subject(typeof(Franc), "multiplication")]
public class when_multiplying_a_franc_amount_by_an_integer
{
    Because of = () => FiveFrancs = new Franc(5, null);
    It should_be_ten_francs_when_multiplied_by_2 = () => FiveFrancs.Times(2).ShouldEqual(Money.Franc(10));
    It should_be_fifteen_francs_when_multiplied_by_3 = () => FiveFrancs.Times(3).ShouldEqual(Money.Franc(15));
    static Franc FiveFrancs;
}

Times()方法返回一个新的Money类型对象,其中包含结果,即这些对象是不可变的。第一个上下文通过了,表明Equals方法按预期工作,即它忽略对象类型,只要它们都继承自Money,并且仅比较金额和货币字段是否相等。第二个上下文失败,输出类似于以下内容:

Machine.Specifications.SpecificationException
  Expected: TDDByExample.Money.Specifications.Franc:[15]
  But was:  TDDByExample.Money.Specifications.Money:[15]
   at TDDByExample.Money.Specifications.when_multiplying_a_franc_amount_by_an_integer.<.ctor>b__2() in MoneySpecs.cs: line 29

平等被定义为数值和货币相同;对象的实际类型应该被忽略,因此预期的结果是,如果我使用Money或Franc对象测试平等性,只要金额和货币字段相同,就不应该有任何影响。然而,事情并没有按计划进行。调试时,我的Equals()方法甚至没有被调用。显然,我在这里有些东西没有理解。我确信当我知道解决方案时,它会变得非常明显,但我看不到它。有人能提供建议,告诉我需要做什么才能使这个工作吗?
下面是Equals()的实现:
public bool Equals(Money other)
{
    return amount == other.amount && currency == other.currency;
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj))
        return false;
    if (ReferenceEquals(this, obj))
        return true;
    return Equals(obj as Money);
}

经过深思熟虑,我认为这可能是一个 MSpec 问题。MSpec 比较对象类型,并根据类型不相同而失败比较。只有当类型相等时,MSpec 才会继续使用基于值的比较。这就是为什么我的 Equals 方法从未被调用的原因。然而,我认为 Liskov 表示如果我的相等定义允许,则应该能够将 Money 与 Franc 进行比较。因此,我已经向 MSpec 项目提出了一个问题。https://github.com/machine/machine.specifications/issues/200 - Tim Long
@Anthony 我更喜欢 Whitesmiths 风格的缩进(花括号缩进)。我并不介意你改变我的缩进,我很乐意让你的编辑保留下来,但另一方面,覆盖其他人的偏好似乎有点自以为是。是否有我不知道的指南? - Tim Long
哦,对不起,我刚才完全假设你的空格/制表符混合有问题之类的。我从SO的一般糟糕的代码块格式化中变得眼花缭乱。 - Anthony Mastrean
PS. 你可能是地球上唯一使用 Whitesmith 风格大括号的 C# 程序员,但是你在个人资料照片中戴着蝴蝶领结,看起来确实是一个非常独特的人物 :D - Anthony Mastrean
“蝴蝶结很酷,你知道的!”(第12任博士)。我在80年代上大学时学到了这种缩进风格,这是我发现非常难改掉的习惯之一。我的编译器编写讲师声称,由于大括号定义了一个块,所以它们是块的一部分,应该与其缩进。他在这一点上非常清楚,我们不敢反驳;-) - Tim Long
2个回答

2
一个完全完成的等式实现看起来会像这样。看看它是否有帮助。
protected bool Equals(Money other)
{
    // maybe you want this extra param to Equals?
    // StringComparison.InvariantCulture
    return amount == other.amount 
      && string.Equals(currency, other.currency);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    var other = obj as Money;
    return other != null && Equals(other);
}

public override int GetHashCode()
{
    unchecked
    {
        return (amount * 997) ^ currency.GetHashCode();
    }
}

public static bool operator ==(Money left, Money right)
{
    return Equals(left, right);
}

public static bool operator !=(Money left, Money right)
{
    return !Equals(left, right);
}

只有在“amount”和“currency”字段是只读的(或保证永远不会更改)时,这种定义才是正确的。要求从“GetHashCode”返回的值在实例的生命周期内是不可变的。 - Enigmativity
@Enigmativity:确实,如上述问题所述:“即对象是不可变的”。 - spender
尽管这是一个非常彻底和完整的相等实现,但它并没有回答OP问题,即为什么它在第一次调用时没有被调用。 - Xint0
@spender - 只是提供一些澄清。不可变性并没有得到很好的宣传,我见过很多人通过创建可变的GetHashCode实现而弄乱代码。 - Enigmativity

0

正如@Harrison所评论的那样,问题在于您的Franc类的Times方法的结果类型。它未能通过测试规范,因为它返回一个Money对象,但规范期望一个Franc实例。要么更改规范以要求一个Money对象,要么重写Times方法以返回一个Franc实例。

更新

在更新测试规范后,您更改了以下行:

It should_be_ten_francs_when_multiplied_by_2 = () => FiveFrancs.Times(2).ShouldEqual(Money.Franc(10));
It should_be_fifteen_francs_when_multiplied_by_3 = () => FiveFrancs.Times(3).ShouldEqual(Money.Franc(15));

但是,在属性上,主题的类型仍然是:

[Subject(typeof(Franc), "multiplication")]

所以我认为它仍然期望一个 Franc 实例而不是一个 Money 实例。


[Subject]属性仅用于文档,对代码没有任何影响。这可能看起来很奇怪,但示例正在进行重构,并朝着使用单个通用Money类取代Franc和Dollar类的方向发展。因此,这是一种有点牵强的情况。我可以简单地剪切到重构完成的代码,但我想了解为什么出了问题。我仍然不明白为什么它不起作用。 - Tim Long
我对[Subject]属性不是很确定,但我能看到的唯一区别是对象的类型。你尝试过将[Subject]属性更改为使用Money类型吗? - Xint0
我尝试更改属性,但没有任何效果。 我可以百分之百确定它除了文档之外没有任何效果(实际上,您可以完全省略它)。 然而,我认为我离解决方案更近了。 我注意到静态字段声明为Franc而不是Money。 将其更改为Money后,测试仍然失败,但现在我有了更清晰的问题指示: 期望值:TDDByExample.Money.Specifications.Franc:[15 CHF] 但是是:TDDByExample.Money.Specifications.Money:[15] - Tim Long
@TimLong:糟糕。法郎构造函数应该像这样,对吧? public Franc(int amount):base(amount,"CHF") { }? :-) - spender
@TimLong 很好!不完全是我想的,但很高兴能帮助你击中要害。 - Xint0
显示剩余5条评论

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