Swift和可变结构体

133

在 Swift 中,关于变异值类型(mutating value types) 我有一些不太理解的地方。

"The Swift Programming Language" iBook中指出:默认情况下,值类型(value type)的属性不能在其实例方法(instance methods)中被修改。

所以,我们可以在结构体(structs)和枚举(enums)中声明带有 mutating 关键字的方法,从而使这种修改成为可能。

但是我不太清楚的是: 您可以从结构体外部更改变量(var),但无法从其自身的方法(methods)中更改它。这对我来说似乎与直觉相违背,因为在面向对象的语言中,通常会尝试封装变量以便仅能在内部进行更改。但是对于结构体来说似乎恰好相反。以下是一个代码片段:

struct Point {
    var x = 0, y = 0
    mutating func moveToX(x: Int, andY y:Int) { //Needs to be a mutating method in order to work
        self.x = x
        self.y = y
    }
}

var p = Point(x: 1, y: 2)
p.x = 3 //Works from outside the struct!
p.moveToX(5, andY: 5) 

有没有人知道为什么结构体无法在自己的上下文中更改其内容,而内容在其他地方可以很容易地被更改?

8个回答

97
可变性属性标记在存储(常量或变量)上,而不是类型。可以将 struct 视为两种模式:可变不可变。如果将结构体值分配给不可变的存储(Swift 中称之为 let常量),该值将变为不可变模式,并且不能更改值中的任何状态(包括调用任何可变方法)。
如果将值分配给可变存储(Swift 中称之为 var变量),则可以自由修改其状态,并允许调用可变方法。
此外,类没有这种不可变/可变模式。我的看法是,这是因为类通常用于表示可引用实体。可引用实体通常是可变的,因为使用适当的性能管理实体的引用图形以不可变方式非常困难。他们可能会在以后添加此功能,但至少现在还没有。
对于 Objective-C 程序员来说,可变/不可变概念非常熟悉。在 Objective-C 中,我们为每个概念有两个单独的类,但在 Swift 中,您可以使用一个 struct 来实现这一点。省去了一半的工作。
对于 C/C++ 程序员,这也是非常熟悉的概念。这正是 C/C++ 中 const 关键字的作用。
另外,不可变值可以被非常好地优化。理论上,Swift 编译器(或 LLVM)可以在传递由let传递的值时执行复制省略,就像在 C++ 中一样。如果明智地使用不可变结构体,则其性能将超过引用计数类。

更新

如 Joseph 所述,此内容并没有提供解释“为什么”的原因。我稍微补充了一下。

结构体有两种方法:普通可变方法。 普通方法意味着不可变(或不可变)。此分离仅存在于支持不可变语义的情况下。不可变模式下的对象不应更改其状态。

然后,不可变方法必须保证语义上的不可变性。这意味着它不应更改任何内部值。因此,编译器禁止在不可变方法中更改自身状态。相反,变异方法可以自由修改状态。

然后,你可能会有一个问题,为什么不可变是默认选择?那是因为很难预测可变值的未来状态,通常成为头痛和错误的主要来源。许多人都认为解决方案是避免可变的东西,因此,在C/C++家族语言及其派生语言中,默认情况下使用不可变是愿望清单的首选项数十年。

有关更多详细信息,请参见纯函数式风格。无论如何,我们仍然需要可变的东西,因为不可变的东西也有一些弱点,讨论它们似乎与本文无关。


