Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Khi Nào Nên Dùng freeze và frozen? Trong Ruby: Các Phương Pháp Hay Nhất Cho Bất Biến

Ngày nay, việc thấy #freeze được sử dụng trong mã Ruby khá phổ biến. Nhưng thường không hoàn toàn rõ RẤT freeze được dùng. Trong bài này, chúng ta sẽ xem xét các lý do phổ biến nhất mà một lập trình viên có thể freeze các biến. Để minh họa cho từng lý do, tôi đã trích xuất mã ví dụ từ codebase Rails và các dự án open-source phổ biến khác.

Tạo hằng số bất biến

Trong Ruby, hằng số là có thể biến đổi. Hơi gây nhầm lẫn, nhưng mã rất dễ hiểu. Ở đây, tôi đã tạo một hằng số chuỗi và nối thêm một chuỗi khác vào nó.

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

Bằng cách sử dụng #freeze, tôi có thể tạo một hằng số thực sự là hằng số. Lần này, khi cố gắng sửa đổi chuỗi, tôi nhận được RuntimeError.

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

Đây là ví dụ thực tế trong codebase ActionDispatch. Rails ẩn dữ liệu nhạy cảm trong log bằng cách thay thế bằng văn bản "[FILTERED]". Văn bản này được lưu trữ trong một hằng số frozen.

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

Giảm việc cấp phát đối tượng

Một trong những điều tốt nhất bạn có thể làm để tăng tốc ứng dụng Ruby là giảm số lượng đối tượng được tạo. Một nguồn gây cấp phát đối tượng phiền toái đến từ các string literals rải rác khắp hầu hết các ứng dụng.

Mỗi khi bạn thực hiện gọi phương thức như log("foobar"), bạn tạo một đối tượng String mới. Nếu mã của bạn gọi phương thức như vậy hàng nghìn lần mỗi giây, đó nghĩa là bạn đang tạo (và garbage-collect) hàng nghìn chuỗi mỗi giây. Đó là rất nhiều overhead!

May mắn thay, Ruby cho chúng ta một lối thoát. Nếu chúng ta freeze string literals, trình thông dịch Ruby sẽ chỉ tạo một đối tượng String và cache nó để sử dụng sau. Tôi đã chuẩn bị một benchmark nhanh so sánh hiệu năng của đối số string frozen vs non-frozen. Nó cho thấy tăng hiệu năng khoảng 50%.

require 'benchmark/ips'
def noop(arg) end
Benchmark.ips do |x|
  x.report("normal") { noop("foo") }
  x.report("frozen") { noop("foo".freeze) }
end
# Kết quả với 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

Bạn có thể thấy điều này trong hành động nếu nhìn vào router của Rails. Vì router được dùng cho mọi request trang web, nó cần nhanh. Nghĩa là rất nhiều frozen string literals.

# trích xuất từ 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

Tối ưu hóa có sẵn trong Ruby >= 2.2

Ruby 2.2 và mới hơn (MRI) sẽ tự động freeze string literals được dùng làm khóa hash.

user = {"name" => "george"}
# Trong Ruby >= 2.2
user["name"] # ...tương đương với này, trong Ruby <= 2.1
user["name".freeze]

Và theo Matz, tất cả string literals sẽ được freeze tự động trong Ruby 3.

Value objects & lập trình hàm

Mặc dù Ruby không phải ngôn ngữ lập trình hàm, nhiều Rubyists đã bắt đầu thấy giá trị của việc làm việc theo phong cách hàm. Một trong những nguyên tắc chính của phong cách này là bạn nên tránh side effects. Các đối tượng không bao giờ nên thay đổi sau khi đã được khởi tạo.

Bằng cách gọi phương thức freeze bên trong constructor, có thể đảm bảo một đối tượng sẽ không bao giờ thay đổi. Bất kỳ side-effect không mong muốn nào sẽ dẫn đến việc ném 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