arakweker
arakweker

Reputation: 1635

Cannot use mutating getter on immutable value: 'self' is immutable error

I'm trying to reuse an older piece of Swift code, but getting an error 'Cannot use mutating getter on immutable value: 'self' is immutable error'. Xcode wanted to add 'mutating' before the func, and offered to do so through a 'fix'. So the error is gone there but still remains at the 'Text' statements.

import SwiftUI

struct ContentView: View {

     typealias PointTuple = (day: Double, mW: Double)
    let points: [PointTuple] = [(0.0, 31.98), (1.0, 31.89), (2.0, 31.77), (4.0, 31.58), (6.0, 31.46)]

    lazy var meanDays = points.reduce(0) { $0 + $1.0 } / Double(points.count)
    lazy var meanMW   = points.reduce(0) { $0 + $1.1 } / Double(points.count)

    lazy var a = points.reduce(0) { $0 + ($1.day - meanDays) * ($1.mW - meanMW) }
    lazy var b = points.reduce(0) { $0 + pow($1.day - meanDays, 2) }

    lazy var m = a / b
    lazy var c = meanMW - m * meanDays        
    lazy var x : Double = bG(day: 3.0)
    lazy var y : Double = bG(day: 5.0)
    lazy var z : Double = bG(day: 7.0)

    mutating func bG(day: Double) -> Double {
        return m * day + c
    }

    var body: some View {
        VStack {
            Text("\(x)")
            Text("\(y)")
            Text("\(z)")
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

Upvotes: 30

Views: 26587

Answers (3)

mfaani
mfaani

Reputation: 36427

A getter cannot mutate

This is mainly a design Swift is enforcing with its getters. The principle is:

The getters should not mutate the object. Because developers may not be expecting that. They should only expect a change when you're using the setter or calling a mutating function. A getter is neither of them.

The following example works as expected:

struct Device {
    var isOn = true
}

let a = Device()
let b = a

print(a.isOn)

When we print, we get the value of a.isOn. Both a,b are identical.

Yet in the following example, the getter will have a side-effect. It won't even compile. But let's just assume that it did and see what happens.

struct Device2 {
    
    var x = 3
    var isOn: Bool {
        x = 5
        return true
    }
}

let a = Device2()
let b = a

print(a.isOn) 

When we print, we get the value of a.isOn. However this time the a,b are no longer identical. a.x will now be 5, because a copy-on-write would have happened, while b.x would still be 3.

Swift has an architecture that doesn't allow "getters to mutate an object".

Exceptions

The Swift architecture has two exceptions to this rule:

  • use lazy
  • use @State or some other property wrapper managed by SwiftUI that can be written to and manages state.

Lazy is mutating:

struct Device2 {
    lazy var x : Double = 3.0

    func log() { // ‼️ ERROR - Cannot use mutating getter on immutable value: 'self' is immutable | Mark method 'mutating' to make 'self' mutable
        print(x)
    } 
}

To make it compile you have to add mutating.

print(x) can then mutate Device2 without problems, because x is a lazy property.

You might be wondering what's different between lazy var x = 5 and var x = 5 and the answer is that the latter has the value of x set upon initialization of the struct...

SwiftUI - special case

Because the body variable is a computed property, you can't mutate/set variables. There's a way around that though.

Mark the variable with a @State property wrapper.

Example. The following code won't compile:

struct ContentView: View {
    var currentDate = Date()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        Text("\(currentDate)")
            .onReceive(timer) { input in
                currentDate = input // ERROR: Cannot assign to property: 'self' is immutable
        }
    }
}

Yet the following will compile, just because it has @State

struct ContentView: View {
    @State var currentDate = Date()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        Text("\(currentDate)")
            .onReceive(timer) { input in
                currentDate = input
        }
    }
}

For more on that see here

Upvotes: 27

Fattie
Fattie

Reputation: 12294

Here is the proof that lazy DOES trigger a mutation in the mind of the Swift compiler chain:

enter image description here

As @MojtabaHosseini says, "Lazy property values will be associated on the first call and this mutates the overall object. So they are considered as mutating."

Upvotes: 1

Mojtaba Hosseini
Mojtaba Hosseini

Reputation: 120113

Because when you call x inside the struct, it's not clear whether the contentView itself is mutable or not. A value type gets mutable only when it is defined as a var.

So it would help if you wrapped it with an immutable value before using it inside a builder function inside the struct.

like this:

func xValue() -> Double {
    var mutatableSelf = self
    return mutatableSelf.x
}

var body: some View {
    VStack {
        Text("\(xValue())")
    }
}

💡Note: Lazy property's value will be associated on the first call and this mutates the object. So they are considered as mutating

Upvotes: 28

Related Questions