【问题标题】:Is there a reason that Swift array assignment is inconsistent (neither a reference nor a deep copy)?Swift数组分配不一致(既不是引用也不是深拷贝)是否有原因?
【发布时间】:2014-07-27 16:12:34
【问题描述】:

我正在阅读文档,并且经常对语言的一些设计决策摇头。但真正让我困惑的是数组是如何处理的。

我冲到操场上试了一下。你也可以试试。所以第一个例子:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

这里ab 都是[1, 42, 3],我可以接受。引用了数组 - 好的!

现在看这个例子:

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

c[1, 2, 3, 42]d[1, 2, 3]。也就是说,d 在上一个示例中看到了变化,但在这个示例中没有看到。文档说这是因为长度改变了。

现在,这个怎么样:

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

e[4, 5, 3],这很酷。有一个多索引替换很好,但是f STILL 没有看到变化,即使长度没有改变。

总而言之,如果您更改 1 个元素,则对数组的常见引用会发生变化,但如果您更改多个元素或追加项目,则会创建副本。

对我来说,这似乎是一个非常糟糕的设计。我这样想对吗?有什么原因我不明白为什么数组应该这样吗?

编辑:数组已更改,现在具有值语义。更理智!

【问题讨论】:

  • 为了记录,我认为这个问题不应该结束。 Swift 是一门新语言,所以在我们学习的过程中会有一段时间会出现这样的问题。我觉得这个问题很有趣,我希望有人能提出令人信服的辩护理由。
  • @Joel 很好,问程序员,Stack Overflow 是针对特定的无主见的编程问题。
  • @bjb568:不过,这不是意见。这个问题应该用事实来回答。如果某个 Swift 开发人员来回答“我们是为 X、Y 和 Z 做的那样”,那么这就是直截了当的事实。您可能不同意 X、Y 和 Z,但如果为 X、Y 和 Z 做出决定,那么这只是语言设计的历史事实。就像我问为什么 std::shared_ptr 没有非原子版本时一样,there was an answer based on fact, not opinion(事实是委员会考虑过但出于各种原因不想要它)。
  • @JasonMArcher:只有最后一段是基于意见(是的,也许应该删除)。问题的实际标题(我将其视为实际问题本身)可以用事实来回答。 数组被设计成按照它们的工作方式工作的原因。
  • 是的,正如 API-Beast 所说,这通常称为“Copy-on-Half-Assed-Language-Design”。

标签: arrays swift


【解决方案1】:

请注意,Xcode beta 3 版本中更改了数组语义和语法 (blog post),因此该问题不再适用。以下答案适用于 beta 2:


这是出于性能原因。基本上,他们尽量避免复制数组(并声称“类似 C 的性能”)。引用语言book

对于数组,仅当您执行可能修改数组长度的操作时才会进行复制。这包括追加、插入或删除项目,或使用范围下标替换数组中的一系列项目。

我同意这有点令人困惑,但至少对它的工作原理有一个清晰简单的描述。

该部分还包括有关如何确保数组被唯一引用、如何强制复制数组以及如何检查两个数组是否共享存储的信息。

【讨论】:

  • 我发现您在设计中既取消分享又复制了一个大红旗。
  • 这是正确的。一位工程师向我描述说,对于语言设计,这是不可取的,他们希望在即将到来的 Swift 更新中“修复”这些问题。用雷达投票。
  • 这就像Linux子进程内存管理中的写时复制(COW),对吧?也许我们可以将其称为长度更改复制(COLA)。我认为这是一个积极的设计。
  • @justhalf 我可以预测会有一群困惑的新手来到 SO 并询问为什么他们的数组是/不共享的(只是以不太清楚的方式)。
  • @justhalf:无论如何,COW 是现代世界的悲观主义,其次,COW 是一种仅用于实现的技术,而这种 COLA 的东西会导致完全随机的共享和取消共享。
【解决方案2】:

From the official documentation of the Swift language:

请注意,当您使用下标语法设置新值时,不会复制数组,因为使用下标语法设置单个值不会改变数组的长度。但是,如果您将新项目附加到数组中,您会修改数组的长度。这会提示 Swift 在您追加新值时创建一个数组的新副本。从今以后,a 是数组的一个单独的、独立的副本.....

阅读本文档中的整个数组的赋值和复制行为部分。您会发现当您替换数组中的一系列项目时,该数组会为所有项目获取自身的副本。

【讨论】:

  • 谢谢。我在我的问题中含糊地提到了该文本。但是我展示了一个示例,其中更改下标范围并没有改变长度并且它仍然被复制。因此,如果您不想要副本,则必须一次更改一个元素。
