2

I suppose this is a question that must get asked for every language, but when you write for example:

while i < array.count {
    ...
}

does array.count get evaluated each time the loop runs? Is it better to store it in a let constant before running the loop like this?

let length = array.count
while i < length {
    ...
}
4
  • Usually such an explicit check can be avoided completely, e.g. for idx in array.indices or for elem in array. – Even for i in 0..<array.count would evaluate the count only once. Commented Nov 18, 2016 at 16:33
  • 3
    You better hope that array.count is evaluated each time because you just might be adding and/or removing objects from the array inside the while loop. Commented Nov 18, 2016 at 16:42
  • @rmaddy some compilers might detect this and change the behavior accordingly Commented Nov 18, 2016 at 16:43
  • 2
    "Yes" in a while and "no" in a for loop Commented Nov 18, 2016 at 16:59

5 Answers 5

11

Yes, the value is accessed every time the condition is evaluated for the loop. In the example from the question, there would be no difference from moving it out to a variable because of how Array is implemented.

Array gets the count property because it conforms to Collection. The documentation for count in Collection states

Complexity: O(1) if the collection conforms to RandomAccessCollection; otherwise, O(n), where n is the length of the collection.

Since Array also conforms to RandomAccessCollection, it is a constant time operation to get the count of the array. There shouldn't be any major performance difference between getting it once at the start vs every loop iteration.

Sign up to request clarification or add additional context in comments.

3 Comments

This answer doesn't actually answer the question of "does array.count get evaluated each time the loop runs".
To be clear, in for i in 0 ..< A.count { ... } , the A.count is evaluated exactly once.
Even though this answer doesn't answer the actual question. I was searching for exactly this information.
6

while loops (and do while loops) have their predicates evaluated on each iteration.

for loops evaluate the sequences once.

Here's is a demonstration:

var array: [Int]

print("Test Case 1 - while i < array.count")
array = [1, 2, 3, 4, 5, 6]
var i = 0
while i < array.count {
    print(array[i])
    if i < 3 { array.append(123) }
    i += 1
}
print("\r\nTest Case 2 - for i in array.indices")
array = [1, 2, 3, 4, 5, 6]
for i in array.indices {
    print(array[i])
    if i < 3 { array.append(123) }
}

print("\r\nTest Case 3 - for i in 0 ..< array.count")
array = [1, 2, 3, 4, 5, 6]
for i in 0 ..< array.count {
    print(array[i])
    if i < 3 { array.append(123) }
}
  • Test Case 1 - while i < array.count

    1
    2
    3
    4
    5
    6
    123
    123
    123
    
  • Test Case 2 - for i in array.indices

      1
      2
      3
      4
      5
      6
    
  • Test Case 3 - for i in 0 ..< array.count

    1
    2
    3
    4
    5
    6
    

Further details

for loops are just syntactic sugar for easily creating an iterator and iterating over it.

This for loop:

let myArray = [1, 2, 3, 4, 5]

for element in myArray {
    print(element)
}

behaves as if you wrote:

let myArray = [1, 2, 3, 4, 5]

let _myArrayIterator = myArray.makeIterator()

while let element = _myArrayIterator.next() {
    print(element)
}

The specific kind of iterator being made depends on the sequence being iterated. In the case of Array, it uses an IndexingIterator (source). The default behaviour of Collections that use IndexingIterators is to construct those iterators with a copy of self (source). When this happens, any further changes to self aren't reflecting in the iterator. It holds onto its own independent copy, so any changes to the size of your array aren't reflected in its independent copy.

You can try this for yourself, by making your own Sequence and Iterator which logs what's happening:

/// A simple wrapper around an array, which prints what's happening
struct ArrayWrapper<Element> {
    var elements: [Element]
}

extension ArrayWrapper: Sequence {
    func makeIterator() -> MyIndexingIterator<Self> {
        print("ArrayWrapper.makeIterator() - The `for` loop made an iterator, copying the wrapped array\n")
        return MyIndexingIterator(
            elements: self, // A *copy* of `self` is given to the new iterator
            position: self.startIndex
        )
    }
}

