为什么compareTo返回一个整数



我最近在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)
    ...

IMO的可读性仍然差得多,而且效率仍然低得多,因为它正在进行两次比较操作,而不是一次(尽管我坦率地承认,这样的性能差异可能无关紧要)。

正如raznagul在下面指出的,你实际上只需要一个比较就可以做到:

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

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

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

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

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

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

例如,要在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的评论。)

让我们坚持简单的事实,绝对不需要手工和/或不必要的/不相关的/依赖于实现的细节

正如您自己已经发现的,compareTo和Java一样古老(来自Integer JavaDoc的Since: JDK1.0);Java 1.0的设计是为了让C/C++开发人员熟悉,并模仿了它的许多设计选择,无论好坏。此外,Java有一个向后兼容性策略,因此,一旦在core-lib中实现,该方法几乎将永远留在其中。

至于C/C++-strcmp/memcmp,它的存在时间与string.h一样长,所以本质上与C标准库一样长,返回完全相同的值(或者更确切地说,compareTo返回与strcmp/memcmp相同的值)-例如参见C ref-strcmp。在Java诞生的时候,这样做是合乎逻辑的。当时Java中没有任何枚举,也没有泛型等。(所有这些都是>=1.5)

strcmp返回值的决定是非常明显的-首先,你可以在比较中得到3个基本结果,所以选择+1表示"更大",选择-1表示"更小",选择0表示"相等"是合乎逻辑的。此外,正如所指出的,你可以通过减法很容易地得到值,并且返回int允许在进一步的计算中容易地使用它(以传统的C型不安全的方式),同时也允许高效的单操作实现。

如果您需要/想要使用基于enum的类型安全比较接口-您可以自由使用,但由于strcmp返回+1/0/-1的约定与当代编程一样古老,它实际上确实传达了语义,以相同的方式CCD_ 27可以被解释为CCD_。也许这不是最好的编码实践,但它确实有其优点,并且仍然常用,例如在C.中

另一方面,询问"为什么XYZ语言的标准库确实符合ABC语言的遗留标准"本身是没有意义的,因为它只能由实现它的设计语言准确回答。

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

顺便说一句,我认为这个问题(以目前的形式)太宽泛了,无法准确回答,高度基于意见,并且由于直接询问设计模式&语言架构

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

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

值为{lt,eq,gt,compatible}的枚举会更正确。

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

请参阅java的comparator文档。

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

事实比较通常作为子动作返回。

作为的一个例子

public class MyComparable implements Comparable<MyComparable> {
    public int num;
    public int compareTo(MyComparable x) {
        return num - x.num;
    }
}

最新更新