Ruby2.3で導入されたfrozen_string_literalマジックコメントでImmutable Stringを実現する

(image)Ruby2.3で導入されたfrozen_string_literalマジックコメントでImmutable Stringを実現する
目次

Immutable String in Ruby3

Ruby3 では文字列がデフォルトで immutable になるという大きな変更が予定されている追記あり)。

Ruby 3.0 では文字列リテラルをデフォルトで immutable (破壊的変更不可) にする、という方針が『決定』しました

via. [Ruby] Ruby 3.0 の特大の非互換について - まめめも

この変更の背景としては引用リンクに書いてある通り、Rubyの最適化のために文字列のいたるところに.freezeを付けてプルリクエストを投げる輩が大挙してきたことだ。

追記(2019-08-07)

「Ruby3 では文字列がデフォルトで immutable になる」と書いたが、「Ruby3 では文字列をデフォルトで immutable にはしない」という決定がMatzによってなされた。

So I officially abandon making frozen-string-literals default (for Ruby3).

via. Feature #11473: Immutable String literal in Ruby 3 - Ruby master - Ruby Issue Tracking System

したがって、Ruby3以降も文字列を immutable にしたければ、引き続きfrozen_string_literal: trueのマジックコメントが必要となる。


Immutable String in Ruby2.3+

実は Ruby2.3 で既にこの Immutable String を有効にする機能が入っている。やり方はRubyファイルの行頭に次のようにfrozen_string_literal: true とマジックコメントを書けばよい。

# frozen_string_literal: true

frozen_string = "This string is frozen!"

frozen_string_literal の機能を試す

実際に試してみよう。frozen_string_literalの設定が入っているRubyコードと入っていないRubyコードの2つを用意して実行してみる。

frozen_string_literal入りのコード

string_with_frozen_option.rb

# frozen_string_literal: true
5.times { puts "a".object_id }

実行すると全て同じ object_id が返ってくる。

$ ruby string_with_frozen_option.rb
70212460463280
70212460463280
70212460463280
70212460463280
70212460463280

frozen_string_literal無しのコード

string_without_frozen_option.rb

5.times { puts "a".object_id }

実行すると全て違う object_id が返ってくる。

$ ruby string_without_frozen_option.rb
70277165754460
70277165754200
70277165754080
70277165754000
70277165753940

frozen_string_literal はファイル毎に設定される

たとえばfrozen_string_literalの設定が入ったものと入っていないファイルが実行された場合はどうなるだろうか。Railsで試してみる。

class ApplicationController < ActionController::Base
  before_action :not_frozen
  def not_frozen
    5.times { logger.debug("a".object_id) }
  end
end
# frozen_string_literal: true
class WelcomeController < ApplicationController
  def index
    5.times { logger.debug("a".object_id) }
  end
end

これでWelcomeController#indexが実行された場合、ログは下記のようになる。

70346238891860
70346238891080
70346238890280
70346238889340
70346238888420
...
70346229343820
70346229343820
70346229343820
70346229343820
70346229343820

つまりfrozen_string_literalが書かれたWelcomeController上で定義された文字列だけがfreezeされていることがわかる。

mutableなStringを定義するにはどうしたらよい?

一度 frozen_string_literal: true のコードを入れると全ての文字列が.freezeされるので、下記のようなコードはRuntimeErrorとなる。

# frozen_string_literal: true
str = "a"
str << "bc"
puts str
# => test.rb:3:in `<main>': can't modify frozen String (RuntimeError)

方法1: String#dup

この場合の対処法としてはfreezeを解除したい文字列に対して、.dupを付けてやれば解決する。

# frozen_string_literal: true
str = "a".dup
str << "bc"
puts str
# => abc

方法2: String#+@

あるいは、String#+@を使って下記のようにも書ける。

# frozen_string_literal: true
str = +"a"
str << "bc"
puts str
# => abc

こちらのほうがdupするよりもパフォーマンスが優れているので、こちらの書き方のほうがベターである。

まとめ

Ruby3 の Immutable String に先駆けて、Ruby2.3 以上が前提の実行環境では、積極的にfrozen_string_literal: trueのマジックコメント設定をしていくべき。

参考