お題
何かのデータファイルから情報を抜き出してテキストファイルに書き出すというお仕事です。
データファイルはテキストファイルで,どの行も先頭が A
から Z
までのいずれかの文字になっています。
こんなふうに:
H 9032,4232
G PXPP
A 123:A-G3
L FO,WE
C 1..9X
G LOPX
この中から,A の行だけを(順序を変えずに)抽出して結合し,また B の行だけを同様に抽出して結合し,この二つを A→B の順に結合したものを result.txt に書き出してください。
データファイル data.txt は何百万行もあるので,File.read
で一気にまるごと読み込むということはせず,File.foreach
で 1 行ずつ読み込んで処理するようにしましょう。
なお,データファイルの末尾には改行が入っているものとします。よって,File.foreach
で取り出した「行」は,最終行まですべて改行が入っていると仮定できます。
コード
text_a = ""
text_b = ""
File.foreach("data.txt") do |line|
case line
when /^A/
text_a += line
when /^B/
text_b += line
end
end
File.write "result.txt", text_a + text_b
講評
速度面では,A の行,B の行の判定に case/when
と正規表現を用いるのをやめて,if
と String#start_with? を用いるのがよさそうですが,この記事で伝えたい点はそこではないので,この点に関しては上記コードのままでよいことにします1。
このコードのまずい点は,見出された A の行,B の行を次々と結合していくのに
text_a += line
のようなやり方をとっていることです。
これは,自己代入式の形で書かれていますが,要するに
text_a = text_a + line
のことですね。
ローカル変数 text_a
に代入されている String オブジェクトと,ローカル変数 line
に代入されている String オブジェクトとを結合した String オブジェクトを新たに生成し,それを text_a
に代入しています。
この式が実行(評価)される前に text_a
に代入されていた String オブジェクトは,実行後にどうなるのでしょうか?
これは,もうどこからも参照されないオブジェクトになります。永遠に使われることはないのです。こういったオブジェクトをガーベジ(garbage)といいます。
もし,A で始まる行が百万行あったとすると,最終的に百万個のガーベジが出来ます。
ガーベジは際限なく溜まり続けるわけではなく,Ruby の処理系が適当なタイミングで消していきます。そうしないとメモリーが足りなくなりますから。この処理をガーベジコレクションといいますが,場合によってはこれがプログラムの効率に無視できない影響を与えることもあります。
ところで,
text_a = text_a + line
のような処理を繰り返していくと,生成する文字列はだんだん長くなりますね。
あとになればなるほど大きなガーベジができるわけです。
以上から,連続する文字列結合で,上記のような書き方は避けたほうがよいことが分かります。
改善
文字列結合に String#<< を使って
text_a << line
とすれば解決します。
String#<<
は破壊的に文字列を結合するメソッドです。
この場合,text_a
に代入されている文字列自身がずんずん伸びていきます(line
に代入されているオブジェクトは変化しません)。
この操作では新たな String オブジェクトが出来たりはしません。
なお,スクリプト先頭に
# frozen_string_literal: true
と書いた場合は,
text_a = ""
text_b = ""
で得られる String オブジェクトが凍結されているので,<<
による破壊的操作ができません。
この場合は
text_a = +""
text_b = +""
のようにして,凍結されていない空の String オブジェクトを作ればいいでしょう。
-
お題をシンプルにするために「A で始まる行」「B で始まる行」としましたが,正規表現を使わないと判定できないようなケースを想定してください。 ↩