'Swift 4.2) Mutate array of struct with for_in/forEach vs. access by index

I am trying to modify struct element in array. I found that you can do that by accessing(iterate) the struct by index, but you can't if you use 'for in' loop or forEach{}.

struct Person
{
  var age = 0
  var name = "James"
}

var personArray = [Person]()
personArray += [Person(), Person(), Person()]


personArray.forEach({$0.age = 10}) // error: "Cannot assign to property: '$0' is immutable"

for person in personArray { 
  person.age = 10 // error: "Cannot assign to property: 'person' is a 'let' constant"
}


for index in personArray.indices {
  personArray[index].age = 10 // Ok
}

Can someone explain?



Solution 1:[1]

As stated in other answers you can't mutate in a for-in loop or in a .forEach method.

You can either use your last formulation which is short and concise:

for index in personArray.indices {
    personArray[index].age = 10
}

Or mutate the orignal personArray entirely:

personArray = personArray.map { person in 
    var person = person // parameter masking to allow local mutation
    person.age = 10
    return person
}

Note that the second option may seems less efficient as it creates a new instance of Person each time but Swift seems to be well optimised for those cases.

Here is a bonus if you really want a mutating counterpart for .forEach method :

extension MutableCollection {
    mutating func mutateEach(_ body: (inout Element) throws -> Void) rethrows {
        for index in self.indices {
            try body(&self[index])
        }
    }
}

This is a wrapper around array mutation equivalent to your first formulation, you can use it like intended:

personArray.mutateEach { $0.age = 10 }

Solution 2:[2]

In Swift a struct is a value type. In the for or foreach loop the person item is a value, and if it were mutable you would only be changing a copy of the original, not the original as you intend.

If you really want an updatable reference to the struct inside the loop add a var keyword, but remember you are updating a copy not the original.

for var person in personArray {
    person.age = 10 // updates a copy, not the original
}

By contrast class is a reference type and in the loop each item is a reference to the original. Updating this reference value now updates the original.

Change your definition of Person to class instead of struct and it will work as expected. For a complete explanation see https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2