Reputation: 2037
In the below code, a struct called Card
is assigned with let
. Then, once assigned, I put this card into an array. Now, in func resetCards
, I want to set each card in the array back to its original state. However, if I use a for loop for each card in the array I get an error saying "cannot assign property to constant"
, which I expect. However, If I do something like: cards[0].variable = false
, I don't get an error and I can change the struct variables. Why if I loop through an array using a for card in cards
loop I can't change the properties of the structs even if the properties are declared using var
, but if I access the structs using an array index e.g. for index in cards.indices
I can?
class Concentration {
var cards = [Card]()
init(numberOfPairsOfCards: Int) {
for _ in 0..<numberOfPairsOfCards {
let card = Card()
cards += [card, card]
}
func resetCards() {
indexOfOneAndOnlyFaceUpCard = nil
for card in cards {
card.variable = true // this doesn't work
cards[0].variable = true // this works
}
}
}
Upvotes: 3
Views: 530
Reputation: 535586
How the struct is "declared" before you put into an array is not really relevant. Let's talk about how things are accessed from an array.
I posit the following test situation:
struct Card {
var property : String
}
var cards = [Card(property:"hello")]
We try to say
for card in cards {
card.property = "goodbye"
}
but we cannot, because card
is implicitly declared with let
and its properties cannot be mutated. So let's try to work around that with a var
reassignment:
for card in cards {
var card = card
card.property = "goodbye"
}
Now our code compiles and runs, but guess what? The array itself is unaffected! That's because card
is a copy of the struct sitting in the array; both parameter passing and assignment make a copy. Actually we could condense that by insisting on a var
reference up front:
for var card in cards {
card.property = "goodbye"
}
But we gain nothing; card
is still a copy, so the array contents are still unaffected.
So now let's try it through indexing, as did in your experiments:
for ix in cards.indices {
cards[ix].property = "goodbye"
}
Bingo! It compiles and runs and changes the contents of the cards
array. That's because we are accessing each card directly within the array. It is exactly as if we had said:
for ix in cards.indices {
var card = cards[ix]
card.property = "goodbye"
cards[ix] = card
}
Yes, we're still making a copy, but we are reassigning that copy back into the same place in the array. The index access is a shorthand for doing that.
However, we are still actually pulling out a copy, mutating it, and reinserting it. We can try to work around that, at the cost of some more elaborate planning, by using inout
, like this:
func mutate(card: inout Card) {
card.property = "goodbye" // legal!
}
for ix in cards.indices {
mutate(card: &cards[ix])
}
As you can see, we are now allowed to set card.property
, because with inout
the parameter is implicitly a var
. However, the irony is that we are still doing a copy and replacement, because a struct is a value type — it cannot really be mutated in place, even though the assignment thru a var
reference gives the illusion that we are doing so.
Upvotes: 4
Reputation: 403
To answer the question why you get a compile error:
You need to declare it as var
instead of let
, which is assumed when omitting the word in a for-in
loop
for var card in cards {
card.variable = true
}
This answer will not help you in the long run, since you are only changing a local copy of a card struct. The array of cards you retain in Concentration are still unchanged
Upvotes: 1
Reputation: 130132
Structs are value types, they are copied when assigned to a variable. When you iterate over an array of value types:
for card in cards {
then card
contains a copy of every element. Any changes won't get saved to the original array.
You can iterate over indices and access the array value directly :
for offset in cards.indices {
cards[offset].variable = true
}
However, usually we are using map
to create a whole new array instead:
cards = cards.map {
var card = $0 // both `$0` and `card` are copies of the original
card.variable = true
return card
}
Upvotes: 2