【问题标题】:Why does Assert.AreEqual on custom struct with implicit conversion operator fail?为什么带有隐式转换运算符的自定义结构上的 Assert.AreEqual 会失败?
【发布时间】:2016-11-24 19:17:15
【问题描述】:

我创建了一个自定义结构来表示金额。它基本上是decimal 的包装。它有一个隐式转换运算符将其转换回decimal

在我的单元测试中,我断言 Amount 等于原始十进制值,但测试失败。

[TestMethod]
public void AmountAndDecimal_AreEqual()
{
    Amount amount = 1.5M;

    Assert.AreEqual(1.5M, amount);
}

当我使用 int 时(我没有为其创建转换运算符),测试确实成功。

[TestMethod]
public void AmountAndInt_AreEqual()
{
    Amount amount = 1;

    Assert.AreEqual(1, amount);
}

当我悬停AreEqual 时,它显示第一个解析为

public static void AreEqual(object expected, object actual);

第二个导致

public static void AreEqual<T>(T expected, T actual);

看起来int1 隐式转换为Amount,而decimal1.5M 不是。

我不明白为什么会这样。我的预期正好相反。第一个单元测试应该能够将decimal 转换为Amount

当我向int 添加隐式转换时(这没有意义),第二个单元测试也失败了。所以添加一个隐式转换运算符会破坏单元测试。

我有两个问题:

  1. 对此行为的解释是什么?
  2. 如何修复 Amount 结构以便两个测试都能成功?

(我知道我可以更改测试以进行显式转换,但如果我不是绝对必须这样做,我不会)

我的 Amount 结构(只是显示问题的最小实现)

public struct Amount
{
    private readonly decimal _value;

    private Amount(decimal value)
    {
        _value = value;
    }

    public static implicit operator Amount(decimal value)
    {
        return new Amount(value);
    }

    public static implicit operator decimal(Amount amount)
    {
        return amount._value;
    }
}

【问题讨论】:

  • 您的两个运营商都是implicit。那么,应该是AreEqual&lt;decimal&gt; 还是AreEqual&lt;Amount&gt;
  • @PetSerAl 是对的。如果您使用AreEqual&lt;decimal&gt;AreEqual&lt;Amount&gt;,它将通过。

标签: c# unit-testing struct implicit-conversion


【解决方案1】:

当您可以双向转换 implicitly 时,就会发生不好的事情,这就是一个例子。

由于隐式转换,编译器能够选择具有相等值的Assert.AreEqual&lt;decimal&gt;(1.5M, amount);Assert.AreEqual&lt;Amount&gt;(1.5M, amount);。*

由于它们是相等的,因此推理不会选择任何重载。

由于没有通过推理选择的重载,因此也不会将其列入选择最佳匹配的列表,并且只有 (object, object) 表格可用。所以它是被选中的。

对于Assert.AreEqual(1, amount),由于存在从intAmount 的隐式转换(通过隐式int->decimal),但没有从Amountint 的隐式转换,编译器认为“显然它们意味着Assert.AreEqual&lt;Amount&gt;() here"†,所以它被选中了。

您可以使用Assert.AreEqual&lt;Amount&gt;()Assert.AreEqual&lt;decimal&gt;() 显式选择重载,但如果可能的话,您最好将其中一个转换“缩小”为explicit,因为您的此功能struct 会再次伤害你。 (单元测试发现缺陷的欢呼声)。


*另一个有效的重载选择是选择Assert.AreEqual&lt;object&gt;,但它从未被推理选择,因为:

  1. 两个被拒绝的重载都被认为更好。
  2. 它总是被采用object 的非泛型形式击败。

因此只能通过在代码中包含&lt;object&gt; 来调用它。

†编译器将对其所说的所有内容视为意义明显或完全不可理解。也有这样的人。

【讨论】:

  • 感谢您的详尽回答。我的情况是,implicit 转换回decimal 并不是真正需要的,所以我将其更改为explicit。不过我想知道,我怎么能自己找出为什么 AreEqual(object, object) 被调用。没有编译器错误,但它并没有达到我的预期。有什么建议吗?你能以某种方式看到编译器的某种决策树吗?
  • @comecme 如果我对重载选择感到困惑,我首先编写一些与签名匹配的方法(所以在这里我可能会创建一个 AssertAreEqual(object x, object y)AssertAreEqual&lt;T&gt;(T x, T y)。然后确认我获得相同的行为(采用对象形式),然后删除选择的那个,看看会发生什么:CS0411 The type arguments for method 'AssertAreEqual&lt;T&gt;(T, T)' cannot be inferred from the usage. Try specifying the type arguments explicitly.。想知道为什么不能推断出它们将是一个很大的线索。现在,当我明确指定时,我已经这个问题……
  • …我应该明确说&lt;decimal&gt;还是明确说&lt;Amount&gt;?啊哈!我不知道,那么编译器怎么办?问题已识别。但最大的问题是,我是一双全新的眼睛,看着你的代码,没有一个计划,这使得最后的精神跳跃更容易实现。当它是自己的代码时,就会有一个计划,并且很难理解为什么对制定该计划的人来说显而易见的东西对编译器来说不是。唯一能做的就是去喝杯咖啡,四处走走,然后再回来看看。
猜你喜欢
  • 1970-01-01
  • 2014-02-02
  • 2010-12-07
  • 1970-01-01
  • 2012-09-14
  • 1970-01-01
  • 1970-01-01
  • 2011-09-09
相关资源
最近更新 更多