假设我有一个整数数组,我想得到所有偶数的和和和所有奇数的和。例如,对于阵列[1,2,3]
,所有奇数的和为4,所有偶数的和为2。
我就是这样做的:
array.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})
这本身就很好,但当我试图解构返回的元组时:
let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})
它给了我一个错误:
元组类型"(Int,Int)"的值没有成员"奇数">
在return
语句上。
为什么解构元组会导致对泛型类型的推断不同?解构主义部分应该只是说对结果做什么。方法调用应该自己进行解释,然后根据模式(oddSum, evenSum)
进行匹配。
为了解决这个问题,我必须将第一个参数更改为(0, 0)
,这使得闭包中的内容非常不可读。我不得不把奇数和称为result.0
,把偶数和称为result.1
。
TL;DR
这种行为是不幸的,但由于以下因素的组合,"如预期那样工作":
- 在类型变量绑定方面,约束系统倾向于未标记的元组类型而不是标记的元组
- 不参与类型推理的多语句闭包
为什么解构元组会导致以不同的方式推断泛型类型?解构主义部分应该只是说对结果做什么。方法调用应该自己进行解释,然后根据模式
(evenSum, oddSum)
进行匹配。
类型检查器进行双向类型推断,这意味着使用的模式可以影响指定表达式的类型检查方式。例如,考虑:
func magic<T>() -> T {
fatalError()
}
let x: Int = magic() // T == Int
图案的类型用于推断T
是Int
。
那么元组解构模式会发生什么呢?
let (x, y) = magic() // error: Generic parameter 'T' could not be inferred
类型检查器创建两个类型变量来表示元组模式的每个元素。此类类型变量在约束解算器内部使用,并且在考虑求解约束系统之前,每个类型变量都必须绑定到Swift类型。在约束系统内,模式let (x, y)
具有类型($T0, $T1)
,其中$T{N}
是类型变量。
函数返回通用占位符T
,因此约束系统推断T
可转换为($T0, $T1)
。然而,没有关于$T0
和$T1
可以绑定到什么的进一步信息,因此系统失败。
好吧,让我们给系统一种方法,通过向函数添加一个参数,将类型绑定到那些类型变量。
func magic<T>(_ x: T) -> T {
print(T.self)
fatalError()
}
let labelledTuple: (x: Int, y: Int) = (x: 0, y: 0)
let (x, y) = magic(labelledTuple) // T == (Int, Int)
现在进行编译,我们可以看到通用占位符T
被推断为(Int, Int)
。这是怎么发生的?
magic
属于(T) -> T
型- 参数的类型为
(x: Int, y: Int)
- 结果模式的类型为
($T0, $T1)
这里我们可以看到约束系统有两个选项,它可以是:
- 将
T
绑定到未标记的元组类型($T0, $T1)
,强制类型(x: Int, y: Int)
的参数执行元组转换以剥离其标签 - 将
T
绑定到带标签的元组类型(x: Int, y: Int)
,强制返回的值执行元组转换,去掉其标签,以便将其转换为($T0, $T1)
(这掩盖了通用占位符被打开到新类型变量中的事实,但这是不必要的细节)
如果没有任何规则来支持一种选择而不是另一种选择,这是模棱两可的。然而,幸运的是,约束系统有一条规则,在绑定类型时更喜欢元组类型的未标记版本。因此,约束系统决定将T
绑定到($T0, $T1)
,在这一点上,由于(x: Int, y: Int)
需要转换为($T0, $T1)
,$T0
和$T1
都可以绑定到Int
。
让我们看看当我们移除元组解构模式时会发生什么:
func magic<T>(_ x: T) -> T {
print(T.self)
fatalError()
}
let labelledTuple: (x: Int, y: Int) = (x: 0, y: 0)
let tuple = magic(labelledTuple) // T == (x: Int, y: Int)
CCD_ 37现在绑定到CCD_ 38。为什么?因为模式类型现在只是类型$T0
。
- 如果
T
绑定到$T0
,则$T0
将绑定到参数类型(x: Int, y: Int)
- 如果
T
绑定到(x: Int, y: Int)
,那么$T0
也将绑定到(x: Int, y: Int)
在这两种情况下,解决方案是相同的,因此没有歧义。T
不可能绑定到未标记的元组类型,这仅仅是因为系统中首先没有引入未标记的tuple类型。
那么,这是如何应用于您的示例的呢?好吧,magic
只是reduce
,没有额外的闭包参数:
public func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult: (_ partialResult: Result, Element) throws -> Result
) rethrows -> Result
当你这样做:
let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})
如果我们现在忽略闭包,我们可以为Result
:选择相同的绑定
- 将
Result
绑定到未标记的元组类型($T0, $T1)
,强制类型(odd: Int, even: Int)
的参数执行元组转换,以剥离其标签 - 将
Result
绑定到带标签的元组类型(odd: Int, even: Int)
,强制返回的值执行元组转换,去掉其标签,以便将其转换为($T0, $T1)
由于有利于元组的未标记形式的规则,约束求解器选择将Result
绑定到($T0, $T1)
,并将其解析为(Int, Int)
。删除元组分解是有效的,因为您不再将类型($T0, $T1)
引入约束系统中——这意味着Result
只能绑定到(odd: Int, even: Int)
。
好吧,但让我们再次考虑结束。我们显然正在访问元组上的成员.odd
和.even
,那么为什么约束系统不能确定Result
到(Int, Int)
的绑定是不可行的呢?这是因为多个语句闭包不参与类型推断。这意味着闭包主体是独立于对reduce
的调用求解的,所以当约束系统意识到绑定(Int, Int)
无效时,已经太晚了。
如果您将闭包缩减为单个语句,则此限制将被取消,并且约束系统可以正确地将(Int, Int)
作为Result
:的有效绑定进行贴现
let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result, int) in
return int % 2 == 0 ? (result.odd, result.even + int)
: (result.odd + int, result.even)
})
或者,如Martin所指出的,如果您更改模式以使用相应的元组标签,则模式的类型现在为(odd: $T0, even: $T1)
,这避免了在约束系统中引入未标记的形式:
let (odd: oddSum, even: evenSum) = a.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})
正如Alladinian所指出的,另一种选择是显式注释闭包参数类型:
let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result: (odd: Int, even: Int), int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})
然而,注意,与前两个示例不同,由于引入优选类型($T0, $T1)
的模式,这导致Result
绑定到(Int, Int)
。允许此示例进行编译的是,编译器为传递的闭包插入元组转换,从而为其参数重新添加元组标签。