允许给自己分配一个新的结构值的理由是什么?
我不是Swift的程序员,所以我对这门语言毫无头绪。我遇到一个语言结构,在一个变异函数中的self
会被分配一个新的值,就像下面例子中的函数moveByAssigmentToSelf
。
struct Point {
var x = 0.0
var y = 0.0
mutating func moveByAssigmentToSelf(_ deltaX: Double, _ deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
mutating func moveByMutatingMembers(_ deltaX: Double, _ deltaY: Double) {
self.x += deltaX
self.y += deltaY
}
}
来自C/C++/Java/C#背景的我认为self
是指向内存中某处结构值(开始)的指针(地址)(例如在堆栈上)。 对我来说,在C++结构中分配给应该相当于this
的东西看起来非常奇怪。据我所知,函数moveByMutatingMembers
应该具有观察上的等效性,而且在C++/Java世界中是很自然的事情。
谁能向一个非Swift的程序员解释一下这个概念背后的原理/想法是什么?
我在语言参考(关于表达式的一章)中只能找到以下模糊的陈述:“在初始化程序、下标或实例方法中,self 指的是它出现的类型的当前实例。” 和 “在值类型的变异方法中,您可以将该值类型的新实例分配给 self。”
我想了解的是,为什么这个作业是个好主意,与传统的解决方案相比,它能解决哪些编程问题?
换句话说:从语言设计的角度来看,为什么这样做有意义?或者说:如果你不得不用C++/Java的方式来做,你会失去什么?
BTW,出于好奇,我看了一下这个例子的Godbolt disassembly,对于我这个未经训练的眼睛来说,moveByAssigmentToSelf
的输出与替代方案相比,看起来效率低得可怕。
this
或self
;我不知道你为什么要提到C语言。
- Peter Cordes 2023-05-03
我来自C/C++/Java/C#的背景,我认为自己是一个指针。
这是不正确的。因为这个类型是一个值类型(一个结构),self
是这个值的一个副本。mutating
的力量在于,它将在函数结束时,用新的值替换之前的值。值没有"实例"。它们只是值。
关于Swift中值类型的有用介绍,请看值和引用类型。同样有用的还有Swift主文档中的结构和枚举是值类型。
Swift的结构有点像C++,如果你抛开对new
、指针和引用的成见,只把结构看作是结构的话。它们作为值(副本)被传递,就像C++的结构一样。只是在 Swift 中,这是做事情的正常方式,不像在 C++ 中,事情通常是通过引用传递的。
对于你关于效率的问题,那只是因为你没有打开优化功能。有了优化,它们的代码简直是一样的,而且它们内联了对init
的调用:
output.Point.moveByAssigmentToSelf(Swift.Double, Swift.Double) -> ():
movupd xmm2, xmmword ptr [r13]
unpcklpd xmm0, xmm1
addpd xmm0, xmm2
movupd xmmword ptr [r13], xmm0
ret
output.Point.moveByMutatingMembers(Swift.Double, Swift.Double) -> ():
jmp (output.Point.moveByAssigmentToSelf(Swift.Double, Swift.Double) -> ())
看看你向@matt提出的问题,我希望这也能帮助你更好地理解这个系统:
var p: Point = Point(x: 1, y: 2) {
didSet { print("new Point: \(p)")}
}
p.moveByMutatingMembers(1, 2)
这只打印了"new Point"一次,因为p
只被替换(设置)了一次。
从另一方面来说:
p.x = 0
p.y = 1
这将打印"new point"两次,因为p
被替换了两次。在Point上调用一个突变器,会用一个新的值替换整个值。
-O
的输出让我感觉好多了。我已经猜到我错过了一个编译器开关。我投了赞成票。
- Stefan Zobel 2023-05-02
self
不是该值的copy
(如你上面所说),而是该值的representative
,对吗?(也就是说,实际上是它的地址?)。
- Stefan Zobel 2023-05-02
赋值给self
有几个原因是有用的,但你的例子没有说明任何原因。虽然用+
和+=
运算符来拼写会更好,但在你的例子中,只有当另一个方法已经存在,对一个新的值执行工作时,赋值给self
才有意义。
func moved(_ deltaX: Double, _ deltaY: Double) -> Self {
.init(x: x + deltaX, y: y + deltaY)
}
mutating func move(_ deltaX: Double, _ deltaY: Double) {
self = moved(deltaX, deltaY)
}
非突变和突变对的另一个选项是将工作切换到突变的变体上:
func moved(_ deltaX: Double, _ deltaY: Double) -> Self {
var point = self
point.move(deltaX, deltaY)
return point
}
mutating func move(_ deltaX: Double, _ deltaY: Double) {
x += deltaX
y += deltaY
}
请看union
,这是标准库中的一个实例。你对性能的担心不是没有根据的,但你在那里找到的__consuming
很快就会把它们解决掉。
结构实际上并不发生变化。仅仅说myPoint.x = 1.0
实际上是替换了该结构。因此,如果一个结构将该结构的新副本替换为self
,也没什么大不了的,因为这正是你在每次向该结构的属性赋值时所做的。
这就是为什么你不能向由let
变量引用的结构实例赋值;这需要向该变量赋值一个新结构,而你不能这样做,因为let
表示一个常量。
let myPoint = Point()
myPoint.x = 1 // illegal, and now you know why
对比一下类,它是可变的地方--这就是为什么你可以赋值到一个由let
变量引用的类实例的属性中去。
didSet
,您会发现每次修改该结构的任何部分时都会调用它,因为整个结构是一个值。
- Rob Napier 2023-05-02