【解决方案3】:

Xcode 6 beta 3 的行为发生了变化。数组不再是引用类型,并且具有 copy-on-write 机制,这意味着只要您从一个或另一个更改数组的内容变量,数组将被复制,只有一个副本会被改变。


旧答案:

正如其他人指出的那样,Swift 会尽可能避免复制数组,包括每次 changing values for single indexes 时。

如果您想确保数组变量 (!) 是唯一的,即不与另一个变量共享,您可以调用 unshare 方法。这会复制数组,除非它已经只有一个引用。当然,您也可以调用copy 方法,该方法将始终进行复制,但最好使用 unshare 以确保没有其他变量保存在同一个数组中。

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]

【讨论】:

  • 嗯,对我来说,unshare() 方法是未定义的。
  • @Hlung 它已在 beta 3 中被删除,我已经更新了我的答案。
【解决方案4】:

该行为与 .NET 中的 Array.Resize 方法极为相似。要了解发生了什么,查看 C、C++、Java、C# 和 Swift 中 . 令牌的历史可能会有所帮助。

在 C 中,结构只不过是变量的集合。将. 应用于结构类型的变量将访问存储在结构中的变量。指向对象的指针不持有变量的聚合,而是识别它们。如果有一个标识结构的指针,则-> 运算符可用于访问存储在该指针标识的结构中的变量。

在 C++ 中,结构和类不仅可以聚合变量,还可以将代码附加到它们。使用. 调用方法将对变量要求作用于变量本身的内容;在标识对象的变量上使用-> 将要求该方法对变量标识的对象进行操作。

在 Java 中,所有自定义变量类型都只是简单地标识对象,并且对变量调用方法将告诉方法该变量标识的对象是什么。变量不能直接保存任何类型的复合数据类型,也没有任何方法可以访问调用它的变量。这些限制虽然在语义上有所限制,但极大地简化了运行时,并促进了字节码验证;在市场对此类问题很敏感的时候,这种简化减少了 Java 的资源开销,从而帮助它在市场上获得了吸引力。它们还意味着不需要与 C 或 C++ 中使用的 . 等效的令牌。尽管 Java 可以像 C 和 C++ 一样使用 ->,但创建者选择使用单字符 .,因为它不需要用于任何其他目的。

在 C# 和其他 .NET 语言中,变量可以识别对象或直接保存复合数据类型。当用于复合数据类型的变量时,. 作用于变量的内容;当用于引用类型的变量时,. 作用于由它标识的对象。对于某些类型的操作,语义区别并不是特别重要,但对于其他类型的操作却很重要。最有问题的情况是在只读变量上调用复合数据类型的方法,该方法将修改调用它的变量。如果尝试对只读值或变量调用方法,编译器通常会复制该变量,让该方法对其进行操作,然后丢弃该变量。这对于仅读取变量的方法通常是安全的,但对于写入变量的方法则不安全。不幸的是,.does 还没有任何方法来指示哪些方法可以安全地用于这种替换,哪些不能。

在 Swift 中,聚合上的方法可以明确指出它们是否会修改调用它们的变量,并且编译器将禁止对只读变量使用变异方法(而不是让它们变异变量的临时副本然后将被丢弃)。由于这种区别,在 Swift 中使用 . 标记来调用修改调用它们的变量的方法比在 .NET 中安全得多。不幸的是,相同的. 令牌用于该目的以作用于由变量标识的外部对象这一事实意味着仍然存在混淆的可能性。

如果有一台时间机器并回到 C# 和/或 Swift 的创建过程中,则可以通过让语言以更接近的方式使用 .-> 标记来追溯避免围绕此类问题的大部分混淆到 C++ 用法。聚合类型和引用类型的方法都可以使用. 作用于调用它们的变量,并使用-> 作用于一个(对于复合)或由此确定的事物(对于引用类型)。然而,这两种语言都不是这样设计的。

在 C# 中,方法修改调用它的变量的常规做法是将变量作为 ref 参数传递给方法。因此,当someArray 标识一个包含20 个元素的数组时调用Array.Resize(ref someArray, 23); 将导致someArray 标识一个包含23 个元素的新数组,而不会影响原始数组。 ref 的使用清楚地表明该方法应该修改调用它的变量。在许多情况下,能够修改变量而不必使用静态方法是有利的; Swift 通过使用. 语法解决了这个问题。缺点是不清楚哪些方法作用于变量,哪些方法作用于值。

