Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyのfreezeメソッド完全解説 – オブジェクトの可変性と不変性を理解しよう

オブジェクトが「変更可能(ミュータブル)」であるとは、どういう意味なのでしょうか?

難しい言葉に構える必要はありません。「可変性(ミュータビリティ)」とは、単純に「オブジェクトの内部状態を後から変更できる」という意味です。これはすべてのオブジェクトのデフォルトの挙動であり、freeze(凍結)されたオブジェクトや、言語側で特別扱いされている一部のオブジェクトだけが例外となります。

つまり、Rubyのすべてのオブジェクトが変更可能というわけではないのです。

なぜ数値やシンボルは変更できないのか?

たとえば、整数・シンボル、さらにはtruefalse(これらもすべてオブジェクトです)が変化するのは意味がありません。数値の1は、常に1だからです。

一方、ArrayやHashのようにデータを保持することを目的としたオブジェクトは、パフォーマンスの観点からも内容を変更できる仕組みが必要です。

代替手段はないのか?

変更を加えた新しいコピーを作成し、その新しいオブジェクトを返す方法もあります。この場合、元のオブジェクトはそのまま残ります。

しかし、もし配列が不変(イミュータブル)だったらどうなるでしょうか。要素を1つだけ変更したい場合でも、変更されていない要素まで含めてすべてのデータをコピーし直す必要があります。

100万要素の配列に対して、どんなに小さな変更でも毎回全体をコピーしなければならない場面を想像してみてください。非常に非効率ですね。

それでは、Rubyにおける可変性の仕組みをもう少し詳しく見ていきましょう。

可変性と「変数は参照(ポインタ)」という考え方

プログラミングエラーの中には、次の2つの要素が組み合わさることで発生するものがあります。

  • オブジェクトが変更可能であること
  • 変数がデータそのものではなく、データが格納されている場所への参照を保持していること

こうしたエラーが顕在化する典型的な例が、変数の「エイリアス化」です。

name       = "Peter"
other_name = name

puts other_name
# => "Peter"

この例では、nameother_nameの両方が同じ文字列オブジェクトへの参照を保持しています。どちらの変数を使っても、文字列の表示や変更が可能です。

問題が起きるのは、other_nameを「コピー」だと勘違いして扱ったときです。

other_name[0] = 'T'

name       # => "Teter"
other_name # => "Teter"

両方の変数が同じ文字列を指しているため、"Peter"が"Teter"に変わってしまいました。"Peter"のまま保持しておきたかったのに、意図せず書き換えてしまったわけです。

dup / clone によるオブジェクトの複製

この問題への対処法のひとつが、dupメソッドの利用です。dupを呼び出すと、Rubyはそのオブジェクトのコピーを返します。

また、似たメソッドとしてcloneもあります。こちらはオブジェクトのコピーに加えて、凍結状態(frozen状態)や特異メソッド(singletonメソッド)まで複製する点が異なります。

numbers = [1, 2, 3]

more_numbers = numbers.dup
more_numbers << 4

numbers      # => [1, 2, 3]
more_numbers # => [1, 2, 3, 4]

この例では、元のnumbers配列が変更されていないことがわかります。3行目のdup呼び出しを削除して実行すると何が起こるか、ぜひ試してみてください。

Rubyのfreezeメソッド

意図しない変更からオブジェクトを守るもうひとつの方法が「freeze(凍結)」です。任意のRubyオブジェクトは、freezeメソッドによって凍結できます。

オブジェクトが凍結されると、そのオブジェクトを変更しようとした時点でFrozenError(Ruby 2.5以前ではRuntimeError)例外が発生します。

補足:frozen?メソッドを使えば、オブジェクトが凍結されているかどうかを確認できます。

animals = %w( cat dog tiger )
animals.freeze

animals << 'monkey'
# FrozenError: can't modify frozen Array

ここで注意すべきは、freezeで凍結されるのはそのオブジェクト1つだけという点です。この例では配列そのものが凍結され、要素の追加や削除ができなくなります。しかし、配列の中の文字列までは凍結されないため、依然として変更できてしまうのです。

animals[1][0] = 't'
# => ["cat", "tog", "tiger"]

中身の文字列ごと凍結したい場合は、各要素に対して個別にfreezeを呼び出します。次のように書けます。

animals.each(&:freeze)

フローズンストリング(凍結文字列)とパフォーマンス

可変性はパフォーマンスにも影響を与えます。特に顕著なのが文字列です。大規模なプログラムでは、同じ文字列が何度も使用されるケースが多いためです。

