Helpex - Trao đổi & giúp đỡ Đăng nhập

Đá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 freezephươ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 RuntimeErrorsẽ đượ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 freezephươ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 freezephươ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 freezephươ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#newchuỗ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_nineGem - 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 freezephươ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 valuesGem - 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 Structthư viện tiêu chuẩn , valuesthì 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ư Structcác lớp, các Valuelớ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ư Structcá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ư freezephươ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ĩ Valuecá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, Structvẫ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à Valuecá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, Structlớ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 Structcá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 adamantiumGem - 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 đè newphương thức lớp. Sau khi một đối tượng đã được cấp phát và initializephương thức của nó đã được chạy, nó sẽ bị đóng băng bằng cách sử dụng ice_ninegem. Đ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 transformphươ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_namephương pháp trên.

Adamantium yêu cầu nhiều tấm boilerplate hơn valuesgem, 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 animaGem - includable Value Semantics

Các animaviê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 valuesviê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, adamantiumequalizer. Để so sánh, valuesgem 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 hamsterGem - 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::Vectorphiên bản, new_onecó thể không phải là một bản sao hoàn toàn của original. Bên trong, new_onegiá trị có thể chỉ giữ 4mộ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 valuesand animagems, 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!

5 hữu ích 0 bình luận 4.5k xem chia sẻ

Có thể bạn quan tâm

loading