Learning Swift

Use Swift Struct For Total Immutability

The many ways to prevent mutability of Swift’s struct

Elye
Elye
Nov 15, 2020 · 8 min read
Image for post
Image for post
Photo by Aaron Burden on Unsplash

In Choosing Between Structures and Classes , the document provides us some guides of when to use Class and when to use Struct.

The reasons to use Struct given as are below.

While the above is true, nothing is mentioned about the great benefits I discovered about Struct, with its ability to ensure true immutable.

I’m amazed how thorough Struct are being protected from mutating when it is let. Let’s check it out.

Member VAR of a LET struct cannot be altered externally

In class, if an object is let, we can still alter the member variable that is var, as shown below.

class A { var value = 10 }
let a = A()
a.value = 20 // Okay, can still mutate it even a is let

It’s the because class are reference type, only the memory location is immutable, but the content mutability is independent from it.

However, for struct, it is an error.

struct A { var value = 10 }
let a = A()
a.value = 20 // Can't assign to property: 'a' is a 'let' constant

Copy by Value prevent accidental change

For class, when it is assigned to a new object, it is changed by reference. Altering the new copy object member variable will also change the original object member variable.

class Aclass { var value = 0 }let classA = Aclass()
var classB = classA
classB.value = 1
print("\(classA.value) \(classB.value)") // result is 1 1

Do note for the snippet above, the classA.value has been altered to 1 even though we just change classB.value.

For struct, when assigned to a new object, an entirely new object is created instead. So alter the new object will have no impact on the original object.

struct Astruct { var value = 0 }let structA = Astruct()
var structB = structA
structB.value = 1
print("\(structA.value) \(structB.value)") // result is 0 1

Do note for the snippet above, the structA.value is remained as 0 even though we just change structB.value.

The below diagram helps illustrate this better

Image for post
Image for post

Member VAR can only be altered by internal mutating function

For a struct that has a function to change its own variable, the below will error out with the message “Left side of mutating operator isn’t mutable: ‘self’ is immutable

struct A {
var value = 10
func changeValue() {
value += 10 // Error
}
}

To fix it, we will need to add mutating keyword to it.

struct A {
var value = 10
mutating func changeValue() {
value += 10
}
}

Note: a class doesn’t need a mutating keyword for function that alter internal variable

This also applies to internal functions that calls the changeValue functions, where a mutating keyword is required to define the function.

struct A {
var value = 10
mutating func changeValue() {
value += 10
}
mutating func callChangeValue() {
changeValue()
}
}

How does this helps ensure immutability?

The mutating keyword is used for the compiler to easily know that a function is changing the internal variable, hence it is only allowed to be called by a var struct variable, but not let struct variable.

struct A {
var value = 10
mutating func changeValue() { value += 10 }
}
var x = A()
x.changeValue()
let y = A()
y.changeValue() // Error

The above snippet, the error on y.changeValue is “Cannot use mutating member on immutable value: ‘y’ is a ‘let’ constant”.

By preventing let struct calling the mutating function, this helps ensure the struct doesn’t get mutated.

Lazy is considered mutating

Look at the below code snippet. When the struct is initialized, the lazyBool is not initialized yet. It will only be initialized upon the first use of it.

struct LazyStruct {
lazy var lazyBool: Bool = { false } ()
func accessLazyBool() -> Bool {
return lazyBool // Error
}
}
let a = LazyStruct()
a.lazyBool // Error

Therefore, it is considered an (internally) mutable variable. This cause the above code to generate the error “Cannot use mutating getter on immutable value: ‘self’ is immutable

Note: this issue will not happen if this is Class instead of Struct

To fix this issue, 2 days

1. Remove the lazy

struct LazyStruct {
var lazyBool: Bool = { false } ()
func accessLazyBool() -> Bool {
return lazyBool
}
}
let a = LazyStruct()
a.lazyBool

By not being lazy, this means the mutability of the variable is handled by the setter. Hence it can be controlled from there. The getter side of it is safe, and not an issue.

2. Change to mutating function or variable

struct LazyStruct {
lazy var lazyBool: Bool = { false } ()
mutating func accessLazyBool() -> Bool {
return lazyBool
}
}
var a = LazyStruct()
a.lazyBool

Here we retain the lazy keyword. But we fix it by making our function mutating and also making the variable var.

Capture self-value in escaping closure during construction

An escaping closure is like a function variable that can be performed at a later time.

If we are sending some self value into it, that will risk the closure behave differently upon its execution at a later time, which is immutable

struct Astruct {
var value: Int = 11 // result is regardless of var or let
lazy var closure: () -> () = { // Error
print("\(self.value)")
}

}

The error states “Escaping closure captures mutating ‘self’ parameter”.

Note: this will not be an issue for a class too.

