为什么比较返回一个整数

2022-08-22 00:00:00 c# java comparator comparable

我最近在SO聊天中看到了一次讨论,但没有明确的结论,所以我最终在那里询问了一下。

这是出于历史原因还是与其他语言的一致性?当查看各种语言的compareTo的签名时,它返回一个int

为什么它不返回枚举。例如,在C#中我们可以这样做:

enum CompareResult {LessThan, Equals, GreaterThan};

和:

public CompareResult CompareTo(Employee other) {
    if (this.Salary < other.Salary) {
         return CompareResult.LessThan;
    }
    if (this.Salary == other.Salary){
        return CompareResult.Equals;
    }
    return CompareResult.GreaterThan;
}

在Java中,枚举是在这个概念之后引入的(我不记得C#了),但它可以通过一个额外的类来解决,比如:

public final class CompareResult {
    public static final CompareResult LESS_THAN = new Compare();
    public static final CompareResult EQUALS = new Compare();
    public static final CompareResult GREATER_THAN = new Compare();

    private CompareResult() {}
}  

interface Comparable<T> {
    Compare compareTo(T obj);
}
我之所以这样问,是因为我认为int不能很好地表示数据的语义。

例如在C#中

l.Sort(delegate(int x, int y)
        {
            return Math.Min(x, y);
        });

及其在Java 8中的孪生兄弟

l.sort(Integer::min);

编译两者是因为Min/min尊重比较器接口的约定(接受两个int并返回一个int)。

显然,这两种情况的结果都不是预期的。如果返回类型为Compare,则会导致编译错误,从而迫使您实现"正确"行为(或者至少您知道自己在做什么)。

此返回类型丢失了很多语义(并且可能会导致难以找到的错误),那么为什么要这样设计它?


解决方案

[此答案适用于C#,但在某种程度上可能也适用于JAVA。]

这是出于历史、性能和可读性的原因。它可能会在两个方面提高性能:

  1. 实现比较的位置。通常,您只需返回"(lhs-rhs)"(如果值是数值类型)。但这可能很危险:请参见下面的内容!
  2. 调用代码可以使用<=>=来自然地表示对应的比较。与使用枚举相比,这将使用单个IL(因此使用处理器)指令(尽管有一种方法可以避免枚举的开销,如下所述)。

例如,我们可以按如下方式检查lhs值是否小于或等于rhs值:

if (lhs.CompareTo(rhs) <= 0)
    ...

使用枚举,如下所示:

if (lhs.CompareTo(rhs) == CompareResult.LessThan ||
    lhs.CompareTo(rhs) == CompareResult.Equals)
    ...

这显然可读性较差,而且效率也很低,因为它要进行两次比较。您可以通过使用临时结果来修复低效:

var compareResult = lhs.CompareTo(rhs);

if (compareResult == CompareResult.LessThan || compareResult == CompareResult.Equals)
    ...

它的可读性仍然很差,而且它的效率也更低,因为它执行两个比较操作而不是一个(尽管我坦率地承认,这样的性能差异很可能不会有什么问题)。

正如raznagul在下面指出的,你实际上可以通过一个比较来做到这一点:

if (lhs.CompareTo(rhs) != CompareResult.GreaterThan)
    ...

所以您可以使其相当高效--但当然,可读性仍然会受到影响。... != GreaterThan不如... <=清楚

(当然,如果使用枚举,则无法避免将比较结果转换为枚举值的开销。)

因此,这样做主要是出于可读性的原因,但在某种程度上也是出于效率的原因。

最后,正如其他人所提到的,这也是出于历史原因。像C的strcmp()memcmp()这样的函数总是返回整数。

汇编比较指令也倾向于以类似的方式使用。

例如,要在x86汇编程序中比较两个整数,可以这样做:

CMP AX, BX ; 
JLE lessThanOrEqual ; jump to lessThanOrEqual if AX <= BX

CMP AX, BX
JG greaterThan ; jump to greaterThan if AX > BX

CMP AX, BX
JE equal      ; jump to equal if AX == BX

您可以看到与CompareTo()的返回值的明显比较。

附录:

这里有一个例子,它表明使用从LHS中减去RHS的技巧来获得比较结果并不总是安全的:

int lhs = int.MaxValue - 10;
int rhs = int.MinValue + 10;

// Since lhs > rhs, we expect (lhs-rhs) to be +ve, but:

Console.WriteLine(lhs - rhs); // Prints -21: WRONG!

显然,这是因为算术溢出。如果您为生成打开了checked,则上面的代码实际上会引发异常。

因此,最好避免使用减法进行比较的优化。(参见下面Eric Lippert的评论。)

相关文章