允许给自己分配一个新的结构值的理由是什么?

回答 3 浏览 1253 2023-05-02

我不是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的输出与替代方案相比,看起来效率低得可怕。

Stefan Zobel 提问于2023-05-02
"来自C/C++/Java/C#的背景,我认为自我是一个指针"--这些语言中有两个半没有把自我作为指针。user253751 2023-05-03
虽然this在C++中是一个指针,但它也可以很容易地成为一个引用R.M. 2023-05-03
C语言甚至没有隐含的thisself;我不知道你为什么要提到C语言。Peter Cordes 2023-05-03
3 个回答
#1楼 已采纳
得票数 16

我来自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上调用一个突变器,会用一个新的值替换整个值。

Rob Napier 提问于2023-05-02
Rob Napier 修改于2023-05-02
啊,-O的输出让我感觉好多了。我已经猜到我错过了一个编译器开关。我投了赞成票。Stefan Zobel 2023-05-02
我的观点是,堆栈上的那些字节被复制了,就像在C++中一样。我所说的 "正常的方式 "是指在C++中你通常会避免这种复制的发生(通过使用引用、智能指针,甚至是原始指针)。在Swift中,让复制发生是正常的(而且数据类型的设计使得复制并不昂贵)。我想你是想跳到价值类型的实现和优化上,然后再回到语言的语义上(这就是C++的一般工作方式)。但Swift是从语义开始的(复制的值),并向前推进到编译器的优化。Rob Napier 2023-05-02
很公平,我现在明白你的意思了。然而,self不是该值的copy(如你上面所说),而是该值的representative,对吗?(也就是说,实际上是它的地址?)。Stefan Zobel 2023-05-02
不,它是一个副本。优化器可能会努力避免复制,作为一个实现细节,但从语义上讲,它绝对是一个复制。Swift有很多技术(最著名的是大多数stdlib数据结构中的写时复制)来使这些复制有效。但不,它是一个副本。对于你提出的关于非常大的结构(不包括具有CoW优化的集合类型)的问题,是的,如果你没有考虑周全,这些结构会导致性能问题。Rob Napier 2023-05-02
请记住,Swift 没有像 C 或 C++ 那样的规范性 "语言规范" 。自从Swift进化过程的兴起,你经常可以找到对特定功能的更详细的解释,但它们不是以对规范的修正形式出现的。Swift的大部分内容都是以WWDC视频或Swift论坛讨论的形式详细介绍的。对于Swift所要实现的目标,最著名的解释可能还是"The Crusty Talk":developer.apple.com/videos/play/wwdc2015/408Rob Napier 2023-05-03
#2楼
得票数 5

赋值给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很快就会把它们解决掉

Jessy 提问于2023-05-02
Jessy 修改于2023-05-02
慢慢地,它开始对我有意义了。可能是我正在寻找的解释。我必须研究一下,让它沉淀一下。我投了赞成票。Stefan Zobel 2023-05-02
#3楼
得票数 4

结构实际上并不发生变化。仅仅说myPoint.x = 1.0实际上是替换了该结构。因此,如果一个结构将该结构的新副本替换为self,也没什么大不了的,因为这正是在每次向该结构的属性赋值时所做的。

这就是为什么你不能向由let变量引用的结构实例赋值;这需要向该变量赋值一个结构,而你不能这样做,因为let表示一个常量。

let myPoint = Point()
myPoint.x = 1 // illegal, and now you know why

对比一下类,它可变的地方--这就是为什么你可以赋值到一个由let变量引用的类实例的属性中去。

matt 提问于2023-05-02
"myPoint.x = 1.0实际上取代了该结构"。这总是真的吗?如果结构体真的很大怎么办?当只有一个8字节的成员需要改变时,复制整个结构并替换旧值,这在我看来是非常低效的。尽管如此,我还是投了赞成票。Stefan Zobel 2023-05-02
只是假设结构太大,以至于无法装入一个xmm寄存器?为什么编译器会在没有必要的情况下加载和存储几个?Stefan Zobel 2023-05-02
"仅仅说myPoint.x=1.0实际上就会替换这个结构。"从汇编的角度来看,这个说法似乎并不正确。我在Godbolt上的例子中玩了一下更大的结构,发现只有部分结构被替换。Stefan Zobel 2023-05-02
那是一种优化。从逻辑上讲,整个结构都被替换了。查看汇编输出并不能告诉你太多关于 Swift 是如何工作的;它只是告诉你优化器被允许做什么,这等同于 在这个程序中 Swift 实际是如何工作的。如果在保存该结构的变量或属性上添加 didSet,您会发现每次修改该结构的任何部分时都会调用它,因为整个结构是一个值。Rob Napier 2023-05-02
请看我答案中的链接。特别是"结构和枚举是值类型。"你可能还会发现编译器源码中的各种文档对理解实现的更多技术部分有帮助:github.com/apple/swift/blob/main/docs/Arrays.md github.com/apple/swift/blob/main/docs/proposals/… github.com/apple/swift/blob/main/docs/OwnershipManifesto.md 但是matt的回答是100%正确的。变异结构等同于用该变化制作一个副本,然后将整个副本分配回原始结构,即使优化器简化了它。Rob Napier 2023-05-03
标签