冒險村11 - frozen_string_literal

11 - frozen_string_literal

延續 Begin from linter : rubocop 中最後 Auto-fix 提到 frozen_string_literal 的設定,這魔法般的註解到底代表什麼意思呢?

先從 Ruby 中常數(constant)變更來理解可以很容易看出 MY_CONSTANT constant 是可以被變更的,不過我們常常在專案中或許想要寫死某個 default 值,會想要建立的是 immutable,可以透過 ruby 提供的 freeze 方法來處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
MY_CONSTANT = "chester"
MY_CONSTANT << "_tang"
puts MY_CONSTANT.inspect

# "chester_tang"
# => nil

Chester = "Chester"
Chester = "ChesterTang"
# :2: warning: already initialized constant Chester
# :1: warning: previous definition of Chester was here
Chester
# => "ChesterTang"

Creating immutable constants

1
2
3
MY_CONSTANT = "chester".freeze
MY_CONSTANT << "_tang"
# FrozenError: can't modify frozen String: "chester"

testing performance with benchmark/ips

透過 benchmark-ips 來測試一定時間內可以執行最多的次數,從結果顯示可以看出約提升 50% 的速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'benchmark/ips'

def noop(arg)
end

Benchmark.ips do |x|
x.report("normal") { noop("foo") }
x.report("frozen") { noop("foo".freeze) }
end

# Results with MRI 2.2.2:
# Calculating -------------------------------------
# normal 152.123k i/100ms
# frozen 167.474k i/100ms
# -------------------------------------------------
# normal 6.158M (± 3.3%) i/s - 30.881M
# frozen 9.312M (± 3.5%) i/s - 46.558M

Value objects & functional programming

也可以在 init 內使用 freeze 方法,保證 constructor 內 object 不會被改變

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Score
attr_accessor :a, :b
def initialize(a, b)
@a = a
@b = b
freeze
end

def change
@a = 3
end
end

score = Score.new(1,2)
score.change # RuntimeError: can't modify frozen Score

Built-in optimizations in Ruby >= 2.2

在 >= 2.2 以後的 Ruby 版本中針對 hash 使用的字串已經有做自動 freeze,不過如果版本在這之前就可能會常常看到下面的這種情況:

1
2
3
4
5
user = {"name" => "george"}
# In Ruby >= 2.2
user["name"]
# ...is equivalent to this, in Ruby <= 2.1
user["name".freeze]

註: Immutable String literal in Ruby 3 原本 Ruby 3.0 中自動 freeze 所有字串目前也沒有後續。

最後,再回來一開始設定 frozen_string_literal 原因,到底是什麼魔法呢?

Magic Comments!

版本 >= 2.3 後的功能,這行註解起來的魔法,可以自動將該檔案內的 String 通通做 #freeze 的方法

1
2
3
4
5
6
7
8
9
10
11
# frozen_string_literal: true
require "sidekiq/version"

module Sidekiq
NAME = "Sidekiq"
LICENSE = "See LICENSE and the LGPL-3.0 for licensing details."
DEFAULTS = {
#...
}
#...
end

參考來源