Rubyは、内容がまったく同じ文字列であっても、文字リテラルが出てくるたびに新しいオブジェクトを生成します。これはirbobject_idメソッドを使えば簡単に確認できます。

a = 'test'
b = 'test'

a.object_id # => 76325640
b.object_id # => 76317550

同じ内容なのに別々のオブジェクトが生成されているため、余分なメモリとCPUサイクルを消費してしまいます。

Ruby 2.1以降では、凍結された文字列(frozen string)を使うことで、同一内容の文字列オブジェクトが再利用されるようになります。重複した文字列の新規生成を避けられるため、メモリの節約と小幅な高速化につながります。

Railsはこの理由から、フレームワーク全体で大量のフローズンストリングを活用しています。

この流れを受けてRuby開発チームは、文字列をデフォルトで不変(イミュータブル)オブジェクトにすることを検討しました。当時リリースされたRuby 2.3では、プロジェクト単位でこの挙動を有効化する方法が2つ用意されています。

  • ファイルの先頭にマジックコメント # frozen_string_literal: true を記述する
  • コマンドライン引数 --enable=frozen-string-literal を指定する

デフォルトでの文字列不変化はRuby 3.0で導入される予定でしたが、最終的には見送られました。現在のRuby 3.x系でも文字列はデフォルトで可変であり、実運用ではマジックコメントによる明示的な有効化が推奨されています。

ただし、闇雲にアプリ内のすべての文字列を凍結すればよいというわけではありません。メリットを実感できるのは、何百回と使い回されるような文字列に対してのみです。凍結候補となる文字列を見つけるための解析ツールも公開されているので、活用してみるとよいでしょう。

メソッドの挙動を正しく理解する

可変なオブジェクトのメソッドが、必ずしもそのオブジェクト自体を変更するとは限りません。たとえばgsubメソッドは新しい文字列を返すだけで、元の文字列は無傷のままです。

一方で、元のオブジェクトを破壊的に(in-placeで)直接変更するバージョンを持つメソッドもあります。多くの場合、こちらの方が効率的です。これらのメソッドは、その影響を示すために末尾に感嘆符 ! を付けていることがよくあります。

いわゆる「bangメソッド」の代表例としては、gsub!map!が挙げられます。

ここが重要なポイント:メソッド名の末尾に!が付いていても、必ずしも「オブジェクトを変更するメソッド」を意味するとは限りません。

より一般的には、!は「危険」を表す記号です。たとえばexit!メソッドは、終了ハンドラを無視して即座にプログラムを終了させる、まさに「危険な」メソッドです。

さらに逆に、オブジェクトを変更しているのに!が付いていないメソッドも存在します。例としては、deleteclearpushconcatなど、多数あります。

まとめ

オブジェクトの可変性は一見厄介なテーマですが、この記事を読んだあなたなら、もう適切に対処する準備はできています。メソッドの挙動に不安があるときは、迷わずRuby公式ドキュメントを確認しましょう。それがトラブル回避の一番の近道です。

この記事が役に立ったと感じたら、ぜひ友人にもシェアしてください。また、同様の解説記事を見逃さないために、ニュースレターの購読もぜひご検討ください。

  1. Rubyオブジェクトモデルを詳細に理解する

    ウィキペディアによると、オブジェクト指向プログラミング(OOP)は、「オブジェクト」の概念に基づくプログラミングパラダイムであり、データとコードを含めることができます。フィールド形式のデータ(多くの場合、属性またはプロパティと呼ばれます)と形式のコードです。手順の(多くの場合、メソッドとして知られています)。 Rubyは純粋なオブジェクト指向言語です。つまり、Ruby言語では、すべてがオブジェクトです。これらのオブジェクトは、文字列、数値、クラス、モジュールなどであるかどうかに関係なく、オブジェクトモデルと呼ばれるシステムで動作します。 。 Rubyはobject_idと呼ばれるメソッドを

  2. Rubyで学ぶ挿入ソート:仕組みから計算量まで徹底解説

    ※本記事は、Rubyでさまざまなソートアルゴリズムを実装するシリーズの第4回です。第1回ではバブルソート、第2回では選択ソート、第3回ではマージソートを取り上げました。データのソート手法をさまざまな角度から探っていくシリーズもいよいよ折り返し地点。今回は挿入ソート(Insertion Sort)に焦点を当てます。挿入ソートには魅力的な特徴がたくさんあります。まず、挿入ソートは安定(stable)なアルゴリズムです。つまり、同じキーを持つ要素同士の相対的な順序が入れ替わることがありません。また、インプレース(in-place)アルゴリズムでもあるため、ソート結果を保存するための新しい配列を作成す