Đánh giá về tính bất biến trong Ruby

Trạng thái có thể thay đổi được chia sẻ là nguồn gốc của rất nhiều lỗi. Khi hai hoặc nhiều đối tượng sử dụng cùng một phần dữ liệu có thể thay đổi, tất cả chúng đều có khả năng phá vỡ lẫn nhau theo những cách khó có thể gỡ lỗi. Tuy nhiên, nếu dữ liệu được chia sẻ là bất biến, các đối tượng này không thể ảnh hưởng lẫn nhau và được tách rời một cách hiệu quả.
Bài viết này là một đánh giá về các tùy chọn có sẵn cho Rubyists liên quan đến tính bất biến. Chúng ta sẽ xem xét các tính năng tích hợp của Ruby 2.3 và một vài viên ngọc.
Cấp đông Lib tiêu chuẩn
Hãy bắt đầu với freeze
phương thức từ thư viện chuẩn:
Đối tượng # đóng băng
Ngăn chặn các sửa đổi thêm đối với obj
. A RuntimeError
sẽ được nâng lên nếu cố gắng sửa đổi. Không có cách nào để giải phóng một đối tượng bị đóng băng. Xem thêm Object#frozen?
.
Phương thức này trả về tự.
a = [ "a", "b", "c" ]
a.freeze
a << "z"
sản xuất:
prog.rb:3:in `<<': can't modify frozen Array (RuntimeError)
from prog.rb:3
Đối tượng của các lớp sau đây luôn đông lạnh: Fixnum
, Bignum
, Float
, Symbol
.
Các freeze
phương pháp sẽ làm việc cho hầu hết các đối tượng, trong đó có trường hợp của các tầng lớp người dùng định nghĩa:
class Foo
def mutate_self
@x = 5
end
end
f = Foo.new
f.freeze
f.mutate_self #=> RuntimeError: can't modify frozen Foo
Ngoại lệ duy nhất là các lớp kế thừa từ BasicObject
. Các freeze
phương pháp được định nghĩa trên Object
, vì vậy nó không phải là có sẵn với các trường hợp BasicObject
:
class BasicFoo < BasicObject; end
bf = BasicFoo.new
bf.freeze #=> NoMethodError: undefined method `freeze' for #<BasicFoo:0x007f912b9c3060>
Bạn sẽ thường thấy freeze
được sử dụng khi gán hằng số, để đảm bảo rằng các giá trị không thể bị thay đổi. Điều này là do việc gán lại một biến hằng số sẽ tạo ra một cảnh báo, nhưng việc thay đổi một giá trị hằng số thì không .
module Family
NAMES = ['Tom', 'Dane']
end
# mutation is allowed
Family::NAMES << 'Alexander'
p Family::NAMES #=> ["Tom", "Dane", "Alexander"]
# reassignment triggers a warning
Family::NAMES = ['some', 'other', 'people']
#=> warning: already initialized constant Family::NAMES
Vì vậy, nếu bạn muốn đảm bảo rằng các hằng số của bạn thực sự không đổi, bạn cần cố định giá trị:
module Family
NAMES = ['Tom', 'Dane'].freeze
end
Vấn đề chính của freeze
phương pháp là nó nông , trái ngược với đệ quy. Ví dụ: một mảng cố định không thể có thêm, xóa hoặc thay thế các phần tử, nhưng bản thân các phần tử hiện tại vẫn có thể thay đổi:
module Family
NAMES = ['Tom', 'Dane'].freeze
end
Family::NAMES.first.upcase!
p Family::NAMES #=> ["TOM", "Dane"]
Frozen String Literals Trong Ruby 2.3
Bạn có thể nhận thấy rằng các biểu tượng và số được tự động đóng băng trong Ruby. Ví dụ, không thể thực hiện add!
phương pháp này :
x = 5
x.add!(2)
x == 7 #=> this can't be true
Trong hầu hết các ngôn ngữ, ký tự chuỗi cũng không thay đổi, giống như số và ký hiệu. Tuy nhiên, trong Ruby, tất cả các chuỗi đều có thể thay đổi theo mặc định.
Điều này sẽ thay đổi trong phiên bản chính tiếp theo của Ruby. Tất cả các ký tự chuỗi sẽ không thay đổi theo mặc định trong Ruby 3, nhưng điều đó vẫn còn vài năm nữa. Trong thời gian chờ đợi, chức năng này có thể được kích hoạt tùy chọn kể từ Ruby 2.3.
Có một tùy chọn dòng lệnh có sẵn cho phép các ký tự chuỗi cố định trên toàn cầu:
ruby --enable-frozen-string-literal whatever.rb
Thật không may, điều này sẽ phá vỡ rất nhiều mã và đá quý đã tồn tại trước đó, bởi vì hầu hết mã được viết với giả định rằng các ký tự chuỗi có thể thay đổi được.
Cho đến khi mã cũ hơn được cập nhật để xử lý các chuỗi bị cố định, tốt hơn nên bật tùy chọn này trên cơ sở từng tệp bằng cách sử dụng “nhận xét kỳ diệu” này ở đầu mỗi tệp:
# frozen_string_literal: true
greeting = 'Hello'
greeting.upcase! #=> RuntimeError: can't modify frozen String
Khi nhận xét kỳ diệu này tồn tại, chuỗi ký tự bên trong tệp sẽ bị đóng băng theo mặc định, nhưng mã trong các tệp khác sẽ không bị ảnh hưởng.
Khi bạn thực sự muốn một chuỗi có thể thay đổi, bạn phải tạo một String#new
chuỗi bằng hoặc sao chép một chuỗi cố định bằng cách sử dụng String#dup
:
# frozen_string_literal: true
# this string is mutable
x = String.new('Hello')
x.upcase!
puts x #=> 'HELLO'
# and so is this
y = 'World'.dup
y.upcase!
puts y #=> 'WORLD'
Các ice_nine
Gem - Freezing Recursive
Nó chỉ ra rằng việc đóng băng một cách đệ quy một đối tượng đúng cách là một chút khó khăn, nhưng may mắn thay có một viên ngọc quý cho điều đó. Các ice_nine
đá quý áp dụng freeze
phương pháp đệ quy, đảm bảo rằng một đối tượng là thực sự đông lạnh:
require 'ice_nine'
module Family
NAMES = IceNine.deep_freeze(['Tom', 'Dane'])
end
Family::NAMES.first.upcase!
#=> RuntimeError: can't modify frozen String
Đá quý cũng cung cấp một phần mở rộng cốt lõi tùy chọn để xác định Object#deep_freeze
, để thuận tiện:
require 'ice_nine'
require 'ice_nine/core_ext/object'
module Family
NAMES = ['Tom', 'Dane'].deep_freeze
end
Các values
Gem - Các lớp Immutable Struct-Like
Thay vì đóng băng các đối tượng có thể thay đổi, thường tốt hơn là tạo các đối tượng không thể thay đổi theo mặc định. Đây là nơi mà values
đá quý rất hữu ích.
Nếu bạn đã quen thuộc với Struct
thư viện tiêu chuẩn , values
thì về cơ bản gem cũng giống như vậy, ngoại trừ việc nó là bất biến theo mặc định.
Đây là một số mã ví dụ:
require 'values'
# `Value.new` creates a new class, just like `Struct`
Person = Value.new(:name, :age)
# The `new` class method works just like `Struct`
tom = Person.new('Tom', 28)
puts tom.age #=> 28
# There is also the `with` class method, that creates an
# object given a hash
dane = Person.with(name: 'Dane', age: 42)
puts dane.age #=> 42
# You can use the `with` instance method to create new objects
# based existing objects, with some attributes changed
ben = tom.with(name: 'Ben')
p ben #=> #<Person name="Ben", age=28>
p tom #=> #<Person name="Tom", age=28>
# Unlike `Struct`, objects do not have any mutating methods defined
tom.name = 'Ben'
#=> NoMethodError: undefined method `name=' for #<Person name="Tom", age=28>
Cũng giống như Struct
các lớp, các Value
lớp này có thể có các phương thức tùy chỉnh:
Fungus = Value.new(:genus, :species, :common_name) do
def display_name
"#{common_name} (#{genus} #{species})"
end
end
f = Fungus.new('Amanita', 'muscaria', 'Fly agaric')
puts f.display_name #=> Fly agaric (Amanita muscaria)
Không giống như Struct
các lớp, các lớp này sẽ tạo ra lỗi nếu thiếu bất kỳ thuộc tính nào khi tạo. Đây là một điều tốt, vì nó cảnh báo bạn về những lỗi tiềm ẩn thay vì im lặng bỏ qua chúng.
Person = Value.new(:name, :age)
Person.new('Tom') #=> ArgumentError: wrong number of arguments, 1 for 2
Person.with(age: 28) #=> ArgumentError: Missing hash keys: [:name] (got keys [:age])
Các lớp này chỉ là bất biến nông, giống như freeze
phương thức tích hợp sẵn. Bản thân các đối tượng không thể thay đổi, nhưng các thuộc tính của chúng vẫn có thể thay đổi được.
tom = Person.new('Tom', 28)
tom.name.upcase!
p tom #=> #<Person name="TOM", age=28>
Toàn bộ viên ngọc chỉ có khoảng 100 dòng mã , vì vậy rất dễ hiểu về toàn bộ.
Đối với phần lớn các tình huống mà bạn sẽ sử dụng Struct
, tôi nghĩ Value
các lớp học là lựa chọn tốt hơn. Đối với những tình huống hiếm hoi mà bạn đang cố gắng đạt được từng giọt hiệu suất cuối cùng, Struct
vẫn là lựa chọn tốt hơn, ít nhất là trên MRI. Điều đó không có nghĩa là Value
các lớp chậm - chúng có hiệu suất tương tự như bất kỳ lớp Ruby nào khác, nếu không muốn nói là tốt hơn do quá trình băm tích cực. Trong MRI, Struct
lớp được thực hiện theo một cách hiệu quả bất thường. Trong các triển khai khác, chẳng hạn như JRuby, có thể không có sự khác biệt về hiệu suất.
Nếu bạn không sử dụng Struct
các lớp, bạn có thể tự hỏi tại sao và nơi bạn muốn sử dụng một trong hai lớp đó. Nguồn tốt nhất mà tôi có thể chỉ cho bạn là Giá trị của các Giá trị của Rich Hickey . Cuối cùng, nó tóm tắt tất cả những lợi ích của ngữ nghĩa giá trị , mà Rich giải thích chi tiết.
Các adamantium
Gem - Tự động Recursive Freezing
Các adamantium
đá quý cung cấp đóng băng đệ quy tự động cho các lớp học của Ruby qua ice_nine
đá quý.
require 'adamantium'
class Person
include Adamantium
attr_reader :name, :age
def initialize(name, age)
@name = name
@age = age
end
def with_name(new_name)
transform do
@name = new_name
end
end
end
tom = Person.new('Tom', 28)
dane = tom.with_name('Dane')
p tom #=> #<Person:0x007f90b182bb28 @name="Tom", @age=28 ...
p dane #=> #<Person:0x007f90b0b28048 @name="Dane", @age=28 ...
Adamantium hoạt động bằng cách ghi đè new
phương thức lớp. Sau khi một đối tượng đã được cấp phát và initialize
phương thức của nó đã được chạy, nó sẽ bị đóng băng bằng cách sử dụng ice_nine
gem. Điều này có nghĩa là bạn có thể thay đổi đối tượng từ bên trong initialize
, nhưng không bao giờ lặp lại.
Để tạo các đối tượng bất biến mới từ những đối tượng hiện có, có một transform
phương pháp. Điều này hoạt động bằng cách tạo ra một bản sao có thể biến đổi, chạy một khối đột biến trên bản sao, sau đó đóng băng sâu bản sao đó trước khi trả lại. Bạn có thể xem một ví dụ về điều này trong with_name
phương pháp trên.
Adamantium yêu cầu nhiều tấm boilerplate hơn values
gem, nhưng nó thực hiện đóng băng đệ quy thích hợp. Nó cũng có chức năng tự động ghi nhớ và đóng băng các giá trị trả về của các phương thức.
Các anima
Gem - includable Value Semantics
Các anima
viên ngọc cơ bản là một hỗn hợp của values
đá quý và adamantium
đá quý.
require 'anima'
class Person
include Anima.new(:name, :age)
end
tom = Person.new(name: 'Tom', age: 28)
rhi = tom.with(name: 'Rhiannon')
p tom #=> #<Person name="Tom" age=28>
p rhi #=> #<Person name="Rhiannon" age=28>
Nó có tính ngắn gọn của values
đá quý và sử dụng Adamantium để đóng băng đệ quy tự động.
Hãy coi đây là phiên bản có trọng lượng nặng của values
viên đá quý. Nó có một vài tính năng hơn, nhưng nó cũng mang lại lăm đá quý như phụ thuộc: ice_nine
, memoizable
, abstract_type
, adamantium
và equalizer
. Để so sánh, values
gem không có phụ thuộc và được triển khai trong một tệp duy nhất với khoảng 100 dòng mã.
Các hamster
Gem - Persistent Cấu trúc dữ liệu
Các hamster
đá quý cung cấp một tập các lớp cấu trúc dữ liệu liên tục. Các lớp học này thay thế bất biến cho các lớp học của Ruby tiêu chuẩn như Hash
, Array
, và Set
. Chúng hoạt động theo kiểu tương tự như các viên ngọc khác - không thể sửa đổi các đối tượng, nhưng bạn có thể tạo các đối tượng mới dựa trên các đối tượng hiện có.
Làm việc với các giá trị không thay đổi thường đòi hỏi nhiều bản sao, giống như sao chép toàn bộ mảng chỉ để thêm một phần tử mới. Cấu trúc dữ liệu liên tục cung cấp hiệu suất tốt hơn cho các loại hoạt động này bằng cách giảm số lượng đối tượng cần được sao chép và sử dụng lại càng nhiều đối tượng càng tốt.
Ví dụ, nếu bạn muốn tạo một mảng được cố định từ một mảng đã được cố định hiện có, bạn sẽ phải làm như thế này trong Ruby đơn giản:
original = [1, 2, 3].freeze
new_one = original.dup # makes a copy
new_one << 4
new_one.freeze
p original #=> [1, 2, 3]
p new_one #=> [1, 2, 3, 4]
Với Hamster::Vector
, điều này sẽ trông giống như:
require 'hamster'
original = Hamster::Vector[1, 2, 3]
new_one = original.add(4)
p original #=> Hamster::Vector[1, 2, 3]
p new_one #=> Hamster::Vector[1, 2, 3, 4]
Trong Hamster::Vector
phiên bản, new_one
có thể không phải là một bản sao hoàn toàn của original
. Bên trong, new_one
giá trị có thể chỉ giữ 4
một tham chiếu cộng với original
. Chia sẻ trạng thái bên trong theo cách này cải thiện cả tốc độ và mức sử dụng bộ nhớ, đặc biệt là đối với các đối tượng lớn. Tất cả điều này diễn ra tự động dưới mui xe, vì vậy bạn không cần phải suy nghĩ về nó.
Để có cái nhìn tổng quan về chủ đề này, tôi giới thiệu một bài nói chuyện Rich Hickey khác: Cấu trúc dữ liệu liên tục và tài liệu tham khảo được quản lý . Chuyển tiếp đến 23:49 để đến phần cụ thể về cấu trúc dữ liệu liên tục.
Đối tượng giá trị Virtus
Tôi muốn nhanh chóng đề cập đến virtus
đá quý , mặc dù tôi khuyên không nên sử dụng nó. Nó có một số chức năng "đối tượng giá trị" hoạt động rất giống với values
and anima
gems, nhưng với các tính năng bổ sung xung quanh xác thực kiểu và cưỡng chế.
require 'virtus'
class Person
include Virtus.value_object
values do
attribute :name, String
attribute :age, Integer
end
end
tom = Person.new(name: 'Tom', age: 28)
sue = tom.with(name: 'Sue')
p tom #=> #<Person name="Tom" age=28>
p sue #=> #<Person name="Sue" age=28>
Về lý do tại sao tôi khuyên bạn không nên sử dụng nó, hãy để tôi trích dẫn tác giả của viên đá quý Piotr Solnica trong chủ đề reddit này :
Lý do tại sao tôi không còn quan tâm đến việc làm việc trên nền tảng công nghệ không phải là điều tôi có thể giải thích dễ dàng, nhưng tôi sẽ cố gắng.
[…]
[Nó] đã được tối ưu hóa cho một trường hợp sử dụng cụ thể là lưu trữ dữ liệu từ một biểu mẫu web để làm cho cuộc sống của chúng ta đơn giản hơn và chức năng này chỉ đơn giản là được đưa vào ORM
[…]
Tôi đã phạm phải một sai lầm mà trước đây đã từng mắc phải từ ActiveRecord.
[…]
Tôi đã mất một lúc để hiểu điều gì đang thực sự diễn ra. Virtus là viên ngọc mang lại di sản của DataMapper, mang lại di sản của… ActiveRecord. Đó là một quá trình dài để hiểu một số vấn đề cơ bản, khi tôi đã hiểu chúng, tôi bắt đầu làm việc trên các thư viện mới để giải quyết những vấn đề đó theo cách tốt hơn. Tôi càng làm việc nhiều hơn với những thư viện đó, tôi càng thấy rõ ràng rằng Virtus sẽ phải được thay đổi hoàn toàn và sẽ không còn phục vụ cùng mục đích nếu tôi muốn xây dựng nó theo cách mà tôi nghĩ là đúng.
Virtus đã cố gắng trở thành một con dao quân đội Thụy Sĩ phổ biến cho các cuộc cưỡng chế, như một hệ quả tự nhiên của việc được trích xuất từ một ORM có nhiều điểm chung với ActiveRecord, nó đã cố gắng làm quá nhiều, với rất nhiều hành vi ngầm, các trường hợp cạnh kỳ lạ, hiệu suất các vấn đề và DSL phức tạp.
[…]
Hơn nữa, thuộc tính DSL với nhiều tùy chọn là một mô hình chống. Đó là những gì tôi đã học được theo thời gian. Và nó không kết thúc ở đây - thiếu an toàn kiểu thực tế là một vấn đề, Virtus có một chế độ nghiêm ngặt nhưng không thể làm cho nó đúng trong một thư viện được sử dụng trong nhiều bối cảnh khác nhau.
Sắp tới tiếp theo: Phong cách kỷ luật và chức năng
Bài viết này chỉ đề cập đến các tùy chọn thuộc loại nặng tay. Tất cả đều yêu cầu đá quý hoặc mã bổ sung.
Bài tiếp theo sẽ nói về cái mà tôi gọi là “phong cách chức năng” - sử dụng kỷ luật để tránh đột biến, thay vì thực thi tính bất biến. Nó không yêu cầu thêm đá quý và không cần thêm mã. Giữ nguyên!
Có thể bạn quan tâm
