为什么 compareTo return 是一个整数

Why does compareTo return an integer

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

这是出于历史原因还是为了与其他语言保持一致?查看各种语言compareTo的签名时,return是一个int

为什么它不 return 枚举。例如在 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# 了)但是它可以通过额外的 class 来解决,例如:

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 尊重比较器接口的约定(采用两个整数和 return 一个整数)。

显然这两种情况的结果都不是预期的。如果 return 类型是 Compare 它会导致编译错误从而迫使你实现 "correct" 行为(或者至少你知道你在做什么)。

这种 return 类型丢失了很多语义(并且可能会导致一些难以发现的错误),那么为什么要这样设计呢?

回复这是由于性能原因。 如果您经常需要比较 int,您可以 return 以下内容:

事实上的比较通常return作为减法。

举个例子

public class MyComparable implements Comparable<MyComparable> {
    public int num;

    public int compareTo(MyComparable x) {
        return num - x.num;
    }
}

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

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

  1. 进行比较的地方。通常你可以 return "(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)
    ...

IMO 的可读性仍然很差 - 而且它的效率仍然较低,因为它执行两个比较操作而不是一个(尽管我坦率地承认这种性能差异很可能很少重要)。

正如 raznagul 在下面指出的那样,您实际上只需进行一次比较就可以做到:

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

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

清楚

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

所以这样做主要是为了可读性,但在某种程度上也是为了效率。

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

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

例如,要在 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() 中看到与 return 值的明显比较。

附录:

这是一个示例,它表明使用从 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 的评论。)

让我们坚持赤裸裸的事实,绝对最少的挥手 and/or unnecessary/irrelevant/implementation 相关细节。

您自己已经知道,compareTo 和 Java 一样大(Since: JDK1.0 来自 Integer JavaDoc); Java 1.0 旨在让 C/C++ 开发人员熟悉,并模仿了它的许多设计选择,无论好坏。此外,Java 具有 向后兼容性 政策 - 因此,一旦在核心库中实施,该方法 almost 必然会保留在其中永远。

至于 C/C++ - strcmp/memcmp,它的存在时间与 string.h 一样长,因此基本上与 C 标准库一样长,return 完全相同的值(或者更确切地说,compareTo returns 与 strcmp/memcmp 相同的值) - 参见例如C ref - strcmp。在 Java 成立之初,这样做是合乎逻辑的。当时 Java 中没有任何枚举,没有泛型等(所有这些都出现在 >= 1.5)

strcmp 的 return 值的决定非常明显 - 首先,您可以比较得到 3 个基本结果,因此为 "bigger" 选择 +1,- 1 代表 "smaller" 和 0 代表 "equal" 是合乎逻辑的事情。此外,正如所指出的,您可以通过减法轻松获得该值,并且 returning int 允许在进一步的计算中轻松使用它(以传统的 C 类型不安全方式),同时还允许高效单操作实现。

如果您 need/want 使用基于 enum 的类型安全比较接口 - 您可以自由地这样做,但是由于 strcmp returning +1/0/-1 与当代编程一样古老,它实际上 确实 传达语义,以相同的方式 null 可以被解释为 unknown/invalid value 或越界 int 值(例如,为纯正质量提供的负数)可以被解释为错误代码。也许这不是最好的编码实践,但它肯定有其优点,并且仍然常用,例如在 C.

另一方面,询问 "why the standard library of language XYZ does conform to legacy standards of language ABC" 本身没有实际意义,因为它只能由实现它的设计语言准确回答。

TL;DR 之所以这样,主要是因为遗留原因在遗留版本中是这样做的,POLA 是为了 C 程序员,并且保持这种方式用于再次向后兼容和 POLA。

作为旁注,我认为这个问题(以目前的形式)过于宽泛,无法准确回答,高度基于意见,并且由于直接询问 设计模式 & 语言架构.

这种做法来自于以这种方式比较整数,并在字符串的第一个不匹配字符之间使用减法。

请注意,这种做法对于部分可比较的事物是危险的,而使用 -1 表示一对事物是不可比较的。这是因为它可能会造成 a < b 和 b < a 的情况(应用程序可能会使用它来定义 "incomparable")。这种情况会导致无法正确终止的循环。

具有值 {lt,eq,gt,incomparable} 的枚举会更正确。

我的理解是这样做是因为你可以对结果进行排序(即操作是自反和传递的)。例如,如果您有三个对象(A、B、C),您可以比较 A->B 和 B->C,并使用结果值对它们进行正确排序。有一个隐含的假设,如果 A.compareTo(B) == A.compareTo(C) 那么 B==C.

请参阅 java 的 comparator 文档。