【问题标题】:Swift 2.2: Non +1 increasing iterator loopsSwift 2.2:非 +1 递增迭代器循环
【发布时间】:2016-07-13 01:27:45
【问题描述】:

我想知道关于 Swift 2.2 中非 +1 递增迭代器循环的最佳解决方案,因为旧样式现已弃用:

for (var i = 0; i < planets.count; i += 1) {
    let string = planets[i]
    if (string == "Venus" || string == "Saturn") {
        i += 2
    }
}

我的第一个解决方案是使用while,它有效

var i = 0
while i < planets.count {
    let string = planets[i]
    if (string == "Venus" || string == "Saturn") {
        i += 2
    }
    i += 1
}

但在整个代码中多次使用它并不是很方便。所以我创建了一个似乎也可以工作的 Array 扩展:

extension Array {
    func loopIncrement(callback: (inout i: Int) -> ()) {
        var index: Int = 0
        while index < self.count {
            callback(i: &index)
            index += 1
        }
    }
    func loopDecrement(callback: (inout i: Int) -> ()) {
        var index: Int = self.count - 1
        while index >= 0 {
            callback(i: &index)
            index -= 1
        }
    }
}

像这样使用它:

planets.loopIncrement { (i) in
    let string = planets[i]
    if (string == "Venus" || string == "Saturn") {
        i += 2
    }
}

planets.loopDecrement { (i) in
    let string = planets[i]
    if (string == "Venus" || string == "Saturn") {
        i -= 2
    }
}

目前这很好,但我相信有更快捷的方法来解决这个问题。

所以让我们在这里开始讨论。非常感谢任何帮助!

如果你想看看,这里是 Playground 文件:NonPlusOneIncrementLoops.playground

示例:

在以下示例中,您可以看到需要实时更改循环索引的地方。 有一个 printObject 数组,其中包含 PrintObject 类,这些类定义了应将其 printText 打印到调试区域的次数。 printCount 属性是随机生成的,因此在循环之前是未知的。

class PrintObject {
    var printCount: Int = 0
    var printText: String = "<Missing>"
}

var printObjects = [PrintObject]()

for i in 0...6 {
    let randomCount = Int(arc4random_uniform(2)) + 1
    let printObject = PrintObject()
    printObject.printCount = randomCount
    printObject.printText = "This is line \(i)"
    printObjects.append(printObject)
}

printObjects.loopIncrement { (i) in
    let printObject = printObjects[i]
    printObject.printCount -= 1
    if printObject.printCount > 0 {
        i -= 1
    }
    print(printObject.printText)
}

这显然是一个微不足道的例子,但它可能会让一些人更清楚在哪些情况下更改循环索引是有意义的。

【问题讨论】:

  • 除了根据条件不同地增加循环索引之外,您还想达到什么目的?
  • 就是这样。讨论仅基于此。
  • 在 for 循环中更改迭代器是 any 语言中的错误代码。您能否添加完整的用例?您如何使用行星(为什么要跳过下一个行星?)? for-loop 没有一个很好的替代品,有很多,而且每一个都用于不同的用例。
  • 您能详细说明为什么在循环内更改迭代器是个坏主意吗?
  • 关于 printObjects 示例 - 在内部添加另一个嵌套循环 (for...i)。对我来说,这似乎是更清洁的方法。

标签: arrays swift for-loop iterator


【解决方案1】:

如果你只想要每个第二个元素

evens = Array(1...10).filter { $0 % 2 == 0 }

非常冗长的代码

func isEven(number: Int) -> Bool {
  return number % 2 == 0
}

evens = Array(1...10).filter(isEven)

【讨论】:

  • 这里的问题是不能根据条件动态改变迭代索引。
  • 您能否提供一个真实的示例,您需要根据条件动态更改迭代索引?看起来没有一个简单的解决方法。
  • 看看我上面的例子。工作正常。
  • 在您的特定行星示例中,我宁愿使用 array.filter 方法而不是实现任何棘手的数组循环。
  • 只有在您事先知道要过滤什么的情况下,过滤器才会起作用。在我的情况下,我需要根据循环本身变化的条件跳过一些星球。
