延迟收集会使用“不稳定”的过滤器和排序导致崩溃

时间:2018-08-19 22:33:53

标签: swift

我刚刚看到一个线程询问了懒惰的集合。他们看起来像这样:

src/main

这是关于惰性集合的说法:“一个集合包含与基本集合相同的元素,但是在其中懒惰地实现了诸如map和filter之类的某些操作。”

该想法是避免大量中间集合。如果您有这样的代码:

让数组= 1 ... 10     .filter {$ 0%2 == 0}     .sorted {$ 0 <$ 1}     .map {String($ 0)}

然后代码中的每个步骤都会创建一个中间数组。对于大型阵列,这可能会占用大量内存。

但是,在尝试惰性时,我发现如果处理惰性集合的一个或多个步骤在每次执行时返回不同的结果,则可能导致问题/崩溃。考虑以下代码:

let array = Array(1...10).lazy

该代码将值从1 ... 100映射为随机值,过滤出<50的值,对其余值进行排序,将其转换为字符串,然后打印出来。

这可能会产生如下输出:

@discardableResult func timeTest() -> Double {
    let start = Date()
    let array = 1...10
    let random = array
        .lazy
        .map { (value) -> UInt32 in
            let random = arc4random_uniform(100)
            print("Mapping", value, "to random val \(random)")
            return random
        }
        .filter {
            let result = $0 < 50
            print("  Testing \($0) < 50", result)
            return result
        }
        .map { (val: UInt32) -> NSNumber in
            print("    Mapping", val, "to NSNumber")
            return NSNumber(value: val)
        }
        .compactMap { (number) -> String? in
            print("      Mapping", number, "to String")
            return formatter.string(from: number)
        }
        .sorted { (lhv, rhv) -> Bool in //--> This is the line that crashes
            print("        Sorting strings")
            return (lhv.compare(rhv, options: .numeric) == .orderedAscending)
    }

    random.enumerated().forEach { print("String[\($0.0)] = \($0.1)") }

    let elapsed = Date().timeIntervalSince(start)

    print("Completed in ", String(format: "%0.3f", elapsed), " seconds")
    return elapsed
}

其次是索引超出范围崩溃。

请注意,当值通过过滤器中的测试时,原始映射到随机数的映射将再次被调用。另外请注意,有时重复映射到随机值的顺序不正确。

我猜想每个高阶函数步骤都会重新评估上一步,并且由于随机数步骤每次都会返回不同的值,然后过滤器步骤每次都会返回不同数量的结果,因此会感到困惑。

阅读本文的人对发生的事情有特定的了解吗?

1 个答案:

答案 0 :(得分:1)

此错误的原因是,当尝试执行sorted(by:)时,数组的计数不同。 filter更改结果数组的计数,并干扰sorted(by:)的工作方式。

sorted(by:)内部调用sort(by:)

@inlinable
public func sorted(
  by areInIncreasingOrder:
    (Element, Element) throws -> Bool
) rethrows -> [Element] {
  var result = ContiguousArray(self) 
  try result.sort(by: areInIncreasingOrder)
  return Array(result)
}

使用连续数组可使排序更快。

跳到sort(by:)的{​​{3}}:

@inlinable
public mutating func sort(
  by areInIncreasingOrder: (Element, Element) throws -> Bool
) rethrows {
  let didSortUnsafeBuffer = try _withUnsafeMutableBufferPointerIfSupported {
  buffer -> Void? in
    try buffer.sort(by: areInIncreasingOrder)
  }
  if didSortUnsafeBuffer == nil {
    try _introSort(within: startIndex..<endIndex, by: areInIncreasingOrder)
  }
}

我们可以看到它调用了内部函数_introSort(within:,by:)及其实际实现_introSortImpl(within:,by:,depthLimit:)

Swift与.Net和C ++一样,使用 Introsort算法对数组的元素进行排序。其中是Quicksort和heapSort的混合体:它通过选择枢轴(median-of-3)对数组进行分区。最后,当深度达到0时,将切换到heapSort。 sort使用的 Introsort 算法的详细信息在此definition中进行了描述。

就地排序连续数组调用paper函数:

public mutating func replaceSubrange<C>(
  _ subrange: Range<Int>,
  with newElements: C
)

内部调用this

internal mutating func _arrayOutOfPlaceReplace<C: Collection>(
  _ bounds: Range<Int>,
  with newValues: C,
  count insertCount: Int
)

哪个进行了this的最终检查:

internal func _expectEnd<C: Collection>(of s: C, is i: C.Index)

如果数组的大小在排序时发生变化,则此消息将引发错误

"invalid Collection: count differed in successive traversals"

与映射或过滤不同,对数组进行排序需要在排序时拥有一个具有恒定大小和恒定值的数组。