【讨论】:

    【解决方案5】:

    对我来说,如果您首先将常量替换为变量,这会更有意义:

    a[i] = 42            // (1)
    e[i..j] = [4, 5]     // (2)
    

    第一行永远不需要更改a 的大小。特别是,它永远不需要进行任何内存分配。不管i的值如何,这是一个轻量级的操作。如果你想象a 的底层是一个指针,它可以是一个常量指针。

    第二行可能要复杂得多。根据ij 的值,您可能需要进行内存管理。如果你想象e是一个指向数组内容的指针,你就不能再假设它是一个常量指针;您可能需要分配新的内存块,将数据从旧内存块复制到新内存块,并更改指针。

    似乎语言设计者试图使 (1) 尽可能轻量级。由于 (2) 无论如何都可能涉及复制,因此他们采用的解决方案始终就像您复制了一样。

    这很复杂,但我很高兴他们没有使用例如特殊情况如“如果(2)中的i和j是编译时常量,编译器可以推断出e的大小不会改变,那么我们就不复制”。 p>


    最后,基于我对 Swift 语言设计原则的理解,我认为一般规则如下:

    • 默认情况下始终使用常量 (let),不会有任何重大意外。
    • 仅在绝对必要时才使用变量 (var),并且在这些情况下要小心谨慎,因为会出现意外情况 [此处:在某些但并非所有情况下奇怪的数组隐式副本]。

    【讨论】:

      【解决方案6】:

      我发现:数组将是引用数组的可变副本当且仅当操作有可能改变数组的长度。在您的最后一个示例中,f[0..2] 索引很多,该操作有可能更改其长度(可能是不允许重复),所以它被复制了。

      var e = [1, 2, 3]
      var f = e
      e[0..2] = [4, 5]
      e // 4,5,3
      f // 1,2,3
      
      
      var e1 = [1, 2, 3]
      var f1 = e1
      
      e1[0] = 4
      e1[1] = 5
      
      e1 //  - 4,5,3
      f1 // - 4,5,3
      

      【讨论】:

      • "视为长度已更改" 我可以理解,如果长度更改,它将被复制,但结合上面的引用,我认为这是一个非常令人担忧的“功能”,并且我想很多人会弄错
      • 一门语言是新的并不意味着它可以包含明显的内部矛盾。
      • 这已在 beta 3 中“修复”,var 数组现在完全可变,let 数组完全不可变。
      【解决方案7】:

      Delphi 的字符串和数组具有完全相同的“特性”。当您查看实现时,它是有道理的。

      每个变量都是一个指向动态内存的指针。该内存包含一个引用计数,后跟数组中的数据。因此,您可以轻松更改数组中的值,而无需复制整个数组或更改任何指针。如果要调整数组大小,则必须分配更多内存。在这种情况下,当前变量将指向新分配的内存。但是您无法轻松追踪指向原始数组的所有其他变量,因此请不要理会它们。

      当然,做出更一致的实现并不难。如果您希望所有变量都看到调整大小,请执行以下操作: 每个变量都是指向存储在动态内存中的容器的指针。容器恰好包含两件事,一个引用计数和指向实际数组数据的指针。数组数据存储在单独的动态内存块中。现在只有一个指向数组数据的指针,因此您可以轻松调整它的大小,所有变量都会看到变化。

      【讨论】:

        【解决方案8】:

        许多 Swift 早期采用者抱怨这种容易出错的数组语义,Chris Lattner 写道,数组语义已经过修改以提供完整的值语义 (Apple Developer link for those who have an account)。我们至少要等到下一个测试版才能看到这到底意味着什么。

        【讨论】:

        • 从 iOS 8/Xcode 6 Beta 3 附带的 SDK 开始,新的 Array 行为现在可用。
        【解决方案9】:

        我为此使用 .copy()。

            var a = [1, 2, 3]
            var b = a.copy()
             a[1] = 42 
        

        【讨论】:

        • 当我运行您的代码时,我得到“'[Int]' 类型的值没有成员 'copy'”
        【解决方案10】:

        在后来的 Swift 版本中,数组行为有什么变化吗?我只是运行你的例子:

        var a = [1, 2, 3]
        var b = a
        a[1] = 42
        a
        b
        

        我的结果是 [1, 42, 3] 和 [1, 2, 3]

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-02-26
          • 2016-09-27
          • 1970-01-01
          • 1970-01-01
          • 2011-03-22
          • 2013-03-09
          • 1970-01-01
          • 2012-01-30
          相关资源
          最近更新 更多