【解决方案2】:

另外,如果你提前知道序列,Swift的Stridable类型(即Int)有stride(to:by:)和@987654324 @ 方法。像这样使用它:

for i in 0.stride(to: 10, by: 2) {
  // ...
}

i 将采用 02468 的值。

Strideable Protocol ReferenceSwiftDoc 中的更多信息。

【讨论】:

  • 你是对的,我也调查过这个,但就我而言,我事先不知道顺序。
【解决方案3】:

我对这个问题还有点不清楚,但这里是……

我认为实际的问题是这样的:

let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]
for (var i = 0; i < planets.count; i += 1) {
    let string = planets[i]
    if (string == "Venus" || string == "Saturn") {
        i += 2
    }
    print(string)
}

这会产生:

Mercury
Venus
Jupiter

如果这是对的,那么问题归结为:当我们在行星中循环时,我们最近多久看到金星(或土星)?如果我们最近看到它,请跳过此迭代。 “跳过此迭代”的 Swift 是continue。所以我们所要做的就是跟踪我们最近看到金星的时间:

var marker = -100
for (ix,planet) in planets.enumerate() {
    if ix - marker < 3 {
        continue
    }
    if (planet == "Venus" || planet == "Saturn") {
        marker = ix
    }
    print(planet)
}

话虽如此,但我不认为我会这样写。我要做的是缩短行星列表本身。现在您可以对剩余行星列表做任何您想做的事情。所以现在我们的目标是从行星列表中删除跟随金星(或土星)的行星和跟随它的行星:

let fewerPlanets = planets.enumerate().filter {
    ix, planet in
    for ix in [ix-1, ix-2] {
        if ix >= 0 && (planets[ix] == "Venus" || planets[ix] == "Saturn") {
            return false
        }
    }
    return true
}.map {$1}

现在fewerPlanets["Mercury", "Venus", "Jupiter"],我们可以循环那个并做我们想做的任何事情。

