【问题标题】:Why constant constraints the property from a structure instance but not the class instance?为什么常量约束结构实例而不是类实例的属性?
【发布时间】:2016-10-26 22:16:21
【问题描述】:

当我尝试更改 byValueObj 实例的 ID 属性时,我收到一条错误消息,告诉我无法分配给常量的属性,即使该属性是变量也是如此。但是,我可以在类实例上做到这一点。我有点知道它可能与按值和按引用机制有关。但我对它的认识不是很清楚和正确。有人可以为我解释一下吗?谢谢。

struct CreatorValue{
    var ID = 2201
}
class CreatorRefer{
    var ID = 2203
}

let byValueObj = CreatorValue()
let byReferObj = CreatorRefer()

byValueObj.ID = 201 //Error: cannot assign to property: 'byValueObj' is a 'let' constant
byReferObj.ID = 203 //works fine here

【问题讨论】:

  • 但是你的结构是一个常量,你不能简单地改变一个let结构实例的内容。
  • 这种情况下的课怎么样?它也被定义为let,但它的属性可以重新分配
  • 类总是被允许改变它们的成员变量值。
  • 谢谢,很高兴知道这一点。

标签: swift class structure constants


【解决方案1】:

Swift 中的结构是 value types - 从语义上讲,值(即值类型的“实例”)是不可变的。

值类型的突变,无论是通过直接更改属性的值,还是通过使用mutating 方法,都等同于将一个全新的值赋给该变量持有它(加上突变触发的任何副作用)。因此,持有它的变量必须是var。属性观察者围绕值类型的行为很好地展示了这种语义,as iGodric points out

所以这意味着你可以这样想:

struct Foo {
    var bar = 23
    var baz = 59
}

// ...

let foo = Foo()
foo.bar = 7 // illegal

这样做:

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)

您可以清楚地看到 - 此代码是非法的。您不能将fooCopy 分配回foo - 因为它是let 常量。因此,您不能更改声明为 let 的值类型的属性,因此需要将其设为 var

(值得注意的是,编译器实际上并没有经过这个palaver;它可以直接改变结构的属性,这可以通过查看the SIL generated看到。这并没有改变语义值类型。)


之所以可以更改let 常量class 实例的可变属性,是因为类是引用类型。因此,let 常量只能确保 reference 保持不变。改变它们的属性不会以任何方式影响您对它们的引用——您仍然指的是内存中的 same 位置。

您可以将引用类型想象为路标,因此代码如下:

class Foo {
    var bar = 23
    var baz = 59
}

// ...

let referenceToFoo = Foo()

你可以这样想内存表示:

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

当你改变一个属性时:

referenceToFoo.bar = 203

引用 (referenceToFoo) 本身 不受影响 - 您仍然指向内存中的相同位置。底层实例的属性发生了变化(意味着底层实例发生了变异):

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

只有当您尝试将 new 引用分配给 referenceToFoo 时,编译器才会给您一个错误,因为您正在尝试改变引用本身:

// 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()

因此,您需要将 referenceToFoo 设为 var 才能使此分配合法。

【讨论】:

  • 很好的解释!非常感谢”)
  • @SLN 乐于助人:)
  • @SLN 仅供参考 - 我今天对这个答案进行了相当大的编辑,以反映结构实际上可以直接变异(无需复制)的事实。我希望您有时间查看更改,如果您有任何问题,我很乐意回答:)
  • 我确实阅读了您的编辑答案。非常感谢您在将近 6 个月后的更新。顺便说一句,新年快乐:)
  • @SLN 好吧,知道什么——我对我的回答做了另一个突变(哈!),我想我现在终于对它感到满意了。真的,我认为我只是陷入了编译器所做工作的实现细节,而不是仅仅关注值类型的重要语义。一如既往,很高兴回答有关编辑的问题,很抱歉再次打扰您!
【解决方案2】:

struct 是一个值类型。如果你编辑它们,你正在调用属性的默认设置器,它只是一个变异方法,它只是结构的一个静态方法,它具有self 作为第一个参数为inout,它返回方法(Swift现在有未应用的方法调用的咖喱语法,但will change that to a flattened one)。顺便说一句:当方法没有发生变异时,它不会是inout

因为inout 正在通过复制输入 - 复制输出 工作,所以即使没有任何更改,也会调用didSet

“如果您将具有观察者的属性作为输入输出参数传递给函数,则将始终调用 willSet 和 didSet 观察者。” 摘自:Apple Inc. “Swift 编程语言 (Swift 3)”。电子书。 https://itun.es/de/jEUH0.l

当属性发生突变时将复制var 结构的代码:

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

调用didSet,即使我们只改变了结构属性中的 Int。

如果它是完整的新副本,那么您会期望变异的结构是具有新内存分配的新副本。但这不是示例中发生的情况,请参见下面的代码。

// 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

所以结构不会被复制。但是因为是inout

“使用copy-in-copy-out给出的模型编写你的代码,而不依赖于引用调用优化,这样无论有没有优化,它都能正确运行。” 摘自:Apple Inc. “Swift 编程语言 (Swift 3)”。电子书。 https://itun.es/de/jEUH0.l


仅限inout 的代码示例:

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

【讨论】:

  • @Hamish 仔细阅读。我只写该结构不在副本的新内存地址上,而不是没有发生复制。是的,我无法检查是否有临时结构。而且我也承认,复制的理论是制作、变异然后设置到相同的地址听起来很合理(这首先是你的理论)。
  • 原来我们都错了——我今天正在查看 SIL(参见 this example gist),结果发现结构实际上可以直接变异(无需复制)。在大多数情况下,只需查找并直接写入属性的地址。
  • 刚刚尝试发出 raw SIL(任何优化之前),结构仍然可以直接变异:)
  • 你是对的,setter 使用 @inout 作为 self 参数——尽管从发出的原始 SIL 中我可以看出,值类型存储属性的 setter 只会被引用在提供 didSet/willSet 观察者或使用协议时。所以在大多数情况下,属性设置器甚至不相关。另请注意,据我所知,inout 参数从复制到复制到通过引用传递似乎发生在原始 SIL 和规范 SIL 之间,当“保证优化”发生时——即使在-Onone 构建。
  • 哦,我当然同意,从高层次的角度来看,最好将值语义视为复制而不是直接变异——抱歉,我只是被 SIL 带走了(可能太多了): ) 当我有时间时,我会在我的答案中添加一个附录以包含这些详细信息,以便我们清理 cmets。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-22
  • 1970-01-01
  • 2014-12-07
  • 2020-07-26
相关资源
最近更新 更多