WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Conceptual questions about Struct's immutability and property changes from within the struct and from the instance

Forums > Swift

@logi  

struct Test {
    var a: Int

    func changeA() {
        a = 10
    }
}

var testInstance = Test(a: 10)
testInstance.a = 1

1.- I understand, Structs are immutable, nonetheless the Swift compiler allows me to change Property "a" to 1, after it was already initialized to 10, were Structs immutable? why this is allowed?

2.- If the Swift compiler allows to change property "a" from the instance, why it doesn't allow to have the method "changeA" to modify "a" from within the Struct without adding the "mutating" modifier? It seems it is allowed to modify the property from the instance, but not from a method within the Struct, why?

3.- These are more conceptual questions, I fully understand that I can add "mutating" in front of the method and it will compile, but why I have to do that, if by default I am allowed to modify the property from outside the method after initialization, why the extra level of rigor with "mutating" to modify a property, that it is already declared as "var"?

I am very much interested in the theory and concepts behing Struct's immutability, thank you very much for your help.

   

Structs are value types. What does this mean? It means that an instance of a struct is the value. With a reference type, like a class, the instance is just a pointer to that value. This is the key to understanding struct immutability.

1.- I understand, Structs are immutable, nonetheless the Swift compiler allows me to change Property "a" to 1, after it was already initialized to 10, were Structs immutable? why this is allowed?

Structs are immutable in the same sense that an Int value is immutable. If you declare a variable like var myInt = 4, you can change the value of myInt, but you can't change the value of 4. Now and forever, 4 is 4 is 4.

Similarly, given this code:

struct User {
    var name: String
    var id: UUID
    var address: String
}

var user = User(name: "Charlotte Grote", id: UUID(), address: "Tackleford, UK")

You can change the value of user but you cannot change the value of User(name: "Charlotte Grote", id: UUID(), address: "Tackleford, UK"). Changing one of the properties of a User instance creates a whole new instance with the new value. Just like this code:

var myInt = 4
myInt = 6

changes the value of myInt but not the value of 4.

So what happens when you change the property of a struct declared with var is that a new value is created and assigned to the variable.

var user = User(name: "Charlotte Grote", id: UUID(), address: "Tackleford, UK")

//this...
user.name = "Shauna Wickle"

//is the equivalent of this...
user = User(name: "Shauna Wickle", id: user.id, address: user.address)

2.- If the Swift compiler allows to change property "a" from the instance, why it doesn't allow to have the method "changeA" to modify "a" from within the Struct without adding the "mutating" modifier? It seems it is allowed to modify the property from the instance, but not from a method within the Struct, why?

It's a little misleading what's going on, to be honest. When you do testInstance.a = 1 you aren't actually changing the instance of the Test struct that you created in the previous line; you are really creating a new Test instance with a different value for the a property and assigning it to the variable called testInstance. You can't change a value type, you can only create a new one and assign it to the same variable.

Unless you use a mutating method. Tagging a method of a value type with the mutating keyword tells the compiler that you can change the value of a value type from within that value type itself.

struct User {
    var name: String
    var id: UUID
    var address: String

    mutating func changeID(newID: UUID) {
        id = newID
    }
}

var user = User(name: "Charlotte Grote", id: UUID(), address: "Tackleford, UK")

//1
user.id = UUID()

//2
user.changeID(newID:UUID())

//1 creates a new User value with a different value for the id property and assigns the new User to the user variable.

//2 modifies the value assigned to the user variable in place.

A neat feature of mutating methods is that they can assign a completely new value to self.

struct PointLocation {
    var xPos: Double
    var yPos: Double

    init() {
        xPos = 0
        yPos = 0
    }

    mutating func resetPosition() {
        self = PointLocation()
    }
}

var loc = PointLocation()
pos.xPos = 32 //move it around
pos.yPos = 117 //move it around some more
pos.xPos = 4 //and more
pos.xPos = 98 //and still more
pos.yPos = 11 //keep on moving
pos.reset() //put us back at `0,0`

And, in fact, behind the scenes a mutating func actually does this same operation when changing a property. Essentially, the mutating keyword tells the compiler that the implicit self parameter that is passed into a method should be passed as a var instead of a let and then a new instance with the changed value is assigned to self.

So that this method from our earlier User struct:

mutating func changeID(newID: UUID) {
    id = newID
}

is the equivalent of this:

mutating func changeID(newID: UUID) {
    self = User(name: self.name, id: newID, address: self.address)
}

1      

@logi  

thank you very much for the detailed explanation, much clearer now

   

This is an interesting question and answer. If I understand what is said, I have another question:

If there is a large struct with lots of / large properties and the code changes one of them, is a whole new struct created and the original one deleted? Can this cause a significant performance issue?

I came up with an approach to testing this.

import Foundation
struct A {
    static var counter = 1
    var largeData = Data(count: 10_000_000)
    var i = 0

    init() {
        A.counter += 1
    }
}

var a = A()

print (A.counter)
let start = Date()
for j in 0..<1_000_000 {
    a.i = j
}

print (A.counter)
print (Date().timeIntervalSince(start))

When I run this it displays:

2

2

24.861852049827576

If I remove the largeData property, it displays:

2

2

13.234109997749329

This clearly shows that there is a performance penalty for the largeData property. However, if a new copy of a is created every time, why doesn't the static counter increment?

Thanks, Mark

   

The compiler has ways of optimizing your code behind the scenes, so I don't think it would actually make 10 million copies. It's far more likely that the compiler recognizes that only that single Int property is changing and optimizes the compiled code so that it makes the change in place without copying the other properties. My explanation above doesn't really take into account any compiler optimizations, mostly because compiler hoodoo isn't something I'm at all skilled or knowledgeable in.

This same question was posted on the official Swift forums and has some interesting answers there. Heck, in general you can find all sorts of interesting info on the official forums.

   

Thanks for the information. I've done some more testing (heck, that's why it's called software engineering 😄.

import Foundation
struct A {
    var largeData = Data(count: 10_000_000)
    var i = 0

    mutating func adjust(_ j: Int) {
        i = j
    }
}

var a = A()

let start = Date()
for j in 0..<1_000_000 {
    a.i = j
    // a.adjust(j)
}

print (Date().timeIntervalSince(start))

By commenting out one or the other lines in the for loop, the code would either call a mutating function or do a direct assignment to a property. The largeData property was also commented in or out for the test.

Mutating Function: With largeData: 48 seconds Without largeData: 24 seconds

Direct Assignment: With largeData: 24 seconds Without largeData: 13 seconds

Mark

   

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.