3

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
    }
  }
}
Darkisa
  • 1,899
  • 3
  • 20
  • 40
  • What are you expecting the line ‘ cards += [card, card]’ to do? – Nick Dec 15 '18 at 21:19
  • Basically I am creating the game concentration. So for every new card that I create, I need to create an identical copy of it as well and add it to an array of cards. The array holds all the cards and must have two and only two of each card. – Darkisa Dec 15 '18 at 21:21
  • This won’t add a copy of the card, the list will contain pointers to the same instance. – Nick Dec 15 '18 at 21:34
  • 1
    Related (if not duplicates): https://stackoverflow.com/q/29777891/1187415, https://stackoverflow.com/q/34317668/1187415. – Martin R Dec 15 '18 at 21:38
  • @nick It would create a copy since structs are value types and not reference types. So `cards += [card, card]` creates two unique copies that don't reference each other i.e. changing one won't change the other. However, if `Card` was a class instead of a struct, you would be correct. – Darkisa Dec 19 '18 at 17:40

3 Answers3

4

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Beautifully explained. Thank you. Just one question. When you say "card is implicitly declared with let" does that mean all for loops implicitly use let e.g. `for let card in cards` or does it depend on something else? – Darkisa Dec 15 '18 at 21:36
  • For more info, see my http://www.apeth.com/swiftBook/ch04.html#SECreferenceTypes and also my answer here: https://stackoverflow.com/a/27366050/341994 – matt Dec 15 '18 at 21:36
  • It means what you said. `for...in` implies `let`. You can actually work around this by saying `for var`; I'll add that to my answer. – matt Dec 15 '18 at 21:38
  • How does this answer explain the line ‘cards += [card,card]‘? – Nick Dec 15 '18 at 21:38
2

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
}
Sulthan
  • 128,090
  • 22
  • 218
  • 270
1

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