<commit_before>---
layout: post
slug: swift-5-string-native-encoding-change
title: Swift 5字符串内部编码变化
date: 2019-04-08 23:59:00
comments: true
categories:
- Programing
tags:
- Swift
- Swift5
- String
- UTF-8
- UTF-16
---

> TLDR：
> 1. 不要假设`String`实例里的编码是UTF-16 / UTF-8
> 2. 不要把`Range`实例和相关联的`String`实例分开使用

**这篇文章假设你已经知道Swift `Range<String.Index>` 对emoji或其他ligature的处理，和Objective-C `NSRange`是不同的！这边文章不是探讨这个问题的。**

下面详细说说来龙去脉，试着先说清楚我们遇到的实际问题，如果你没兴趣了解太多实际问题的上下文，可以直接跳到下文「实验」部分。

## 遇到的bug

遇到一个Crash，我们有一个功能，是在TextView删去一段字符（比如`@Arthur `）的最后一个字符（空格 ` `）的时候，删去整段字符（`@Arthur `），类似微信里你删除一个@的人的名字的时候的体验。

我实现的方式是：

1. 在 `textView(_:shouldChangeTextIn:replacementText:)` 回调中，检查如果用户在尝试删除 `@Arthur` 中那个空格的时候，找到`@Arthur`的Range，把Range记录下来。
2. 之后这个Range会被传来传去，最后在另一个地方拿着这个Range去删除 `textView.attributedText.string` 里的对应字符。当然我可以确保这个过程中 `textView.attributedText.string` 并没有发生变化。

两者都会把Swift `Range<String.Index>`转为`NSRange`，但转的时候基于的String是相同内容的不同实例。以上是前提。

在Swift4.2以及之前的版本里，都没有问题。升级到Swift5之后发生了Crash，表现是步骤 1 和 2 中拿到的 `Range` 并不相同。比如 1 的时候找到的是 `{2, 6}`，到了 2 的操作时候变成了 `{6, 16}`，这都是 `NSRange(range, in: string)` 来转的结果，如前面所说这个string是内容相同的不同实例。

这里真正就是这个不同的实例，1里的string是一个在Swift初始化的 `String` 实例，而2里是`textView.attributedText.string`。

看到这里可能nb的你已经注意到了，前者是一个swift `String`，在5的内部编码（native encoding）被改成了UTF-8；而后者虽然在编译时是一个swift `String`类型，但`textView.attributedText.string`原先是一个 Foundation 的 `NSString`，所以他的内部编码是UTF-16。

Range是在UTF-8上找到的，再应用到UTF-16的字符串上自然是有问题了。（比如ASCII在前者是单个Byte，后者则两个Bytes等）

## 实验

得到这个结论后，写了个例子<commit_msg>fix format<commit_after>---
layout: post
slug: swift-5-string-native-encoding-change
title: Swift 5字符串内部编码变化
date: 2019-04-08 23:59:00
comments: true
categories:
- Programing
tags:
- Swift
- Swift5
- String
- UTF-8
- UTF-16
---

> TLDR：
> 1. 不要假设`String`实例里的编码是UTF-16 / UTF-8
> 2. 不要把`Range`实例和相关联的`String`实例分开使用

**这篇文章假设你已经知道Swift `Range<String.Index>` 对emoji或其他ligature的处理，和Objective-C `NSRange`是不同的！这边文章不是探讨这个问题的。**

下面详细说说来龙去脉，试着先说清楚我们遇到的实际问题，如果你没兴趣了解太多实际问题的上下文，可以直接跳到下文「实验」部分。

## 遇到的bug

遇到一个Crash，我们有一个功能，是在TextView删去一段字符（比如`@Arthur `）的最后一个字符（空格）的时候，删去整段字符（`@Arthur `），类似微信里你删除一个@的人的名字的时候的体验。

我实现的方式是：

1. 在 `textView(_:shouldChangeTextIn:replacementText:)` 回调中，检查如果用户在尝试删除 `@Arthur` 中那个空格的时候，找到`@Arthur`的Range，把Range记录下来。
2. 之后这个Range会被传来传去，最后在另一个地方拿着这个Range去删除 `textView.attributedText.string` 里的对应字符。当然我可以确保这个过程中 `textView.attributedText.string` 并没有发生变化。

两者都会把Swift `Range<String.Index>`转为`NSRange`，但转的时候基于的String是相同内容的不同实例。以上是前提。

在Swift4.2以及之前的版本里，都没有问题。升级到Swift5之后发生了Crash，表现是步骤 1 和 2 中拿到的 `Range` 并不相同。比如 1 的时候找到的是 `{2, 6}`，到了 2 的操作时候变成了 `{6, 16}`，这都是 `NSRange(range, in: string)` 来转的结果，如前面所说这个string是内容相同的不同实例。

这里真正就是这个不同的实例，1里的string是一个在Swift初始化的 `String` 实例，而2里是`textView.attributedText.string`。

看到这里可能nb的你已经注意到了，前者是一个swift `String`，在5的内部编码（native encoding）被改成了UTF-8；而后者虽然在编译时是一个swift `String`类型，但`textView.attributedText.string`原先是一个 Foundation 的 `NSString`，所以他的内部编码是UTF-16。

Range是在UTF-8上找到的，再应用到UTF-16的字符串上自然是有问题了。（比如ASCII在前者是单个Byte，后者则两个Bytes等）

## 实验

得到这个结论后，写了个例子
{'fraction_non_alphanumeric': 0.13172851889000334, 'fraction_numerical': 0.0320962888665998, 'mean_word_length': 8.651612903225807, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7318258', 'n_tokens_mistral': 1988, 'n_tokens_neox': 2053, 'n_words': 167}