Why constant constraints the property from a structure instance but not the class instance?

后端 未结 2 1589
野趣味
野趣味 2020-11-27 22:34

When I trying to change the ID property of the byValueObj instance, I received an error that told me I cannot assign to the property of a constant,

相关标签:
2条回答
  • 2020-11-27 22:46

    struct is a value type. If you edit them you are calling the default setter for the property which is nothing but a mutating method, which is nothing but a static method of the struct which has self as the first argument as inout which returns the method (Swift for now has curry syntax for unapplied method calls, but will change that to a flattened one). Just as a side note: When the method is not mutating it will not be inout.

    Because inout is working by copy in - copy out, didSet is called, even if nothing changed.

    “If you pass a property that has observers to a function as an in-out parameter, the willSet and didSet observers are always called.” Excerpt From: Apple Inc. “The Swift Programming Language (Swift 3).” iBooks. https://itun.es/de/jEUH0.l

    Code that a var struct will be copied when a property is mutated:

    struct Size {
        var width: Int
        var height: Int
    }
    
    struct Rectangle {
        var size: Size
    }
    
    var screenResolution = Rectangle.init(size: Size.init(width: 640, height: 480)) {
        didSet {
            print("Did set the var 'screenResolution' to a new value! New value is \(screenResolution)")
        }
    }
    
    screenResolution.size.width = 800
    

    Calls the didSet even though we only mutated a the Int in the property of the struct.

    If it would be complete new copy then you would expect the mutated struct to be a new copy with new memory allocation. But this is not what happens in the example, see code below.

    // Value of a pointer is the address to the thing it points to
    internal func pointerValue(of pointer: UnsafeRawPointer) -> Int {
        return unsafeBitCast(pointer, to: Int.self)
    }
    
    internal struct MyStruct {
        internal var a: Int
    }
    
    internal var struct1: MyStruct = MyStruct.init(a: 1)
    pointerValue(of: &struct1) // output: 4405951104
    
    struct1.a = 2
    pointerValue(of: &struct1) // output: 4405951104
    

    So the structure is not copied. But because it is inout:

    “Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.” Excerpt From: Apple Inc. “The Swift Programming Language (Swift 3).” iBooks. https://itun.es/de/jEUH0.l


    Code example with inout only:

    struct MyType {
        var x: Int
        mutating func m1(y: Int) -> Int {
            x += 1
            return x + y
        }
    
    }
    
    let mytypem1: (inout MyType) -> (Int) -> Int = MyType.m1
    var myType = MyType.init(x: 1) // has to be "var"
    mytypem1(&myType)(2) // returns 3
    
    0 讨论(0)
  • 2020-11-27 23:09

    Structures in Swift are value types – and, semantically speaking, values (i.e 'instances' of value types) are immutable.

    A mutation of a value type, be it through directly changing the value of a property, or through using a mutating method, is equivalent to just assigning a completely new value to the variable that holds it (plus any side effects the mutation triggered). Therefore the variable holding it needs to be a var. And this semantic is nicely showcased by the behaviour of property observers around value types, as iGodric points out.

    So what this means is that you can think of this:

    struct Foo {
        var bar = 23
        var baz = 59
    }
    
    // ...
    
    let foo = Foo()
    foo.bar = 7 // illegal
    

    as doing this:

    let foo = Foo()
    
    var fooCopy = foo // temporary mutable copy of foo.
    
    fooCopy.bar = 7   // mutate one or more of the of the properties
    
    foo = fooCopy     // re-assign back to the original (illegal as foo is declared as
                      // a let constant)
    

    And as you can clearly see – this code is illegal. You cannot assign fooCopy back to foo – as it's a let constant. Hence, you cannot change the property of a value type that is declared as a let, and would therefore need make it a var.

    (It's worth noting that the compiler doesn't actually go through this palaver; it can mutate the properties of structures directly, which can be seen by looking at the SIL generated. This doesn't change the semantics of value types though.)


    The reason you can change a mutable property of a let constant class instance, is due to the fact that classes are reference types. Therefore being a let constant only ensures that the reference stays the same. Mutating their properties doesn't in any way affect your reference to them – you're still referring to the same location in memory.

    You can think of a reference type like a signpost, therefore code like this:

    class Foo {
        var bar = 23
        var baz = 59
    }
    
    // ...
    
    let referenceToFoo = Foo()
    

    you can think of the memory representation like this:

    |    referenceToFoo     |  --->  | Underlying Foo instance |
    | (a reference to 0x2A) |        |<----------------------->|
                                     |0x2A       |0x32         |0x3A
                                     |  bar: Int |  baz : Int  |
                                     |     23    |      59     |
    

    And when you mutate a property:

    referenceToFoo.bar = 203
    

    The reference (referenceToFoo) itself isn't affected – you're still pointing to the same location in memory. It's the property of the underlying instance that's changed (meaning the underlying instance was mutated):

    |    referenceToFoo     |  --->  | Underlying Foo instance |
    | (a reference to 0x2A) |        |<----------------------->|
                                     |0x2A       |0x32         |0x3A
                                     |  bar: Int |  baz : Int  |
                                     |    203    |      59     |
    

    Only when you attempt to assign a new reference to referenceToFoo will the compiler give you an error, as you're attempting to mutate the reference itself:

    // attempt to assign a new reference to a new Foo instance to referenceToFoo.
    // will produce a compiler error, as referenceToFoo is declared as a let constant.
    referenceToFoo = Foo()
    

    You would therefore need to make referenceToFoo a var in order to make this assignment legal.

    0 讨论(0)
提交回复
热议问题