はじめに
例えば、UITextView
やUITextField
で禁則文字チェックをしたい時に、どう実装すると速いのか計測してみた。
TL;DR
チェックする文字列が、
短ければ CharacterSet
!
長ければ NSRegularExpression
!
検証方法
「〜〜に特定の文字が含まれないこと」って要件があった時に思い付くであろう、以下の4パターンで検証を行った。
※検証環境はSwift 5.3.2
、Simulator上のiOS 14.4
にて。
前提として、ASCIIコードの記号類、以下の文字を禁則文字としてチェックする。
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
Arrayでチェック!
変数array
は前述の禁則文字をCharacter
の配列として保持。
let array: [Character] = 禁則文字のString.map({ $0 })
/// Arrayでcontainsするケース
func containsOfArray(_ target: String) -> Bool {
return target.contains(where: { array.contains($0) })
}
Setでチェック!
変数set
も同様に禁則文字を保持。
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
で分解する辺りが異なる。
let characterSet: CharacterSet = CharacterSet(charactersIn: 禁則文字のString)
/// CharacterSetでcontainsするケース
func containsOfCharacterSet(_ target: String) -> Bool {
return target.unicodeScalars.contains(where: { characterSet.contains($0) })
}
NSRegularExpressionでチェック!
条件が異なってしまうが、要するに半角英数のみ許容したいんでしょ?って意訳でチェックします。
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のカウントを取っている。
実行!
以下の文字をチェックします!
/// 禁則文字1文字だけ
let test1 = "<"
以下の要領で、10万回3セットで実行します!(コード最適化オプションはNoneです。)
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対策ですかね?
/// 禁則文字の含まれる長い文字列
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
を使用したパターンは同列くらいで速い!
さらに増やしちゃう!
文章くらいの以下の長さの文字列で検証する。
/// 禁則文字の含まれる長いめちゃくちゃ文字列
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
が活用できそうです!
絵文字の入力チェックや、禁則文字がもっと多いケースでは結果が変わってくると思いますが、参考になればと思います!