Darkisa
Darkisa

Reputation: 2037

Swift can change struct declared with let if using an index but not if using a loop

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

Answers (3)

matt
matt

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

Philipp Kinschel
Philipp Kinschel

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

Sulthan
Sulthan

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

Related Questions