【问题标题】:Is it acceptable to throw from IComparable.CompareTo()?从 IComparable.CompareTo() 抛出是否可以接受?
【发布时间】:2018-08-23 10:49:24
【问题描述】:

示例(货币是枚举):

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 时,该类型的任何两个实例都应该是可比较的,并且如果客户端代码尝试比较它们,则不应抛出异常?

【问题讨论】:

  • 我认为这段代码看起来不错,我认为问题是其他使用 MoneyQuantity 的开发人员是否期望一件事或另一件事。例如,如果我提供货币为欧元的 MoneyQuantity 和货币为美元的 MoneyQuantity,我是否被告知它会进行转换以比较它们?您提供了有关为什么它是无效操作的信息,因此这看起来完全合法并解释了我的情况。
  • 您可以使货币具有可比性,例如,所有美元金额的比较小于所有英镑金额——显然,不同货币的两种金额不应该相等。问题是这样的事情对呼叫者是否有用。可以说,当他们得到一组无与伦比的货币,并试图进行比较时,已经出了问题。是抛出异常还是执行一些一致的比较更好取决于您的用例。例如,出于显示目的,任何排序都比没有好。

标签: c# .net exception icomparable


【解决方案1】:

我认为这是一个问题,而不是不可接受的。显然,抛出Sort 的东西或调用CompareTo() 的任何操作(包括用户可能没有意识到调用它的东西)将不如“正常工作”的东西有用,所以这种可能性必须是有据可查(错误消息有帮助)。但另一方面,它比以一种可能不太正确的方式“正常工作”的东西要好。

您迫使用户要么避免此类比较,要么自己想办法进行比较(例如,转换为一致的货币进行比较),但这也迫使他们确保按照适合他们的条件进行比较(例如,根据他们实际使用的费率进行此类转换)。这可能是你能做到的最好的平衡。

【讨论】:

    【解决方案2】:

    我过去曾尝试将“单位”(例如此处的货币)建模为类型,然后您可以将 MoneyQuantity 设为其中一种类型的泛型。 (你可以做很多花哨的尝试来确保它被参数化的类型只是你想要支持的货币)。这可以立即防止问题发生(因为MoneyQuantity&lt;Dollar&gt; 可以与其他MoneyQuantity&lt;Dollar&gt;s 相媲美,但不能与MoneyQuantity&lt;PoundSterling&gt; 或任何其他类型相媲美)。

    例如下面的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&lt;Currency&gt;s一个有效的类型)

    如果使用 C# 7.2 或更高版本,您可以将Currency 的构造函数private protected 作为防止人们创建流氓Currency 类型的一种保护措施。

    当然,这可能并不适合您的问题领域的所有部分,所以不要在没有权衡利弊的情况下直接跳槽去做。当然,F# 的单位看起来好多了,但我没有在愤怒中使用它们。

    【讨论】:

    • 这很适合某些用例,尽管我认为不是我的。它确实促使你的类的消费者不得不知道类型参数。
    • @Hammerite - 是的,我最初将其写为评论。它有点扩展为一个答案。我知道它并不适合所有人(希望你能看到它)。我只是认为尝试使用类型系统来强制执行这种约束是一种有趣的方式(不同的货币不能直接比较)。你可以在MoneyQuantity 下面引入一个非泛型基类,它允许一些运行时弹性,但你必须放弃struct。同样希望清楚的是,如果您想使用类型系统来执行此操作,则需要权衡取舍。
    • @Hammerite - 我有时会考虑按照domain Money:decimal units {Dollar, PoundsSterling} supports (Equality, Maths); 的方式提出一个语言功能,并让编译器吐出所有使其工作的样板。这对我来说看起来不错(以及为什么我在我的答案中尝试了类似的东西)但提供真正令人信服的用例(而不是“这不是很好”)是我失败的地方。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多