【问题标题】:Are Swift "mutable" strings really mutable, or are they just like Java strings?Swift“可变”字符串真的是可变的,还是它们就像 Java 字符串一样?
【发布时间】:2014-08-04 23:38:36
【问题描述】:

Swift 编程语言中,在字符串部分的字符串可变性小节中,它是这样说的:

您可以通过将特定String 分配给变量(在这种情况下可以修改)或常量(在这种情况下它不能修改):

并给出示例代码:

var variableString = "Horse"
variableString += " and carriage"
// variableString is now "Horse and carriage"

let constantString = "Highlander"
constantString += " and another Highlander"
// this reports a compile-time error - a constant string cannot be modified”

iBooks 中的图书here,或网络浏览器中的here

在下一段中,它声称“字符串是值类型”。

我的问题:对我来说,这看起来不像是一个可变字符串。它看起来就像我在 Java(或 C#、Python 等)中所习惯的:具有可变变量绑定的不可变字符串对象。换句话说,有一个对象“马”,然后它创建了一个新的字符串对象“马和马车”,并将其设置为相同的变量。而且由于无法区分对不可变对象的引用与值类型之间的区别(对吗?),我想知道:他们为什么要这样描述它?这些 Swift 字符串和它在 Java 中的方式有​​什么区别吗? (或 C#、Python、Objective-C/NSString)

【问题讨论】:

  • 他们的描述不正确。他们真正谈论的是Java中的最终变量与非最终变量。这里没有字符串可变性。
  • Apple 文档还说:“在幕后,Swift 的编译器优化了字符串的使用,因此只有在绝对必要时才会进行实际的复制。这意味着当使用字符串作为值类型时,您总是可以获得出色的性能”。对我来说,编译器似乎在后台决定是否将其复制取决于您使用字符串的方式。
  • 如果您准确地定义了“可变”和“不可变”的含义,将会很有帮助。我理解它的方式不是(常量或变量的)名称或不可变的值 - 它是它们之间的匹配。调用可变的只是意味着变量名称可以在初始设置后与新值匹配。调用不可变的东西只是意味着常量名称在最初设置后无法与新值匹配。也许我误解了什么,但这就是我理解可变性的方式。
  • 好问题.. 以及非常草率的 Swift 文档措辞。它混合了 objects 的可变性(可以观察到)和 bindings
  • 我认为this screenshot from a Playground 说了很多关于 Swift 中“变异”字符串的各种方式的情况。

标签: string swift immutability


【解决方案1】:

在某种意义上,“可变”和“不可变”只有在谈论引用类型时才有意义。如果您尝试将其扩展到值类型,那么所有值类型都可以被认为在功能上等同于“不可变”引用类型。

例如,考虑Int 类型的var。这是可变的吗?你们中的一些人可能会说,当然——您可以通过分配 (=) 来更改它的可见“值”。但是,对于 NSNumberNSString 中的 var 也可以这样说——您可以通过分配给它来更改其可见值。但是NSNumberNSString 被描述为不可变 类。

引用类型真正发生的事情是,分配给它们会导致变量(指针)指向一个新对象。旧对象和新对象本身都没有“改变”,但由于它指向不同的对象,因此您“看到”了一个新值。

当我们说一个类是“可变的”时,我们的意思是它提供了一个 API(方法或引用)来实际更改对象的内容。但是我们怎么知道对象已经改变了呢? (而不是一个新对象?)这是因为我们可以对同一个对象有另一个引用,并且通过一个引用对对象的更改可以通过另一个引用看到。但是这些属性(指向不同的对象,有多个指向同一个对象的指针)本质上只适用于引用类型。根据定义,值类型不能有这样的“共享”(除非“值”的一部分是引用类型,如Array),因此,值类型不会发生“可变性”的后果。

因此,如果您创建一个包装整数的不可变类,它在操作上将等同于 Int——在这两种情况下,更改变量值的唯一方法是将 (=) 分配给它.所以Int 也应该同样被认为是“不可变的”。

Swift 中的值类型稍微复杂一些,因为它们可以有方法,其中一些可以是mutating。所以如果你可以在一个值类型上调用mutating 方法,它是可变的吗?但是,如果我们考虑在值类型上调用 mutating 方法作为为其分配全新值的语法糖(无论该方法将其更改为什么),我们就可以克服这个问题。

【讨论】:

    【解决方案2】:

    在 Swift 中,Structures and Enumerations Are Value Types:

    事实上,Swift 中的所有基本类型——整数、浮点数、布尔值、字符串、数组和字典——都是值类型,并且在幕后作为结构实现。

    因此,字符串是一种值类型,它在赋值时被复制并且不能有多个引用,但它的底层字符数据存储在一个可共享的、写时复制的缓冲区中。 API reference for the String struct 说:

    虽然 Swift 中的字符串具有值语义,但字符串使用写时复制策略将其数据存储在缓冲区中。然后,该缓冲区可以由字符串的不同副本共享。当多个字符串实例使用同一个缓冲区时,字符串的数据只会在突变时延迟复制。因此,任何变异操作序列中的第一个可能会花费 O(n) 时间和空间。

    确实,varlet 声明了一个可变与不可变绑定到一个看起来不可变的字符缓冲区。

    var v1 = "Hi"      // mutable
    var v2 = v1        // assign/pass by value, i.e. copies the String struct
    v1.append("!")     // mutate v1; does not mutate v2
    [v1, v2]           // ["Hi!", "Hi"]
    
    let c1 = v1        // immutable
    var v3 = c1        // a mutable copy
    // c1.append("!")  // compile error: "Cannot use mutating member on immutable value: 'c1' is a 'let' constant"
    v3 += "gh"         // mutates v3, allocating a new character buffer if needed
    v3.append("?")     // mutates v3, allocating a new character buffer if needed
    [c1, v3]           // ["Hi", "High?"]
    

    这就像 non-final vs. final Java String 变量有两个皱纹。

    1. 使用可变绑定,您可以调用变异方法。由于值类型不能有任何别名,因此除了性能影响之外,您无法判断变异方法是否实际修改了字符缓冲区或重新分配给 String 变量。
    2. 当字符缓冲区仅由一个 String 实例使用时,该实现可以通过在适当的位置更改字符缓冲区来优化更改操作。

    【讨论】:

    • 这篇文章的底部是唯一/最相关的部分:extend 不会改变原始对象。而是a mutating function变异函数实际上与绑定的隐式/就地重新分配相同(这就是为什么重新分配必须允许它工作),但允许函数/方法在修改原始对象时表现出来。这是原始接收器的“输出”(有点魔力)。他们不。您可以通过查看对象的地址来验证这一点 - 它会发生变化。
    • @user2864740 因此调用静音函数会悄悄地分配给变量。事实上,测试最近在字符串上添加的unsafeAddressOf() 非常适合。 unsafeAddressOf() 不适用于像博客文章的 Rectangle 示例这样的结构。你有这方面的权威来源吗?字符串是否实现为拥有堆节点的结构?
    • @Jerry101 String 确实是作为结构实现的。你所要做的就是检查来源,杰瑞。
    • 谢谢@nhgrif!上面的答案是在 Apple 开源 Swift 之前发布的,甚至已经很好地记录了它。从那以后我就没有研究过 Swift。他们是否澄清了有关“值类型”的文档?他们是否记录了发生内存分配/释放的时间?
    • 你几乎总是能够命令点击从类型名称至少到公共界面。 i.stack.imgur.com/YwEyW.png
    【解决方案3】:

    实际上,Swift 的字符串看起来就像 Objective-C (immutable) NSString;我在您链接到的文档中找到了这个 -

    Swift 的 String 类型无缝连接到 Foundation 的 NSString 类。如果你在 Cocoa 或 Cocoa Touch 中使用 Foundation 框架,除了本章描述的 String 特性之外,整个 NSString API 都可以调用你创建的任何 String 值。您还可以将 String 值与任何需要 NSString 实例的 API 一起使用。

    【讨论】:

    • 对。而且 NSString 是不可变的。
    • @SteveWaddicor 对,而 NSMutableString 不是。
    • 几页后,另一个注释说它与NSString 不同,因为它是一个值类型。
    • @RobN 只是因为它在传递给任何函数之前被复制了。
    【解决方案4】:

    Swift 字符串是值,而不是对象。当你改变一个值时,它就变成了一个不同的值。所以在第一种情况下,使用var,您只是将新值分配给同一个变量。而let 保证在分配后不会有任何其他值,因此会产生编译器错误。

    所以回答您的问题,Swift 字符串的处理方式与 Java 中的几乎相同,但被视为值而不是对象。

    【讨论】:

    • 你同意称它们为“可变字符串”很奇怪吗?我的意思是我不认为 Int 是可变的。它可以绑定到var,但我认为这是一个可变绑定,而不是可变 Int。也许这是错误的,或者不是每个人都这么想的。
    • 是的,他们这样描述它绝对是奇怪的,尤其是当下一段更深入地探讨不变性时,解释了一个字符串在分配给另一个变量时会被复制。示例中唯一可变的是var
    • @RobN 你能看到 Int 数组是可变的吗?我们可以就地修改数组,而无需进行内存分配工作。 Swift 数组是值对象,这意味着赋值复制整个数组,并且没有办法对同一个数组进行两个引用。但同样,它们可以是可变的或不可变的,具体取决于变量声明。我在我的回答中添加了一个附录。
    • @Jerry101 是的,我可以。幸运的是,从去年夏天开始,Swift 的可变/不可变概念就已经澄清了。那时的数组不同。我的困惑是关于“可变”的不同含义,在值与引用语义方面。
    【解决方案5】:

    首先,您正在使用创建新值实例的不可变方法。您需要使用mutatingextend 方法来执行变异语义中的操作。

    您在这里所做的是创建一个新的不可变字符串并将其绑定到现有名称。

    var variableString = "Horse"
    variableString += " and carriage"
    

    这是在没有任何额外名称绑定的情况下就地改变字符串。

    var variableString = "Horse"
    variableString.extend(" and carriage")
    

    其次,不可变/可变分离的目的是提供更简单、更安全的编程模型。您可以安全地对不可变数据结构做出更多假设,并且可以消除许多令人头疼的情况。这有助于优化。如果没有不可变类型,则在将数据传递给函数时需要复制整个数据。否则,原始数据可能会被函数变异,这种影响是不可预测的。那么这样的函数需要像“这个函数不会修改传入的数据”这样的注释。使用不可变类型,您可以安全地假设该函数无法修改数据,那么您不必复制它。默认情况下,Swift 会自动隐式执行此操作。

    是的,实际上可变/不可变的区别只不过是 Swift 等高级语言中接口的区别。值语义只是意味着它不支持身份比较。由于许多细节被抽象出来,内部实现可以是任何东西。 Swift 代码注释澄清了字符串正在使用 COW 技巧,然后我相信这两个不可变/可变接口实际上都映射到了大部分不可变的实现。我相信无论界面选择如何,您都会得到几乎相同的结果。但是,这仍然提供了我提到的不可变数据类型的好处。

    然后,代码示例实际上做了同样的事情。唯一的区别是您不能改变以不可变模式绑定到其名称的原始

    【讨论】:

      【解决方案6】:

      回答原来的问题。 Swift 中的字符串与 Java 或 C# 中的字符串不同(我不了解 Python)。它们在两个方面有所不同。

      1) Java 和 C# 中的字符串是引用类型。 Swift(和 C++)中的字符串是值类型。

      2) Java 和 C# 中的字符串始终是不可变的,即使引用未声明为 final 或 readonly。 Swift(和 C++)中的字符串可以是可变的或不可变的,具体取决于它们的声明方式(Swift 中的 let 与 var)。

      当您在 Java 中声明 final String str = "foo" 时,您正在声明对不可变 String 对象的不可变引用。 String str = "foo" 声明对不可变 String 对象的可变引用。

      当你在 Swift 中声明 let str = "foo"时,你声明的是一个不可变的字符串。 var str = "foo" 声明一个可变字符串。

      【讨论】:

        猜你喜欢
        • 2021-04-09
        • 1970-01-01
        • 2022-06-28
        • 2013-05-08
        • 1970-01-01
        • 2014-06-29
        • 1970-01-01
        • 2020-08-18
        • 2023-03-14
        相关资源
        最近更新 更多