【问题标题】:Nested function crashing the Swift compiler嵌套函数使 Swift 编译器崩溃
【发布时间】:2015-02-23 17:29:20
【问题描述】:

我写了一个递归的mergeSort函数:

func mergeSort<T: Comparable>(inout array: [T]) {
    if array.count <= 1 {
        return
    }

    var leftSlice = [T](array[0..<array.count / 2])
    var rightSlice = [T](array[array.count / 2...array.endIndex - 1])

    mergeSort(&leftSlice)
    mergeSort(&rightSlice)
    array = merge(leftSlice, rightSlice)
}

func merge<T: Comparable>(var left: [T], var right: [T]) -> [T] {
    var mergedValues = [T]()

    while !left.isEmpty && !right.isEmpty {
        mergedValues.append(left.first! < right.first! ? left.removeAtIndex(0) : right.removeAtIndex(0))
    }

    if !left.isEmpty {
        mergedValues += left
    } else if !right.isEmpty {
        mergedValues += right
    }

    return mergedValues
}

现在,由于merge() 应该只被mergeSort() 使用,我将它放在mergeSort() 内,因此将merge() 设为nested function

func mergeSort<T: Comparable>(inout array: [T]) {
    func merge<T: Comparable>(var left: [T], var right: [T]) -> [T] {
        var mergedValues = [T]()

        while !left.isEmpty && !right.isEmpty {
            mergedValues.append(left.first! < right.first! ? left.removeAtIndex(0) : right.removeAtIndex(0))
        }

        if !left.isEmpty {
            mergedValues += left
        } else if !right.isEmpty {
            mergedValues += right
        }

        return mergedValues
    }

    if array.count <= 1 {
        return
    }

    var leftSlice = [T](array[0..<array.count / 2])
    var rightSlice = [T](array[array.count / 2...array.endIndex - 1])

    mergeSort(&leftSlice)
    mergeSort(&rightSlice)
    array = merge(leftSlice, rightSlice)
}

现在first 版本可以正常工作,但second 版本不行。
怎么可能?

【问题讨论】:

    标签: swift generics nested mergesort nested-generics


    【解决方案1】:

    您似乎在编译器中发现了与嵌套泛型函数相关的错误。这是一个也会导致 1.2 编译器崩溃的缩减:

    func f<T>(t: T) {
        func g<U>(u: U) { }
    }
    

    但在这种情况下,您实际上不需要merge 的通用版本。它的泛型参数与外部函数相同,因此只需使用它:

    func mergeSort<T: Comparable>(inout array: [T]) {
        // no generic placeholder needed, T is the T for mergeSort
        func merge(var left: [T], var right: [T]) -> [T] {
          // etc.
        }
    }
    

    这似乎工作正常。

    但是,还值得指出的是,在您的 merge 函数中,您在循环中调用 removeAtIndex,这是一个 O(n) 函数。这意味着您的合并排序不会具有预期的复杂性。

    这里有一个可供考虑的替代版本:

    func mergeSort<T: Comparable>(inout array: [T], range: Range<Int>? = nil) {
    
        func merge(left: Range<Int>, right: Range<Int>) -> [T] {    
            var tmp: [T] = []
            tmp.reserveCapacity(count(left) + count(right))
    
            var l = left.startIndex, r = right.startIndex
    
            while l != left.endIndex && r != right.endIndex {
                if array[l] < array[r] {
                    tmp.append(array[l++])
                }
                else {
                    tmp.append(array[r++])
                }
            }
            // where left or right may be empty, this is a no-op
            tmp += source[l..<left.endIndex]
            tmp += source[r..<right.endIndex]
    
            return tmp
        }
    
        // this allows the original caller to omit the range,
        // the default being the full array
        let r = range ?? indices(array)
        if count(r) > 1 {
            let mid = r.startIndex.advancedBy(r.startIndex.distanceTo(r.endIndex)/2)
            let left = r.startIndex..<mid
            let right = mid..<r.endIndex
    
            mergeSort(&array, range: left)
            mergeSort(&array, range: right)
            let merged = merge(left, right)
            array.replaceRange(r, with: merged)
        }
    }
    

    我还要说,由于merge 本身可能是一个通用的有用函数,您不妨将其设为独立而不是嵌套它(类似地,partition 在实现快速排序时)。嵌套不会给您带来任何好处(除了我上面使用的从其中引用外部参数的技巧之外,无论如何这可能是一种不好的做法,我这样做主要是为了向您展示:)

    【讨论】:

    • 我也注意到了这一点。如果我引入嵌套泛型,Playground 甚至都不会运行,并且尝试使用嵌套泛型进行编译会产生分段错误。
    • 是的,这是编译器崩溃而不是运行时错误(我已经编辑了问题标题)
    • 好的,我有几个问题:(1) 你怎么知道removeAtIndex 需要O(n) 时间? || (2) 是否可以通过在let 定义的数组上以某种方式使用reserveCapacity 来创建静态数组而不初始化它们? || (3) 为什么你对 Swift 了解这么多?
    • (1) 简单的答案:它在文档中 :) 将鼠标悬停在 removeAtIndex 上,您会看到信息。而且,在 Swift 中声明的数组是一个连续的内存块,这很直观——所有后续元素都需要重新排列以填补空白。事实上,删除第一个元素是最坏的情况,因为这意味着最大的洗牌。 (2) 没有,但想想为什么这没有意义——let 声明了一个不可变的值,而数组是值类型,所以如果用let 声明它们就永远无法添加——那么为什么要保留额外的空间? (3) 实验!
    【解决方案2】:

    您无需将merge 设为通用函数。通用 T 已为 mergeSort 定义,因此您只需将 [T] 设置为内部函数中的参数:

    func merge(var left: [T], var right: [T]) -> [T] {
        var mergedValues = [T]()
        ...
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-01-01
      • 2020-12-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多