extension ArrayWrapper: Collection { // Forward all the collection requirements to the real underlying array
    var startIndex: Int { elements.startIndex } 
    var endIndex: Int { elements.endIndex }
    func index(after i: Int) -> Int { elements.index(after: i) }
    
    subscript(index: Int) -> Element {
        get { elements[index] }
        set { elements[index] = newValue }
    }
}

struct MyIndexingIterator<Elements: Collection>: IteratorProtocol {
    let elements: Elements
    var position: Elements.Index
    
    mutating func next() -> Elements.Element? {
        print("ArrayWrapper.next() - The `for` loop is pulling the next element from the iterator")
        if position == elements.endIndex {
            print("\t- Position (\(position)) reached the endIndex (\(elements.endIndex)), so there are no more elements.")
            return nil
        }
        
        let element = elements[position]
        print("\t- Found \"\(element)\" at position \(position)")
        
        let positionBefore = position
        elements.formIndex(after: &position) // Advance the position by one
        print("\t- Advancing the position from \(positionBefore) -> \(position)")
        
        return element
    }
}

let myArray = ArrayWrapper(elements: ["A", "B", "C", "D", "E"])

print("Before the `for` loop\n")
for element in myArray {
    print("`for` loop body - printing \"\(element)\"\n")
}
print("\nAfter the `for` loop")

prints:

Before the `for` loop

ArrayWrapper.makeIterator() - The `for` loop made an iterator, copying the wrapped array

ArrayWrapper.next() - The `for` loop is pulling the next element from the iterator
    - Found "A" at position 0
    - Advancing the position from 0 -> 1
`for` loop body - printing "A"

ArrayWrapper.next() - The `for` loop is pulling the next element from the iterator
    - Found "B" at position 1
    - Advancing the position from 1 -> 2
`for` loop body - printing "B"

ArrayWrapper.next() - The `for` loop is pulling the next element from the iterator
    - Found "C" at position 2
    - Advancing the position from 2 -> 3
`for` loop body - printing "C"

ArrayWrapper.next() - The `for` loop is pulling the next element from the iterator
    - Found "D" at position 3
    - Advancing the position from 3 -> 4
`for` loop body - printing "D"

ArrayWrapper.next() - The `for` loop is pulling the next element from the iterator
    - Found "E" at position 4
    - Advancing the position from 4 -> 5
`for` loop body - printing "E"

ArrayWrapper.next() - The `for` loop is pulling the next element from the iterator
    - Position (5) reached the endIndex (5), so there are no more elements.

After the `for` loop

6 Comments

does this mean unless you know what you're doing, you shouldn't add/remove elements in a for loop?
99% of the time, there are better ways. Some times there aren't. But filter, slicing, etc. can get you pretty far.
If I tell you I understand why those if statements aren't executing, I'm lying
@Fourth Wanna elaborate on what's confusing you? I can probably help explain it
@Fourth It's not necessarily a safety thing, but it's just the semantics of for. It creates an iterator once at the start by calling makeIterator on the sequence you gave it. That iterator is already made; further changes to the array's count don't magically travel back in time to effect that iterator
|
2

Yes it's evaluated on each iteration.

Assigning to a constant will be slightly more performant. However with all of the optimisations in a modern compiler I wouldn't bother. Unless the loop count is going to be humongous.

1 Comment

Careful with a for loop ... count in a range will get evaluated only once!
1

Careful with for loops like the following. Since elems.count is part of a range that gets constructed once at the top of the loop, it is evaluated exactly once. The following code will die when i = 4:

var elems = [1, 2, 3, 4, 5, 6]

for i in 0 ..< elems.count {
    if i % 2 == 0 { // remove even elements
        elems.remove(at: i)
    }
}

The array.count in your while does indeed get evaluated each time the condition is evaluated.

Comments

0

Yes, it gets called every time

Let's run a simple test, first of all we need the following Array Extension

extension Array {
    var myCustomCount: Int {
        print("myCustomCount")
        return self.count
    }
}

And then we can try this code

let nums = [1, 2, 3, 4, 5]
var i = 0
while i < nums.myCustomCount {
    i += 1
}

The output is

myCustomCount
myCustomCount
myCustomCount
myCustomCount
myCustomCount
myCustomCount

enter image description here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.