意外更改结构的副本而不是结构本身

Accidentally mutating a copy of a struct instead of the struct itself

提问人:pseudosudo 提问时间:12/2/2018 更新时间:9/29/2023 访问量:315

问:

多年来,我使用标识类型进行编程,我发现使用变异值类型非常有压力,因为经常存在意外分配(从而复制)新变量,然后改变该副本并期望看到这些更改反映在原始结构中的风险(最后给出的示例)。

我的实际问题是有哪些方法/编码风格/实践可以防止这种事情发生?


我在想什么:我无法想象这个问题的答案只是“只要记住你处理的是结构而不是类”,因为这非常容易出错,特别是因为类性/结构性对于使用任何特定数据结构的代码都是完全不透明的。

我还看到很多关于使用“纯功能风格”的讨论,我完全赞成,除了:

  • 在 mutating 方法还需要返回值的情况下(如下面的示例所示),这样做似乎非常尴尬。当然,我可以返回一个元组,但这不可能是 The Way。(newStructInstance, returnValue)
  • 我想,只要你想要突变,实例化结构的新副本就不可能有任何效率(不像重新分配结构,据我所知,它会创建一个新实例而不会产生任何成本)。

我的问题的一些可能答案可能是:

  • 你的例子是人为的,这种事情在实践中很少发生,如果有的话
  • 你使用的是迭代器(反)模式(见下面的代码),所以这就是你的原罪
  • 放宽并仅将 S 用于不可变类型,将 es 用于可变类型mutatingstructclass

这些都朝着正确的方向发展吗?


示例:假设我们有一个结构体,它简单地包装一个整数,它用一个 mutating 方法递增 1。next()

struct Counter {
    var i: Int = 0

    mutating func next() -> {
        self.i += 1
        return self.i
    }
}

在某个地方的某个初始值设定项中,我们像这样实例化

self.propertyWithLongAndAnnoyingName = Counter()

后来在同一范围内,忘记了这个属性包含一个结构体,我们将其分配给简洁命名的局部变量并递增p

var p = self.propertyWithLongAndAnnoyingName
d.next() // actually increments a copy
迅速 结构 不变性 可变

评论

0赞 deej 5/7/2019
在处理整数和浮点数等类型时,您是否也遇到过这个问题?

答:

2赞 matt 12/2/2018 #1

作为一个在 Objective-C 中编程多年,然后在 Swift 首次向公众发布时学习它的人,值类型对象(如结构体)的存在是一个巨大的解脱,而不是你似乎认为的问题。

在 Swift 中,结构体是卓越的对象类型。当你只需要一个对象时,你使用一个结构。使用类是非常特殊的,仅限于以下情况:

  • 你使用的是 Cocoa(它是用 Objective-C 编写的,你只需要接受它的对象是类)

  • 你需要一个超类/子类关系(在纯 Swift 编程中非常罕见;通常这种情况只是因为你使用的是 Cocoa)

  • 其目的是表示外部现实,其中个人身份至关重要(例如,UIView 需要是引用类型,而不是值类型,因为界面中确实存在单独的视图,我们需要能够区分它们)

  • 真正的可变性是必然的

因此,一般来说,大多数程序员的感觉与你假设的心理状态正好相反:当一个对象意外地变成一个引用类型(一个类)并且突变影响到一个远程引用时,他们会感到惊讶。在您提供的代码示例中:

var p = self.propertyWithLongAndAnnoyingName
p.next()

...如果也发生变异,那将是一个彻底的震惊。值类型表示安全的一种形式。的赋值和突变恰恰是为了防止被改变。self.propertyWithLongAndAnnoyingNameppself.propertyWithLongAndAnnoyingName

事实上,可可本身会竭尽全力防止这种事情发生。这就是为什么,例如,模式永远不会是(从外部看到的)NSMutableString,而是一个NSString,它围绕对象设置了一个不可变的围栏,即使它是一个引用类型。因此,在 Swifts 中使用结构解决了一个问题,Cocoa 必须通过更复杂和(对许多初学者来说)神秘的措施来解决这个问题。propertyWithLongAndAnnoyingName

然而,像这样的情况在实际中相对罕见,因为你的假设的另一个方面也是错误的:

我无法想象这个问题的答案只是“只要记住你处理的是结构而不是类”,因为这非常容易出错,特别是因为类性/结构性对于使用任何特定数据结构的代码都是完全不透明的

我自己的经验是,在任何重要的情况下,我总是知道我是在处理结构还是类。简单地说,如果它是一个 Swift 库对象(String、Array、Int 等),它就是一个结构体。(该库基本上没有定义任何类。如果它是一个 Cocoa 对象(UIView、UIViewController),则它是一个类。

事实上,命名约定通常对您有所帮助。基金会叠加层就是一个很好的例子。NSData 是一个类;数据是一种结构。

另请注意,您不能改变对结构的引用,但可以改变对类的引用。因此,在实践中,您很快就会体验到哪个是哪个,因为如果可以的话,您总是使用。letletlet

最后,如果将一个对象交给某个第三方进行就地突变对你来说真的很重要,那就是目的。在这种情况下,您知道这一点,因为您必须显式传递引用(地址)。如果你传递给的参数不是 ,你会假设你的原始对象是安全的,不会发生突变,你会为此感到高兴。inoutinout