Swiftではどのcontainsが早いか？（爆速で禁則文字のチェックをしたい！）

# はじめに

例えば、`UITextView`や`UITextField`で禁則文字チェックをしたい時に、どう実装すると速いのか計測してみた。

# TL;DR

チェックする文字列が、
**短ければ `CharacterSet`！**
**長ければ `NSRegularExpression`！**

# 検証方法

「〜〜に特定の文字が含まれないこと」って要件があった時に思い付くであろう、以下の４パターンで検証を行った。
*※検証環境は`Swift 5.3.2`、Simulator上の`iOS 14.4`にて。*

前提として、ASCIIコードの記号類、以下の文字を禁則文字としてチェックする。

```
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
```

## Arrayでチェック！

変数`array`は前述の禁則文字を`Character`の配列として保持。

```swift
let array: [Character] = 禁則文字のString.map({ $0 })

/// Arrayでcontainsするケース
func containsOfArray(_ target: String) -> Bool {
    return target.contains(where: { array.contains($0) })
}
```

## Setでチェック！

変数`set`も同様に禁則文字を保持。

```swift
let set: Set<Character> = Set(禁則文字のString.map({ $0 }))

/// Setでcontainsするケース
func containsOfSet(_ target: String) -> Bool {
    return target.contains(where: { set.contains($0) })
}

```

## CharacterSetでチェック！

変数`characterSet`も同様だが、`unicodeScalars`で分解する辺りが異なる。

```swift
let characterSet: CharacterSet = CharacterSet(charactersIn: 禁則文字のString)

/// CharacterSetでcontainsするケース
func containsOfCharacterSet(_ target: String) -> Bool {
    return target.unicodeScalars.contains(where: { characterSet.contains($0) })
}
```

## NSRegularExpressionでチェック！

条件が異なってしまうが、要するに半角英数のみ許容したいんでしょ？って意訳でチェックします。

```swift
let regularExpression: NSRegularExpression = try! NSRegularExpression(pattern: "[^0-9a-zA-Z]")

/// firstMatchの有無チェックするケース
func containsOfRegularExpression(_ target: String) -> Bool {
    let range = NSMakeRange(.zero, target.utf16.count)
    return regularExpression.firstMatch(in: target, range: range) != nil
}
```

**NS系ではStringはUTF-16で統一されているため、NSMakeRangeでもUTF-16のカウントを取っている。**

## 実行！

以下の文字をチェックします！

```swift
/// 禁則文字1文字だけ
let test1 = "<"
```

以下の要領で、**10万回3セット**で実行します！（コード最適化オプションはNoneです。）

```swift
var start = Date()
for _ in 1...100000 {
    _ = containsOfArray(test1)
}
print(Date().timeIntervalSince(start))
```

実行結果は以下の通り。

```
containsOfArray
0.5713109970092773
0.5437520742416382
0.5676651000976562

containsOfSet
0.07195603847503662
0.06907808780670166
0.06997907161712646

containsOfCharacterSet
0.06921708583831787
0.06789004802703857
0.06551504135131836

containsOfRegularExpression
0.2778419256210327
0.2965569496154785
0.27724194526672363
```

真っ先に思い付きそうな、`NSRegularExpression`と`Array`を使用したパターンが遅いです。
逆に、あまり使ったことがない、`Set`、`CharacterSet`が同列くらいで速い！

## 長い文字列だと？

次は、テキストフィールドには収まらなさそうな、長めの文字列で計測してみます。
*XSS対策ですかね？*

```swift
/// 禁則文字の含まれる長い文字列
let test2 = "TestTextTestTextTestTextTestTextTestTextTestText<script>console.log('TestText')</script>"
```

実行結果です。

```
containsOfArray
39.32771301269531
39.11132490634918
39.04578995704651

containsOfSet
0.5995030403137207
0.6084669828414917
0.5990009307861328

containsOfCharacterSet
0.31386303901672363
0.31512701511383057
0.3245450258255005

containsOfRegularExpression
0.34255504608154297
0.3371340036392212
0.34821200370788574
```

`Array`が目立って遅いです。
対して、`CharacterSet`、`NSRegularExpression`を使用したパターンは同列くらいで速い！

## さらに増やしちゃう！

文章くらいの以下の長さの文字列で検証する。

```swift
/// 禁則文字の含まれる長いめちゃくちゃ文字列
let test2 = "TestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestTextTestText<script>console.log('TestText')</script>"

```

結果は、

```
containsOfArray
終わらないのでスキップ

containsOfSet
14.058897972106934
13.955461025238037
13.425432920455933

containsOfCharacterSet
4.884488105773926
4.920299053192139
4.912343978881836

containsOfRegularExpression
0.6589760780334473
0.6670709848403931
0.6521110534667969
```

やはり、文字列の検証は正規表現を使うと優秀ですね！

# 結論

基本的に、特定の文字が含まれるか？をチェックするのは、正規表現`NSRegularExpression`を使えば良さそうです。
実装の素直さや分かりやすさって意味でも、恩恵が受けられそうです！（NSじゃない版が欲しいっすね。）

また、`textView(_:shouldChangeTextIn:replacementString:)`で入力チェックを行う場合など、反応の良さを求められる局面では、`CharacterSet`を使う手もありだな、と言うのが今回の検証で分かりました。

ちなみに`Array`を使ったケースが遅いのは、順序不動かどうかによるものだと思います。
ですので、文字チェックを行う以外でも、何かしら列挙したものに一致するかどうか？を評価する際には、安定して速めな`Set`が活用できそうです！

絵文字の入力チェックや、禁則文字がもっと多いケースでは結果が変わってくると思いますが、参考になればと思います！

https://github.com/shion-skgw/ContainsPerformanceTest