Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เมื่อไหร่ควรใช้ freeze และ frozen ใน Ruby: แนวทางปฏิบัติที่ดีที่สุดสำหรับความไม่เปลี่ยนแปลง

ในปัจจุบัน การเห็น #freeze ในโค้ด Ruby เป็นเรื่องปกติ แต่เหตุผลว่าทำไมถึงใช้ freeze บ่อยครั้งไม่ชัดเจน ในบทความนี้เราจะดูเหตุผลทั่วไปที่นักพัฒนาอาจแช่แข็งตัวแปร โดยใช้โค้ดตัวอย่างจาก Rails codebase และโปรเจกต์โอเพนซอร์สยอดนิยม

การสร้างค่าคงที่ (Constants) ที่ไม่เปลี่ยนแปลงได้

ใน Ruby ค่าคงที่ (constants) เป็น mutable (เปลี่ยนแปลงได้) อาจสับสนเล็กน้อย แต่โค้ดเข้าใจง่าย ตัวอย่าง: สร้างค่าคงที่สตริงแล้วต่อสตริงอีกตัวเข้ามา

MY_CONSTANT = "foo"
MY_CONSTANT << "bar"
puts MY_CONSTANT.inspect # => "foobar"

ด้วยการใช้ #freeze เราสามารถสร้างค่าคงที่ที่เป็นค่าคงที่จริงได้ ครั้งนี้พยายามแก้ไขสตริงจะได้ RuntimeError

MY_CONSTANT = "foo".freeze
MY_CONSTANT << "bar" # => RuntimeError: can't modify frozen string

ตัวอย่างจริงใน ActionDispatch codebase: Rails ซ่อนข้อมูลละเอียดอ่อนในล็อกโดยแทนที่ด้วยข้อความ "[FILTERED]" ข้อความนี้เก็บในค่าคงที่ที่แช่แข็งไว้

module ActionDispatch
  module Http
    class ParameterFilter
      FILTERED = '[FILTERED]'.freeze
      ...

ลดการจัดสรรออบเจกต์ (Object Allocations)

หนึ่งในวิธีที่ดีที่สุดเพื่อเร่งแอป Ruby คือลดจำนวนออบเจกต์ที่สร้างขึ้น แหล่งที่มาของการจัดสรรออบเจกต์ที่น่ารำคาญมาจากสตริงลิเทอรัล (string literals) ที่กระจายอยู่ทั่วแอป

ทุกครั้งที่เรียกเมธอดอย่าง log("foobar") จะสร้างออบเจกต์ String ใหม่ หากโค้ดเรียกเมธอดแบบนี้หลายพันครั้งต่อวินาที หมายความว่าคุณสร้าง (และ garbage-collect) สตริงหลายพันตัวต่อวินาที นั่นคือ overhead มาก!

다행히 Ruby ให้ทางออก: หากเราแช่แข็งสตริงลิเทอรัล ตัวแปลภาษา Ruby จะสร้างออบเจกต์ String เพียงตัวเดียวและแคชไว้ใช้ใหม่ ผมทำ benchmark เปรียบเทียบประสิทธิภาพระหว่างสตริงแช่แข็งและไม่แช่แข็ง พบประสิทธิภาพเพิ่มขึ้นประมาณ 50%

require 'benchmark/ips'
def noop(arg) end
Benchmark.ips do |x|
  x.report("normal") { noop("foo") }
  x.report("frozen") { noop("foo".freeze) }
end
# ผลลัพธ์กับ 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

เห็นได้ใน Rails router: เนื่องจาก router ใช้ทุกคำขอเว็บ ต้องเร็ว จึงใช้สตริงลิเทอรัลแช่แข็งเยอะ

# ดึงมาจาก https://github.com/rails/rails/blob/f91439d848b305a9d8f83c10905e5012180ffa28/actionpack/lib/action_dispatch/journey/router/utils.rb#L15
def self.normalize_path(path)
  path = "/#{path}"
  path.squeeze!('/'.freeze)
  path.sub!(%r{/+\Z}, ''.freeze)
  path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
  path = '/' if path == ''.freeze
  path
end

การเพิ่มประสิทธิภาพ built-in ใน Ruby >= 2.2

Ruby 2.2 ขึ้นไป (MRI) จะแช่แข็งสตริงลิเทอรัลที่ใช้เป็นคีย์แฮชอัตโนมัติ

user = {"name" => "george"}
# ใน Ruby >= 2.2
user["name"] # ...เทียบเท่ากับนี้ใน Ruby <= 2.1
user["name".freeze]

และตาม Matz สตริงลิเทอรัลทั้งหมดจะถูกแช่แข็งอัตโนมัติใน Ruby 3

Value Objects & Functional Programming

แม้ Ruby จะไม่ใช่ภาษา functional programming แต่หลาย Rubyists เริ่มเห็นคุณค่าของการทำงานแบบ functional หนึ่งในหลักการหลักคือหลีกเลี่ยง side effects ออบเจกต์ไม่ควรเปลี่ยนแปลงหลัง initialization

ด้วยการเรียก freeze ภายใน constructor สามารถรับประกันได้ว่าออบเจกต์จะไม่เปลี่ยนแปลง Side effects ที่ไม่ได้ตั้งใจจะทำให้เกิด exception

class Point
  attr_accessor :x, :y
  def initialize(x, y)
    @x = x
    @y = y
    freeze
  end
  def change
    @x = 3
  end
end

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