【讨论】:

    【解决方案4】:

    我的方法是编写一个专门用于“行星”的全局函数:

    func planetIncrement(planet: String) -> Int {
        return planet == "Venus" || planet == "Saturn" ? 2 : 1
    }
    

    然后在while循环中使用函数:

    var i = 0
    while i < planets.count {
        i += planetIncrement(planets[i])
    }
    

    好处是函数和while 循环都简单易读。折磨和复杂的循环逻辑是错误的秘诀,所以我会保持简单。

    您编写的 loopIncrementloopDecrement 函数非常酷,但它们可能有点矫枉过正,而且基本上要求您为每个不同的用例编写一个完整的函数。

    另一种重写 while 循环的方法是使用递归函数:

    func iteratePlanets(planets: [String], index i: Int) {
        switch i {
        case let i where i >= planets.count:
            return
        case let i where planets[i] == "Venus" || planets[i] == "Saturn"
            iteratePlanets(planets, index: i + 2)
        case let i:
            iteratePlanets(planets, index: i + 1)
        }
    }
    

    这将与while 循环完全相同。

    编辑:

    也许这样的事情会起作用:

    extension Array {
    
        func loop(start: Int = 0, condition: (Int -> Bool)? = nil, increment: Element -> Int = { _ in 1 }, action: (Element -> ())? = nil) {
            var index = start
            while condition?(index) ?? (index < self.count) {
                action?(self[index])
                index += increment(self[index])
            }
        }
    }
    

    它会循环,并允许您提供自定义条件来终止循环、更改增量,甚至在循环内执行操作,如下所示:

    planets.loop(increment: { planet in
        return planet == "Venus" || planet == "Saturn" ? 2 : 1
    })
    

    不过,我自己的感觉是,这会掩盖代码。由于每个用例都有自己的内部循环逻辑,因此您最终不得不每次都重新编写逻辑。这意味着您基本上每次都编写一个新函数。所以你不妨把逻辑写在循环里面,或者写一个简单易懂的外部函数,这样一切就清楚了。

    也可以将其推广到所有CollectionTypes:

    extension CollectionType {
    
        func loop(start start: Index.Distance = 0, condition: (Index -> Bool)? = nil, increment: Generator.Element -> Index.Distance = { _ in 1 }, action: (Generator.Element -> ())? = nil) {
            var index = self.startIndex.advancedBy(start)
            while condition?(index) ?? (index.distanceTo(self.endIndex) > 0) {
                let item = self[index]
                action?(item)
                index = index.advancedBy(increment(item))
            }
        }
    }
    

    例如,这会遍历 Set 的所有元素,打印每个项目...

    Set([1, 2, 3, 4, 5]).loop(action: { print($0) })
    

    ...这会以相反的顺序遍历数组,打印每个项目:

    [1, 2, 3, 4, 5].loop(start: 4, condition: { $0 >= 0 }, increment: { _ in -1}, action: { print($0) })
    

    它实际上很好地反映了 C 风格的 for 循环。填写完所有参数后,函数调用如下所示:

    planets.loop(
        start: 0,
        condition: { $0 < planets.count },
        increment: { $0 == "Venus" || $0 == "Saturn" ? 2 : 1 },
        action: { print($0) })
    

    使用您的 printObjects 示例,您可以像这样使用它:

    printObjects.loop(
        increment: { $0.printCount >= 0 ? 0 : 1 },
        action: { 
            $0.printText = "This is line \($0.printCount)"
            print($0.printText)
            $0.printCount -= 1 })
    

    编辑:

    我将函数参数名称从terminator更改为condition,以更清楚地表明只要condition返回true,循环就应该继续,这更符合C风格编写了 for 循环。

    【讨论】:

    • 这里的问题是您的函数不是全局的,因为它仅限于行星数组。如果它是一个整数数组呢?如果我想跳过的行星在循环功能运行之前不知道怎么办?在您的代码中,您必须再次像本讨论中的其他示例一样提前知道所有这些。我想要替换索引是变量的 oly C 样式循环。如果我在运行循环之前获得了所有信息,那么我可以简单地用数组过滤器函数交换您的代码。
    • 我在最初的帖子中添加了示例代码。也许现在手头的任务会更清楚。
    【解决方案5】:

    您可以实现一个 Generator/SequenceType 以便能够使用 for-in-loop:

    struct IrregularGenerator<T> : GeneratorType, SequenceType {
        let getIncrement: T -> Int
        var index: Int
        let array: [T]
    
        init(array: [T], start: Int, getIncrement: T -> Int) {
            guard start >= 0 && start < array.count else {
                preconditionFailure("start index is out of bounds")
            }
            self.array = array
            self.index = start
            self.getIncrement = getIncrement
        }
    
        mutating func next() -> T? {
            guard index >= 0 && index < array.count else {
                return nil
            }
            defer {
                index += getIncrement(array[index])
            }
            return array[index]
        }
    }
    
    let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
    
    for planet in IrregularGenerator(array: planets, start: planets.count - 1, getIncrement: { ($0 == "Venus" || $0 == "Saturn") ? -3 : -1 }) {
        print(planet)
    }
    

    这是用我的 IrregularGenerator 重写的 PrintObject 示例(我对其进行了更改以删除 0 增量保护,请注意,这可能会引发无限循环):

    for printObject in IrregularGenerator(
        array: printObjects, start: 0,
        getIncrement: { $0.printCount -= 1; return $0.printCount == 0 ? 1 : 0 }
    ) {
        print(printObject.printText)
    }
    

    【讨论】:

    • 如果我没记错这里你不能根据条件动态改变迭代索引。您必须在循环之前预定义它们。
    • 这就是您使用这种解决方案所能获得的。 for-in-loop 值不可修改。
    • 这就是为什么我创建了在我最初的帖子中显示的扩展,它使用 while 但行为类似于旧的 C 样式 for 循环,您可以在循环内动态更改索引。
    • 我在最初的帖子中添加了示例代码。也许现在手头的任务会更清楚。
    • 我用你的新例子更新了我的答案(并稍微改变了我的 IrregularGenerator)。
    猜你喜欢
    • 2016-07-10
    • 2016-10-20
    • 2010-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多