代码之家  ›  专栏  ›  技术社区  ›  Sweeper

为什么从reduce的返回值解构元组会导致错误?

  •  7
  • Sweeper  · 技术社区  · 6 年前

    假设我有一个整数数组,我想得到所有偶数和所有奇数的和。例如,对于数组 [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)”的值没有成员“odd”

    return 声明。

    为什么解构元组会导致以不同方式推断泛型类型?解构主义部分应该只说明如何处理结果。方法调用应该自己解释,然后与模式匹配 (oddSum, evenSum) .

    要解决这个问题,我必须将第一个参数更改为 (0, 0) 这使得闭包中的内容非常不可读。我不得不把零头称为 result.0 甚至总和 result.1 .

    1 回复  |  直到 6 年前
        1
  •  9
  •   Hamish    6 年前

    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美元) .

    (这掩盖了一个事实,即通用占位符被打开为新的类型变量,但这在这里是不必要的细节)

    没有任何规则支持一个选项胜过另一个,这是含糊不清的。然而幸运的是,约束系统 has a rule to prefer the un-labelled version of a tuple type 绑定类型时。因此约束系统决定约束 T (T0美元,T1美元) ,此时两者 $0 T1美元 可以绑定到 int 因为事实上 (x:int,y:int) 需要转换为 (T0美元,T1美元) .

    让我们看看移除元组解构模式时会发生什么:

    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)
    

    T 现在必须 (x:int,y:int) .为什么?因为模式类型现在只是类型 $0 .

    • 如果 T 绑定到 $0 然后 $0 将绑定到参数类型 (x:int,y:int) .
    • 如果 T 绑定到 (x:int,y:int) ,然后 $0 也会受到约束 (x:int,y:int) .

    在这两种情况下,解决方案是相同的,因此没有歧义。没有可能 T 绑定到一个未标记的元组类型仅仅是因为没有未标记的元组类型首先被引入到系统中。

    那么,这如何适用于您的示例呢?好, 魔术 只是 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 :

    • 束缚 结果 到未标记的元组类型 (T0美元,T1美元) ,强制类型的参数 (odd: Int, even: Int) 执行一个元组转换,将其从标签中剥离出来。
    • 束缚 结果 到标记的元组类型 (奇数:int,偶数:int) ,强制返回的值执行元组转换,该转换将其标签剥离,以便将其转换为 (T0美元,T1美元) .

    并且由于规则支持未标记的元组形式,约束求解器选择绑定 结果 (T0美元,T1美元) ,决定 (int,int) .移除元组分解可以工作,因为您不再引入类型 (T0美元,T1美元) 进入约束系统,意思是 结果 只能绑定到 (奇数:int,偶数:int) .

    好吧,但我们再考虑一下结束。很明显我们正在联系会员 .odd .even 关于元组,那么为什么约束系统不能找出 结果 (int,int) 不可行吗?嗯,这是因为 multiple statement closures don't participate in type inference . 这意味着闭包体独立于调用 减少 因此,当约束系统意识到 (int,int) 是无效的,太晚了。

    如果您将闭包减少到一个语句,则解除此限制,并且约束系统可以正确地对其进行折扣。 (int,int) 作为有效的绑定 结果 :

    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)
    })
    

    或者,如果您将模式更改为使用相应的元组标签, as pointed out by 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)
      }
    })
    

    另一种选择,如 pointed out by 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)
      }
    })
    

    但请注意,与前两个示例不同,这会导致 结果 势必 (int,int) 由于模式引入了首选类型 (T0美元,T1美元) . 允许这个例子编译的是编译器为传递的闭包插入了一个元组转换,它为其参数重新添加了元组标签。