为什么 Kotlin 在 sumOf 函数的 lambda 中默认不将数字视为 "Int" 类型?

3
在下面的代码中:
val sum = listOf(1, 2, 3).sumOf { if (it % 2 == 0) 1 else 0 }

Kotlin出现以下错误:
Kotlin: Overload resolution ambiguity: 
public inline fun <T> Iterable<TypeVariable(T)>.sumOf(selector: (TypeVariable(T)) -> Int): Int defined in kotlin.collections
public inline fun <T> Iterable<TypeVariable(T)>.sumOf(selector: (TypeVariable(T)) -> Long): Long defined in kotlin.collections

如果我显式地使用 toInt(),错误就会消失,但我会收到一个冗余调用的警告。

Playground

val sum = listOf(1, 2, 3).sumOf { if (it % 2 == 0) 1.toInt() else 0 }

为什么 Kotlin 没有自动使用 Int 类型?

你会接受“因为规范这样说”作为答案吗? - Sweeper
是的,但有没有一种解决方案,我不会收到那个冗余调用警告? - Arpit Shukla
这个代码可以正常运行并返回相同的结果:val sum = listOf(1, 2, 3).sumOf { 1 - it % 2 }。然而,(1)我不太确定为什么它可以工作而原始代码不能;(2)不幸的是,这不是你问题的通用解决方案。 - Klitos Kyriacou
@Sweeper,我希望您能简要解释一下规范的哪个部分适用。我期望表达式 if (it % 2 == 0) 1 else 0 能明确地解析为一个整数。 - Tenfour04
@KlitosKyriacou 感谢您的回答,但我在问题中提供的示例只是我的原始问题的简化版本。我拥有的列表有点更复杂,因此 1 - it % 2 在这里无法帮助。我拥有的是像 list.sumOf { if(someCondition) x else y } 这样的东西,其中 x 和 y 是通常的整数。 - Arpit Shukla
1个回答

7
该规范对整数字面量类型的类型有以下说明:

没有标记的字面量具有特殊的整数字面量类型,该类型取决于字面量的值:

  • 如果值大于最大的kotlin.Long值,则为非法整数字面量,应该是一个编译时错误;
  • 否则,如果值大于最大的kotlin.Int值,则其类型为kotlin.Long
  • 否则,它具有包含所有内置整数类型的整数字面量类型,保证能够表示此值。
因此,像“1”这样的整数字面量没有像kotlin.Intkotlin.Long这样的简单类型。它具有“整数字面量类型”。

例如:整数文字0x01的值为1,因此具有类型ILT(kotlin.Byte,kotlin.Short,kotlin.Int,kotlin.Long)。整数文字70000的值为70000,不能使用类型kotlin.Bytekotlin.Short表示,因此具有类型ILT(kotlin.Int,kotlin.Long)

这些ILT的子类型规则在这里。对于您的问题非常重要:

Ti∈{T1,…,TK}:ILT(T1,…,TK)<:Ti

这个规则基本上是说 ILT(交集类型)的工作方式。例如,ILT(kotlin.Int,kotlin.Long)kotlin.Intkotlin.Long 的子类型。

现在让我们来看看你的 lambda 表达式 { if (it % 2 == 0) 1 else 0 }。它返回字面量 0 或字面量 1。它们都具有以下类型:

ILT(kotlin.Byte,kotlin.Short,kotlin.Int,kotlin.Long)

这是kotlin.Longkotlin.Int的子类型。因此,您的lambda表达式可以转换为(T) -> Long(T) -> Int,就像(T) -> Dog可以转换为(T) -> Animal一样。

当您使用toInt()时,只有(T) -> Int重载与返回类型匹配,因为Int不能隐式转换为Long

显然,如果您在整个表达式上执行toInt(),则没有多余的toInt警告:

fun main() {
    val sum = listOf(1, 2, 3).sumOf { (if (it % 2 == 0) 1 else 0).toInt() }
}

请注意,编译器仅查看lambda返回类型,因为sumOf带有OverloadResolutionByLambdaReturnType注释。如果没有这个注释,即使使用toInt(),仍将收到模棱两可的错误。有关更多信息,请参见使用lambda返回类型来细化函数适用性
在简单情况下选择Int重载的原因是:
fun foo(x: Int) {}
fun foo(x: Long) {}
fun main() { foo(43) }

这是由于重载决议中的 "选择最特定的候选项" 步骤导致的。在此步骤中,它以不同的方式处理内置数字类型,并将 Int 视为 "最具体"。然而,这一步发生在 "使用 lambda 返回类型来细化函数适用性" 之前,并认为 (T) -> Int(T) -> Long 同样具体。

很棒的回答!请注意,OverloadResolutionByLambdaReturnType被标记为实验性的;因此,虽然它还没有确定,但是否值得提出问题?另外,请注意,虽然IntelliJ在1.toInt()上发出了冗余警告,但在(1).toInt()上却没有。但是,将toInt()放在整个表达式上当然更好。 - Klitos Kyriacou
1
@KlitosKyriacou 显然已经有人做过了 :-) - Sweeper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接