2
所以基本上,我可以这样解释:结构体事先不知道它是可变的还是不可变的,因此它假定是不可变的,除非另有说明。从这个角度来看,它确实有意义。这正确吗? - Mike Seghers
3
虽然这是迄今为止两个答案中最好的答案,但我认为它没有回答为什么值类型的属性默认情况下不能从实例方法内部修改。你提到结构体默认是不可变的,但我认为那不是真的,希望你能解释一下原因。 - Joseph Knight
4
并不是结构体默认为不可变。而是结构体的方法默认为不可变。可以将其与 C++ 的相反假设进行比较。在 C++ 中,方法默认为非常量 this,如果要使方法具有“常量 this”并允许在常量实例上使用,则必须显式添加 const 关键字(普通方法不能在常量实例上使用)。Swift 也采用了与之相反的默认设置:方法默认具有“常量 this”,如果必须禁止在常量实例上使用,则需要标记为另外一种方式。 - Analog File
3
@golddove 是的,你不能这么做。你不应该这么做。你总是需要创建或派生一个新版本的值,并且你的程序需要有意识地采用这种方式。该特性存在是为了防止这种意外的变异。再次强调,这是函数式编程的核心概念之一。 - eonil
1
@VanDuTran 不行。let 值将永远是不可变的。你只能在初始化器中分配它们一次。 - eonil
显示剩余10条评论

47

注意:以下内容为通俗易懂的描述。

虽然以下解释在代码细节上不是非常准确,但被一位实际从事Swift开发的人审查后认为已足够作为基本解释。

现在我想试着简单明了地回答一个问题:“为什么我们可以在没有修改关键字的情况下更改结构体参数,却需要将结构体函数标记为“mutating”呢?”

大致上来讲,这与保持Swift快速(swift)的理念有很多关系。

你可以把它看作是管理物理地址的问题。当您更改您的地址时,如果有很多人有您当前的地址,您必须通知所有人您已经搬家了。但是,如果没有人知道您的当前地址,您可以随时搬到任何地方,无需通知任何人。

在这种情况下,Swift就像邮局。如果有很多人和很多联系人经常搬家,那么它的开销就会非常高。它必须支付一大批员工来处理所有这些通知,这个过程需要耗费大量时间和精力。这就是为什么Swift的理想状态是让城镇中的每个人都尽可能地少有联系人。那么它就不需要一个大型的员工来处理地址更改,并且可以更快、更好地完成其他所有事情。

这也是为什么Swift中关于值类型和引用类型的问题备受推崇。从本质上讲,引用类型在各个方面都会增加“联系人”的数量,而值类型通常不需要超过几个联系人。因此,值类型更具有“Swift”性质。

回到细节问题:结构体。在Swift中,结构体非常重要,因为它们可以做大部分对象能够做的事情,但它们是值类型。

让我们继续用物理地址的比喻来想象一个住在someObjectVille中的misterStruct。这个比喻有点混乱,但我认为它仍然有帮助。

因此,为了模拟更改struct变量,假设misterStruct有绿色头发,并收到了换成蓝色头发的指令。就像我说的那样,这个比喻有点混乱,但所发生的事情有点像是老人搬出去,一个带着蓝色头发的新人搬进来,这个新人开始称自己为misterStruct。没有人需要收到地址更改通知,但如果有人查看该地址,他们会看到一个有蓝色头发的人。

现在让我们模拟在struct上调用函数时会发生什么。在这种情况下,就像misterStruct收到诸如changeYourHairBlue()之类的指令一样。所以邮局将指示传递给misterStruct,“去把你的头发染成蓝色,告诉我什么时候完成。”

如果他按照以前的相同例程进行,如果他在直接更改变量时所做的事情仍然是一样的话,那么misterStruct将会搬出自己的房子并呼叫一个新的蓝发人。但这是问题。

指令是“去把你的头发染成蓝色,告诉我什么时候完成”,但收到该指令的是绿发的那个家伙。在蓝发家伙搬进来之后,仍然必须发送“作业完成”通知。但蓝发家伙一无所知。

[为了使这个比喻更加混乱,技术上发生的是绿发家伙在搬出去后,他立即自杀了。因此,也不能通知任何人任务已经完成]

