String.Index is not valid after mutating its associated string

Here is a simple function that replaces "Hello" in "Hello,My friend" with helloReplacement and prints the character that is expected to be 'i'. To do so, the index that points to 'i' in the original string is calculated and adjusted accordingly after replacement. When helloReplacement contains an emoji, the function doesn't work as expected.

Does anyone know the reason? I could not get my head around it. It seems after mutating a string, Index obtained before mutation is not valid anymore. Is it true? If yes, what is happening under the hood that leads to this result?

/// Replace "Hello" with `helloReplacement` and print the character expected to be 'i'
func replaceHello(with helloReplacement: String) {
    var str = "Hello,My friend"
    let offset  = helloReplacement.utf16.count - "Hello".utf16.count
    let i_index = str.utf16.index(str.utf16.startIndex, offsetBy: 11)
    let o_index = str.utf16.index(str.startIndex, offsetBy: 5)

    str.replaceSubrange(str.startIndex ..< o_index, with: helloReplacement)
    let i_new_index = str.utf16.index(i_index, offsetBy: offset)

    print(str[i_new_index])
}

replaceHello(with: "Hi")
replaceHello(with: "๐Ÿ™‹๐Ÿปโ€โ™‚๏ธ")
replaceHello(with: "Goodbye")
replaceHello(with: "ุณู„ุงู…")
replaceHello(with: "๐Ÿ˜€")

/* Output:
i
,
i
y
f
*/

Tested on Swift4.2 and 5.1

In general, Swift collection indexes are not valid after mutation of the source collection. That they sometimes work (especially for Array, which uses Int for the index type) leads to this misunderstanding.

3 Likes

See the last paragraph Accessing Individual Element section on Collection Documentation.

Saved indices may become invalid as a result of mutating operations. For more information about index invalidation in mutable collections, see the reference for the MutableCollection and RangeReplaceableCollection protocols, as well as for the specific type youโ€™re using.

As to โ€œwhyโ€, well, itโ€™s probably some optimization for fast traversal.

1 Like