Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 427
juliannehong Avatar asked Oct 11 '18 23:10

juliannehong


People also ask

Can you mutate with forEach?

No, forEach does not mutate the original array. You can achieve what you are looking for by giving a second parameter of index , then updating the values of the original array.

What is difference between forEach and for loop in Swift?

To sum up: Using a for loop gives us a much greater degree of control over an iteration, while using forEach enables us to take advantage of the power of closures and first class functions, even though we won't be able to stop an iteration once it was started (apart from throwing an error, that is).

How do you change a struct to an array?

C = struct2cell( S ) converts a structure into a cell array. The cell array C contains values copied from the fields of S . The struct2cell function does not return field names. To return the field names in a cell array, use the fieldnames function.

What is mutating function in Swift?

What can a mutating function do? Essentially, a function that's been marked as mutating can change any property within its enclosing value. The word “value” is really key here, since Swift's concept of structured mutations only applies to value types, not to reference types like classes and actors.


2 Answers

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 }
like image 119
Louis Lac Avatar answered Sep 22 '22 08:09

Louis Lac


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

like image 33
Dale Avatar answered Sep 20 '22 08:09

Dale