为了避免这个问题,在这种情况下,Swift必须直接进入该地址的房屋并实际更改当前居住者的头发。这是一个完全不同的过程,不同于只是派遣一个新人进去。
这就是为什么Swift希望我们使用"mutating"关键字的原因!
对于必须引用结构体的任何内容,最终结果看起来都一样:房屋的居民现在有蓝色的头发。但是实现它的过程实际上是完全不同的。它看起来在做相同的事情,但实际上它正在做一个非常不同的事情。它是一件Swift结构体通常从不会做的事情。
因此,为了帮助可怜的编译器,并不让它自己解决每个结构体函数是否会改变结构体,我们被要求要用"mutating"关键字。
基本上,为了帮助Swift保持迅速,我们所有人都必须尽自己的一份力。 :)

6
这篇文章写得太棒了!非常易懂。我在网上搜索了很久,终于找到了答案(无需查看源代码)。感谢你抽出时间写这篇文章。 - Vaibhav
3
我只是想确认我是否正确理解了:我们可以说,在 Swift 中直接更改结构体实例的属性时,该结构体实例被“终止”,并在幕后创建一个具有更改属性的新实例吗?而当我们使用突变方法在实例内更改该实例的属性时,这个终止和重新初始化过程就不会发生了吗? - Luke
@Teo,我希望我能给出明确的答案,但我只能说我向一位实际的Swift开发者请教过这个类比,他们说“基本上是准确的”,这意味着在技术层面上还有更多内容。但是,在广义上理解——除了这个之外还有更多内容——是的,这个类比就是在表达这个意思。 - Le Mot Juiced
太棒了,伙计! - SteBra

27

结构体是一组字段的聚合; 如果特定结构体实例是可变的,则其字段将是可变的; 如果实例是不可变的,则其字段将是不可变的。因此,结构体类型必须为任何特定实例的字段可能可变或不可变做好准备。

为了使结构体方法能够改变基础结构体的字段,这些字段必须是可变的。如果在不可变结构上调用修改底层结构体字段的方法,它将尝试修改不可变字段。由于这样做没有任何好处,因此需要禁止这种调用。

为了实现这一点,Swift将结构体方法分成两类:修改底层结构的方法只能在可变的结构体实例上调用;不修改底层结构的方法应该可以在可变和不可变的实例上调用。后者的使用可能更频繁,因此是默认的。

相比之下,.NET目前(仍然!)没有区分修改结构体和不修改结构体的结构体方法的方法。相反,在不可变结构实例上调用结构体方法将导致编译器制作一个可变复制品,并让方法对其进行任意操作,最后丢弃该复制品。这会导致编译器不管方法是否修改它都浪费时间复制结构,即使添加复制操作几乎永远不会将语义上不正确的代码转换为语义正确的代码;它只会导致具有一定语义错误的代码以不同的方式出现语义错误(允许代码认为它在修改结构体,但放弃尝试的更改)。允许结构体方法指示它们是否会修改底层结构可以消除无用的复制操作,并确保尝试错误使用时会得到标记。


1
与.NET的比较以及没有变异关键字的示例使我清楚地了解到了问题。弄清为什么变异关键字会带来任何好处。 - Binarian

8

Swift结构体可以通过letvar关键字来实例化。

考虑一下Swift的Array结构体(是的,它是一个结构体)。

var petNames: [String] = ["Ruff", "Garfield", "Nemo"]
petNames.append("Harvey") // ["Ruff", "Garfield", "Nemo", "Harvey"]

let planetNames: [String] = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
planetNames.append("Pluto") //Error, sorry Pluto. No can do

为什么在行星名称方面追加操作无法实现?因为“append”被标记为“mutating”关键字。并且由于使用“let”声明了“planetNames”,故所有标记的方法都无法使用。
在您的示例中,编译器可以告诉您正在修改结构体,方法是在init之外分配一个或多个属性。如果您稍微更改一下代码,您会发现在结构体外并非始终可以访问“x”和“y”。请注意第一行上的“let”。
let p = Point(x: 1, y: 2)
p.x = 3 //error
p.moveToX(5, andY: 5) //error