To prevent this issue, we’ll have to capture the value during construction, instead of accessing self in the closure itself. The capturing is done using [] as shown in the code below

struct Astruct {
var value: Int = 11. // result is regardless of var or let
lazy var closure: () -> () = {
[self] in print("\(self.value)")
}

}

Optionally, you can also capture the value itself as below.

struct Astruct {
let value: Int = 11
lazy var closure: () -> () = {
[value] in print("\(value)")
}

}

As this value is captured during construction, it is more deterministic and considered immutable.

Self capturing differences of class and struct

Self capturing not only works for Struct, but also for Class. They seem to behave differently although technically it’s still the expected behavior.

If I have the below set of code

struct Foo {
var bar: Bool
init(bar: Bool) { self.bar = bar }
mutating func doSomething() {
print("Bar was \(self.bar)")
let closure = { [self, oriBar = bar] in
if originalBar != self.bar {
print("Bar has changed \(oriBar) \(self.bar)")
} else
print("Bar didn't change \(oriBar) \(self.bar)")
}
}
self.bar.toggle()
print("Bar is \(self.bar)")
closure()
}
}
var foo = Foo(bar: true)
foo.doSomething()
foo.doSomething()

The result will be as below

Bar was true
Bar is false
Bar didn't change true true
Bar was false
Bar is true
Bar didn't change false false

You’ll notice that for Struct, the below self captured is done during the construction. Hence the self.bar returns the boolean value at the time of closure construction.

       let closure = { [self, oriBar = bar] in
if originalBar != self.bar {
print("Bar has changed \(oriBar) \(self.bar)")
} else
print("Bar didn't change \(oriBar) \(self.bar)")
}
}

However, when we change to class

class Foo {
var bar: Bool
init(bar: Bool) { self.bar = bar }
func doSomething() {
print("Bar was \(self.bar)")
let closure = { [self, oriBar = bar] in
if originalBar != self.bar {
print("Bar has changed \(oriBar) \(self.bar)")
} else {
print("Bar didn't change \(oriBar) \(self.bar)")
}
}
self.bar.toggle()
print("Bar is \(self.bar)")
closure()
}
}
var foo = Foo(bar: true)
foo.doSomething()
foo.doSomething()

The result as below

Bar was true
Bar is false
Bar has changed true false
Bar was false
Bar is true
Bar has changed false true

For Class, it looks as if the below self captured is only done during the execution of construction, hence the self.bar returns the boolean after self.bar.toggle() i.e. opposite from when it is set.

       let closure = { [self, oriBar = bar] in
if originalBar != self.bar {
print("Bar has changed \(oriBar) \(self.bar)")
} else
print("Bar didn't change \(oriBar) \(self.bar)")
}
}

In reality, for both Struct and Class, they are still being captured during construction of the closure when the [self] has been used.

The only difference is, Struct is the value type and Class is the reference type. Hence during construction,

The code sample credits to the below article

Getter properties can have access to self

Previously we show the below

struct Astruct {
var value: Int = 11 // result is regardless of var or let
lazy var closure: () -> () = { // Error
print("\(self.value)")
}

}

The error states “Escaping closure captures mutating ‘self’ parameter”.

But if I do a minor modification as below

struct Astruct {
var value: Int = 11 // result is regardless of var or let
var closure: () -> () {
{ print("\(self.value)") }
}

}

No more error!! Why? Do we allow to pass in self into a closure without first capturing it?

The main difference between the two is as below

lazy var closure: () -> () = {  // Error
print("\(self.value)")
}

and

var closure: () -> () {
{ print("\(self.value)") }
}

The first is really just a variable closure, where a fixed closure i.e. { print(“\(self.value)”) } is set when it is constructed. The = is the key to indicate that.

The second is a getter variable (i.e. there’s no =, but it is surrounded by {}), where each time it is called, it will generate a new closure { print(“\(self.value)”) }. Therefore it is allowed to use self.value, since upon a new closure is generated, it will get the self.value of that moment.

Hopes this clarifies the difference, and avoids confusion. The Struct is still protecting the immutability perfectly.

With the above sharing, we can see how well the Struct protect it’s immutability, to ensure nothing within it really changed, either internally or externally.

Feel free to share with me if you have more insight into how Struct enhances the immutability that is not covered above. Thanks!

Mobile App Development Publication

Sharing Mobile App Development and Learning

Elye

Written by

Elye

Passionate about learning, and sharing mobile development and others https://twitter.com/elye_project https://www.facebook.com/elye.proj

Mobile App Development Publication

Sharing iOS, Android and relevant Mobile App Development Technology and Learning

Elye

Written by

Elye

Passionate about learning, and sharing mobile development and others https://twitter.com/elye_project https://www.facebook.com/elye.proj

Mobile App Development Publication

Sharing iOS, Android and relevant Mobile App Development Technology and Learning

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store