为什么比较返回整数

2022-09-01 21:08:22

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

这是由于历史原因还是与其他语言的一致性?查看各种语言的签名时,它会返回 .compareToint

为什么它不返回枚举。例如,在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);
}

我之所以问这个问题,是因为我不认为 an 很好地代表了数据的语义。int

例如,在 C# 中,

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

以及它在Java 8中的孪生兄弟,

l.sort(Integer::min);

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

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

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


答案 1

[这个答案是针对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()

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

例如,要在 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

因此,最好避免优化使用求减法来实现比较。(请参阅下面埃里克·利珀特的评论。


答案 2

让我们坚持赤裸裸的事实,绝对最少的挥手和/或不必要的/不相关/依赖于实现的细节。

正如你自己已经弄清楚的那样,它与Java(来自Integed JavaDoc)一样古老;Java 1.0被设计为C/C++开发人员所熟悉的,并模仿了很多它的设计选择,无论好坏。此外,Java有一个向后兼容性策略 - 因此,一旦在core lib中实现,该方法几乎注定要永远留在其中。compareToSince: JDK1.0

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

返回值的决定非常明显 - 首先,您可以比较3个基本结果,因此选择+1表示“较大”,选择-1表示“较小”,选择0表示“相等”是合乎逻辑的事情。此外,如前所述,您可以通过减法轻松获得该值,并且返回允许在进一步的计算中轻松使用它(以传统的C类型不安全方式),同时还允许有效的单操作实现。strcmpint

如果你需要/想要使用你基于typesafe的比较接口 - 你可以自由地这样做,但是由于返回//的约定与当代编程一样古老,它实际上确实传达了语义意义,以同样的方式可以解释为或超出界限的int值(例如,为正质量提供的负数)可以被解释为错误代码。也许它不是最好的编码实践,但它肯定有其优点,并且仍然常用,例如在C中。enumstrcmp+10-1nullunknown/invalid value

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

TL;DR 之所以如此,主要是因为出于遗留原因,它在遗留版本中以这种方式完成,而对于 C 程序员来说,以这种方式完成,并且为了向后兼容性和 POLA 而保持这种方式。

顺便说一句,我认为这个问题(目前的形式)太宽泛了,无法精确地回答,高度基于意见,并且由于直接询问设计模式语言架构,因此在SO上偏离了主题。


推荐