8
1

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 1 year has passed since last update.

RubyAdvent Calendar 2021

Day 18

【初級者向け】RubyのStringとHashのeach系メソッドを体系的に整理

Last updated at Posted at 2021-12-17

最初に

Ruby Advent Calendar 2021 Part2の18日目の記事です。
昨日は著名なigaigaさんの「Ruby3.0キーワード引数仕様変更に伴う書き換えをしたので調べたことのまとめAdd Star」でした!

RubyのアドベントカレンダーのPart2は枠が余ってたし、備忘用に書いたものがあったこともあり、編集して公開してみることにしました。
StringやHashのeach系のメソッドが体系化されて説明された記事を読んだことがないし、他の人も読んだことがなさそうだと思ったので、そういった内容です。
ちょっと表にしたり、ちょっと歴史的な流れを説明したりって感じですけど。

Stringのeach系メソッドについて

配列(Array)やハッシュ(Hash)にはeachというインスタンスメソッドがあります。
対して、今の文字列(String)にはeachメソッドは存在しません。

昔のRuby 1.8時代までは文字列にもeachメソッドが存在しており、
今でいうeach_lineの機能で行ごとに回していたのですが、
文字列のeachは何の単位で回すのか自明ではないためRuby 1.9で削除されました。

今はRuby 3.0が最新ですが、
実際のところ文字列にはeach_で始まるメソッドが5つ存在します。
さらに、これと対応して、(ブロックがなければ)配列を返すメソッドが5つ存在します。

配列を返すメソッド each系メソッド 要素のクラス 要素の単位
chars each_char String (旧式単位の)文字
lines each_line String
bytes each_byte Integer バイト
codepoints each_codepoint Integer コードポイント
grapheme_clusters each_grapheme_cluster String 書記素単位の文字

特にRuby/Railsを書く際に「配列が入る変数名は複数形にする(要素名はその単数形)」という慣習が存在しますが、Ruby本体のメソッド名にも「配列を返すメソッド名は複数形にする」というケースがあることがわかります。
対してeach_始まりのメソッドは単数形になっていて、全体的に英語の文法にちゃんと則っている感じがしますね。

追記:
ところで、ruby-jpでscivolaさんから、この表の要素の単位について、charsもcodepointsも同じ単位のはずなのに違うように見える旨の指摘がありました。確かにそうですね。ここに追記する形で済ませておきます。

簡単な実行例

それぞれのメソッドの実行例を簡単に見てみましょう。

s = "aあ\n💪🏼"
p s.chars             # ["a", "あ", "\n", "💪", "🏼"]
p s.bytes             # [97, 227, 129, 130, 10, 240, 159, 146, 170, 240, 159, 143, 188]
p s.lines             # ["aあ\n", "💪🏼"] 
p s.codepoints        # [97, 12354, 10, 128170, 127996]
p s.grapheme_clusters # ["a", "あ", "\n", "💪🏼"]

この中で、grapheme_clustersは比較的新しいメソッドで、
簡単にいえばcharsの新しい版みたいなもんです。
もともとあったcharsの形式だと、新しく導入された絵文字で不合理なことが起きるケースがあります。
例えば、実行例にある通り、charsだと「うすだいだい色の腕の絵文字💪🏼」が、「腕の絵文字(黄色)」と「うすだいだい色」に分離されてしまうみたいなことが起きるため、そうならないように書記素単位で文字を分離するgrapheme_clustersメソッドがRuby 2.5から導入されています。

linesは、名前のとおり行ごとに区切るメソッドですね。
"\n"のような改行で区切るメソッドです。

bytesは、バイト単位に区切るメソッドです。要素は、Integerです。
"a"は97で、"あ"は3バイトで構成されるマルチバイト文字であり227, 129, 130をに分割されます。

codepointsは、コードポイント単位で区切るメソッドです。要素は、Integerです。
"a"は97で、"あ"は12354となります。

chars等でもブロックをとって回せるが

なお、chars等の複数形のメソッドでも、each系と同じようにブロックをとって回せます。

# ブロック有りの場合は、どちらも同じ動作をする
"abc".each_char{ |c| p c }
"abc".chars{ |c| p c }

ただchars等でブロックをとって回せるのは、知らない人にはわかりにくいはずです。

charsでブロックをとって回せなくしようという議論もあったはずで、
実際chars等でブロックをとって回すのがdeprecatedであるという警告が、Ruby 2.6まではでてました。
しかし、Ruby 2.7で警告はでなくなったので、これから先charsでブロックをとって回せなくなる可能性はかなり低くなったように思います。

依然としてcharsにブロックをつけて文字を回せますが、
ブロックで回すなら業務等ではeach_charで書いた方が良いだろうと思います。

Hashのeach系メソッド

文字列は、eachは存在せず、charslines等でもブロックをとって回すことも出来ました。
一方、Hashには、eachが存在しEnumerableで、keysvaluesはブロックで回せずeach_keyeach_valueでないと回せません。
keysvaluesでブロックで回せないこと自体はいいとして、Stringのchars等はブロックで回せるので、文字列とハッシュで動作が一貫してない点には注意ですね。

Hashのeach始まりのメソッドは下記です。

配列を返すメソッド each系メソッド 要素
to_a each, each_pair キーと値(の配列)
keys each_key キー
values each_value

Hashのeacheach_pairはエイリアスです。
Hashではeachよりもeach_pairを使った方が、Hashであろうという推測が成り立ち可読性が高くなると思うのですが、個人的にはeach_pairを使ったコードを見た覚えがないです。
なお、Stringの流れ的にはあっても変じゃないと思うのですが、pairsというメソッドは存在しないです。

余談1「charsは配列を返していなかった」

もともとRuby 1.9まではStringのcharseach_charとエイリアスで、ブロックがない際はEnumerableのオブジェクトを返していました。しかし、次のyharaさんによる提案から、変更されたようです。

ザッと読む感じ、次のようなコードでto_aが必要な理由を初心者に説明しにくいよねって発端です。

str = "abc"
str.chars.to_a.last
str.chars.to_a[1,3]

余談2「ARGF」

ARGFオブジェクトのchars等は、Ruby 3.0で削除されました。
文字列のcharsと異なり、このARGFcharsは、配列を返したわけではなく、each_charと同じ振る舞いをしてたようです。

参考リンク

String#eachがRuby 1.9で削除された話について、
詳しく知りたい方は下記リンク先などを読むといいかもしれないです。

それで文字とかバイトで使いたいときには、明示的に区切ってから繰り返しなさいと(していた)。言ってみれば「行」が一級市民で、「文字」と「バイト」が二級市民と、扱いに差別があったわけです。差別はよくないので、何に対して繰り返すかを明示的に指定することにしました。

最後に

参考になったら、他の記事も読んでもらえると嬉しいです!
今年は、次の企画アドベントカレンダーの記事もやってみましたので、皆様も挑戦されてみてはいかがでしょう。

8
1
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
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?