从 IComparable.CompareTo() 抛出是否可以接受?

Is it acceptable to throw from IComparable.CompareTo()?

示例(货币是一个枚举):

public struct MoneyQuantity: IComparable<MoneyQuantity>
{
    public Currency Currency { get; }
    public decimal  Amount   { get; }

    public MoneyQuantity(Currency currency, decimal amount)
    {
        Currency = currency;
        Amount   = amount;
    }

    public int CompareTo(MoneyQuantity that)
        => Currency == that.Currency ?
            Amount.CompareTo(that.Amount) :
            throw new InvalidOperationException("Can't compare quantities of money in different currencies");
}

这可以接受吗?或者当一个类型是 IComparable 时,是否期望该类型的任何两个实例都应该是可比较的,并且如果客户端代码试图比较它们时不应抛出异常?

我认为这是一个问题,而不是不可接受的。显然,抛出 Sort 的东西或任何调用 CompareTo() 的操作(包括用户可能没有意识到调用它的东西)将不如 "just work" 有用,所以必须详细记录可能性(错误消息有帮助)。但另一方面,它比 "just works" 在某种程度上可能不太正确的东西要好。

您是在强迫用户避免此类比较或自己想办法进行比较(例如,转换为一致的货币进行比较),但这也迫使他们确保他们在适用的期限内这样做他们(例如,根据他们实际使用的费率进行此类转换)。总的来说,这可能是您能做的最好的事情。

我过去玩过建模 "units",例如这里的货币,作为类型,然后您可以使 MoneyQuantity 对其中一种类型通用。 (你可以做很多花哨的尝试来确保它被参数化的类型只是你想要支持的货币)。这立即防止了问题(因为 MoneyQuantity<Dollar> 可以与其他 MoneyQuantity<Dollar> 相媲美,但不能与 MoneyQuantity<PoundSterling> 或任何其他类型相媲美)。

例如下面的 Console.WriteLine 行无法编译,但其余的都很好:

using System;

namespace PlayAreaCSCon
{
    class Program
    {
        static void Main(string[] args)
        {
            var d = new MoneyQuantity<Dollar>(1);
            var p = new MoneyQuantity<PoundSterling>(1);

            Console.WriteLine(p.CompareTo(d));
        }
    }
    public abstract class Currency
    {
        protected Currency() { }
    }
    public class Dollar : Currency
    {
        public Dollar() : base() { }
    }
    public class PoundSterling : Currency
    {
        public PoundSterling() : base() { }
    }
    public struct MoneyQuantity<TCurrency> : 
           IComparable<MoneyQuantity<TCurrency>>
           where TCurrency : Currency, new()
    {
        public decimal Amount { get; }

        public MoneyQuantity(decimal amount)
        {
            Amount = amount;
        }

        public int CompareTo(MoneyQuantity<TCurrency> that)
                => Amount.CompareTo(that.Amount);
    }
}

(当它们更多地用作标记时,我对在上面构建的 DollarPoundSterling 并不完全满意,但它允许我们使用 new()防止 MoneyQuantity<Currency>s 成为有效类型的约束)

如果使用 C# 7.2 或更高版本,您可以将 Currency 的构造函数 private protected 作为防止人们创建恶意 Currency 类型的守卫之一。

当然,这可能并不适合您的问题领域的所有部分,所以不要在没有权衡利弊的情况下直接投入并执行此操作。当然,F#的单位看起来好多了,但我一气之下就没用过。