10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Swiftではどのcontainsが早いか?(爆速で禁則文字のチェックをしたい!)

Last updated at Posted at 2021-05-25

はじめに

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

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

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

長い文字列だと?

次は、テキストフィールドには収まらなさそうな、長めの文字列で計測してみます。
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が目立って遅いです。
対して、CharacterSetNSRegularExpressionを使用したパターンは同列くらいで速い!

さらに増やしちゃう!

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

/// 禁則文字の含まれる長いめちゃくちゃ文字列
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が活用できそうです!

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

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?