意外地突变了结构的副本,而不是结构本身

时间:2018-12-02 03:20:49

标签: swift struct immutability mutable

使用身份类型编程已有多年,我发现使用变异值类型非常困难,因为存在不断偶然地将变量赋值(并复制)到新变量然后变异该 copy 的风险。希望看到那些更改反映在原始结构中(最后给出示例)。

我的实际问题有什么方法/编码样式/做法可以防止此类情况的发生?


我在想什么:我无法想象这个问题的答案仅仅是“只记得您正在处理一个结构而不是一个类”,因为这极容易出错尤其是因为class-ness / struct-ness对于使用任何特定数据结构的代码完全不透明。

我也看到了很多有关使用“ purely functional style”的讨论,除了:

    在mutation方法还需要返回值的情况下,
  • 似乎很尴尬(例如下面的示例)。当然,我可以返回一个像(newStructInstance, returnValue)这样的元组,但是那不可能是The Way。
  • 我认为无论何时要进行突变实例化结构的新副本都不会有效(与重新分配结构不同,据我所知,结构的重新创建不会产生任何成本)。

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

  • 您的示例是人为设计的,这种事情很少(如果有的话)在实践中发生
  • 您正在使用迭代器(反)模式(请参见下面的代码),所以那是您的原始罪过
  • 轻松使用mutating,仅将struct s用于不可变类型,将class es用于可变类型

这是否朝着正确的方向?


示例:假设我们有一个结构,该结构简单地包装了一个整数,该整数使用突变方法next()递增1。

struct Counter {
    var i: Int = 0

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

在某个初始化器中,我们像这样实例化

self.propertyWithLongAndAnnoyingName = Counter()

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

var p = self.propertyWithLongAndAnnoyingName
d.next() // actually increments a copy

1 个答案:

答案 0 :(得分:3)

您的问题,如果它不仅仅是一个精心设计的巨魔,似乎比实际更具有心理性,因此这可能不适合Stack Overflow:这是一个见解。但是,认真对待您并假设问题是真诚的,作为经验,我可以告诉您,作为一个在Objective-C进行了多年编程然后在首次向公众发布Swift时就学到了东西的人,诸如结构之类的值类型对象是一个巨大的救济,而不是像您所假定的那样存在问题。

在Swift中,结构是 par Excellence 对象类型。当您只需要一个对象时,就可以使用结构。使用类是非常例外的,仅限于以下情况:

  • 您正在使用Cocoa(它是用Objective-C编写的,您只需要接受其对象是类)即可。

  • 您需要一个超类/子类关系(在纯Swift编程中很少见;通常这种情况仅是因为您使用的是可可粉)

  • 该对象用于表示个人身份至关重要的外部现实(例如,UIView必须是引用类型,而不是值类型,因为接口中确实存在单独的视图,因此我们需要能够区分它们)

  • 真正的可变性是我们的迫切需求

因此,通常,大多数程序员会感到与您所假设的心态恰恰相反:当对象意外地成为引用类型(类)并且变异影响远程引用时,他们会感到惊讶。在您提供的代码示例中:

var p = self.propertyWithLongAndAnnoyingName
p.next()

...如果self.propertyWithLongAndAnnoyingName也被突变,将是完全震惊。值类型表示安全的一种形式。准确地将p的分配和p的变异更改为绝缘 self.propertyWithLongAndAnnoyingName

事实上,可可本身竭尽全力保护反对这类事情。例如,这就是为什么模式是propertyWithLongAndAnnoyingName永远不会(从外面看)是NSMutableString -它将是NSString,即使它是对象,它也围绕对象建立了不变性的篱笆。参考类型。因此,在Swifts中使用结构解决了这个问题,可可必须通过更为详尽的(对许多初学者而言)神秘的方法来解决。

但是,由于您的假设的另一个方面也是错误的,因此这种情况在实际情况中相对很少见:

  

我无法想象这个问题的答案仅仅是“只记得您正在处理一个结构而不是一个类”,因为这极容易出错,尤其是因为class-ness / struct-ness完全不透明使用任何特定数据结构的代码

我自己的经验是,在任何重要的情况下,我总是知道我是在处理结构还是在处理类。简单来说,如果它是一个Swift库对象(字符串,数组,整数等),则它是一个结构。 (该库基本上根本没有定义任何类。)如果它是可可对象(UIView,UIViewController),则它是一个类。

实际上,命名约定通常可以为您提供帮助。基础叠加层就是一个很好的例子。 NSData是一个类;数据是一个结构。

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

最后,如果将一个对象交给某个第三方进行就地突变真的很重要,那就是inout的目的。在这种情况下,您就知道了,因为您必须显式地传递引用(地址)。如果您要传递的参数是 not inout,则您将假定原始对象是安全的,并且不会发生突变。