6
考虑与C ++进行比较。Swift中的结构体方法是否为 mutating /非 mutating 类似于C ++中方法是非 const / const 。在C ++中标记为 const 的方法也不能改变结构。

您可以从结构体外部更改var,但无法从其本身的方法更改它。

在C ++中,您还可以“从结构体外部更改var”-但仅当您拥有非 const 结构体变量时才可以。如果您有一个 const 结构体变量,则无法分配给var,并且您也无法调用非 const 方法。同样,在Swift中,只有当结构体变量不是常量时,才能更改结构体的属性。如果您有一个结构体常量,则无法将属性分配给var,您也无法调用 mutating 方法。

4
我开始学习Swift时也曾经想过同样的问题,虽然这些回答或许提供了一些见解,但它们本身也有点啰嗦和令人困惑。其实我认为回答你的问题非常简单...
在你的结构体内定义的可变方法希望获得修改未来创建的每个实例的权限。如果其中一个实例被分配到一个用let声明的不可变常量中,那就有麻烦了。为了保护你自己(并让编辑器和编译器知道你试图做什么),你必须明确地表达出你想给实例方法这种权力。
相比之下,从结构体外部设置属性是在已知的结构体实例上操作的。如果它被分配给一个常量,Xcode会在你输入方法调用的那一刻告诉你。
这是我越来越喜欢使用Swift的原因之一——在输入时被警告错误。这当然要比排除晦涩难懂的JavaScript错误要好得多!

1

Swift 可变结构体

还有一种变体

struct MyStruct {
    var myVar = "myVar"
    let myLet = "myLet"
}

func testMutateString() {
    //given
    let myStruct = MyStruct()
    
    //Change var
    //when
    var myStructCopy = myStruct
    myStructCopy.myVar = "myVar changed 1"
    
    //then
    XCTAssert(myStructCopy.myVar == "myVar changed 1")
    
    //Change let
    //when
    withUnsafeMutableBytes(of: &myStructCopy) { bytes in
        let offset = MemoryLayout.offset(of: \MyStruct.myLet)!
        let rawPointerToValue = bytes.baseAddress! + offset
        let pointerToValue = rawPointerToValue.assumingMemoryBound(to: String.self)
        pointerToValue.pointee = "myLet changed"
    }
    
    //then
    XCTAssert(myStructCopy.myLet == "myLet changed")
}

[Swift 类型]


0

SWIFT:在结构体中使用可变函数

Swift程序员以这样的方式开发了结构体,使其属性不能在结构体方法内修改。例如,请检查下面给出的代码:

struct City
{
  var population : Int 
  func changePopulation(newpopulation : Int)
  {
      population = newpopulation //error: cannot modify property "popultion"
  }
}
  var mycity = City(population : 1000)
  mycity.changePopulation(newpopulation : 2000)

执行上述代码时,我们会收到一个错误,因为我们试图为结构体 City 的属性 population 分配一个新值。默认情况下,结构体的属性不能在其自己的方法中进行变异。这是苹果开发人员构建它的方式,使得结构体默认具有静态性质。

我们该如何解决它?有什么替代方案吗?

Mutating 关键字:

在结构体内声明函数为 mutating 允许我们修改结构体中的属性。 以上代码的第 5 行将更改为以下内容:

mutating changePopulation(newpopulation : Int)

现在我们可以将newpopulation的值分配给方法范围内的属性population

注意:

let mycity = City(1000)     
mycity.changePopulation(newpopulation : 2000)   //error: cannot modify property "popultion"

如果我们在结构体对象中使用let而不是var,那么我们就无法改变任何属性的值。这也是为什么当我们尝试使用let实例调用可变函数时会出现错误的原因。因此,最好在更改属性值时始终使用var。
期待听到您的评论和想法...

原始问题并非关于如何使用Mutating关键字,而是为什么结构体外部的内容可以轻松更改。 - Manish Nahar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接