`String#split`と`String#chars`の小咄

最近、String#splitString#chars まわりの Cop をメンテナンスしたり実装した際の、それらメソッドに関する知見を書き残しておきます。

String#split の小咄

RuboCop Performance に string.split(/re/) より string.split('re') の方が速いので、Cop にどうかという提案があったのがはじまり。

split の引数が文字列で済む場合は、正規表現ではなく文字列を渡すのは良いとして、正規表現と文字列で振る舞いが異なるものがある。

たとえば split の引数が空白なしの // である場合は '' と結果は同じなので、安全に置換できる。

'a    b    c'.split(//)
#=> ["a", " ", " ", " ", " ", "b", " ", " ", " ", " ", "c"]
'a    b    c'.split('')
#=> ["a", " ", " ", " ", " ", "b", " ", " ", " ", " ", "c"]

しかし split の引数が空白一つの / / である場合は ' ' と結果が異なり、戻り値に互換性がなくなる。

'a    b    c'.split(/ /)
#=> ["a", "", "", "", "b", "", "", "", "c"]
'a    b    c'.split(' ')
#=> ["a", "b", "c"]

なお、空白2つ以上は、互換性があるため安全に置換できる。

'a    b    c'.split(/  /)
#=> ["a", "", "b", "", "c"]
'a    b    c'.split('  ')
#=> ["a", "", "b", "", "c"]

このあたり戻り値の違いへの考慮に関して、つい今しがたリリースした RuboCop Performance 1.10.2Performance/RedundantSplitRegexpArgument の既知の誤検知については修正しておいた。

github.com

String#chars の小咄

たとえば split の引数が空白なしの // である場合は '' と結果は同じなので、安全に置換できると前述した続き。Twitter での @onk のツイート。

Ruby 1.9 以前は戻り値に違いがあった。

% ruby -v
ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-darwin13.0.2]

% ruby -e "p 'foo'.split(//)"
["f", "o", "o"]
% ruby -e "p 'foo'.chars"
#<Enumerator: "foo":chars>

Ruby 2.0 以降は戻り値に差異がなくなったため、String#chars を使った方が明確な名前になると思う。

% ruby -v
ruby 2.0.0p648 (2015-12-16 revision 53162) [x86_64-darwin13.0.2]

% ruby -e "p 'foo'.split(//)"
["f", "o", "o"]
% ruby -e "p 'foo'.chars"
["f", "o", "o"]

こういった歴史的経緯があるとき、アンラーニングしきれず手癖で split の方を使ってしまったりするケースもあると思う。

このあたり次の RuboCop のリリース (たぶん 1.12.0) で、Style/StringChars cop として追加される予定です。