Các biến lớp của Ruby rất khó hiểu. Ngay cả những người dùng Ruby chuyên nghiệp cũng có thể khó xâm nhập vào chúng. Ví dụ rõ ràng nhất liên quan đến sự kém hiệu quả:
class Fruit
@@kind = nil
def self.kind
@@kind
end
end
class Apple < Fruit
@@kind = "apple"
end
Apple.kind
# => "apple"
Fruit.kind
# => "apple"
Thay đổi kind
biến trên lớp con cũng thay đổi nó trên cha. Nó khá lộn xộn. Nhưng đây chỉ là cách ngôn ngữ được cho là hoạt động. Đó là một quyết định thiết kế được đưa ra cách đây khá lâu để bắt chước Smalltalk.
Nó trở nên tồi tệ hơn
Có những ví dụ khác về sự kỳ lạ của biến lớp dường như không phải là lựa chọn kiến trúc nhiều như những điều kỳ quặc về triển khai. Hôm nay tôi sẽ nói một chút về một trong số những điều này mà tôi thấy thú vị.
Bây giờ chúng ta sẽ so sánh hai đoạn mã. Có vẻ như chúng sẽ tạo ra một kết quả giống hệt nhau nhưng lại không.
Trong ví dụ đầu tiên của chúng tôi, chúng tôi đặt một biến lớp và tạo một phương thức để trả về nó. Hoàn toàn không có gì lạ mắt xảy ra ở đây. Và mọi thứ hoạt động như mong đợi.
class Foo
@@val = 1234
# This is shorthand for declaring a class method.
class << self
def val
@@val
end
end
end
Foo.val
# => 1234
Có lẽ bạn không biết điều này, nhưng class << self
không nhất thiết phải nằm trong định nghĩa lớp. Trong ví dụ dưới đây, chúng tôi đã chuyển nó ra bên ngoài. Một phương thức lớp được thêm vào nhưng nó không thể truy cập vào biến lớp.
class Bar
@@val = 1234
end
class << Bar
def val
@@val
end
end
Bar.val
# warning: class variable access from toplevel
# NameError: uninitialized class variable @@val in Object
Khi chúng tôi cố gắng truy cập biến lớp từ hàm của mình, chúng tôi nhận được một cảnh báo và một ngoại lệ. Chuyện gì đang xảy ra?
Nhập phạm vi từ vựng
Tôi ngày càng tin rằng phạm vi từ vựng là nguồn gốc của 99% các khía cạnh kỳ lạ và khó hiểu của Ruby.
Trong trường hợp bạn không quen thuộc với thuật ngữ này, phạm vi từ vựng đề cập đến việc nhóm mọi thứ lại với nhau dựa trên vị trí chúng xuất hiện trong mã, chứ không phải nơi chúng thuộc về trong một mô hình đối tượng trừu tượng. Sẽ dễ dàng hơn nhiều khi chỉ cho bạn một ví dụ:
class B
# x and y share the same lexical scope
x = 1
y = 1
end
class B
# z has a different lexical scope from x and y, even though it's in the same class.
z = 3
end
Lớp được xác định Lexically
Vậy phạm vi từ vựng hoạt động như thế nào trong ví dụ về biến lớp của chúng ta?
Để truy xuất một biến lớp, Ruby phải biết lớp nào để lấy nó từ đó. Nó sử dụng phạm vi từ vựng để tìm lớp.
Nếu chúng ta xem xét kỹ hơn ví dụ làm việc, chúng ta thấy rằng mã truy cập vào biến lớp là về mặt vật lý trong định nghĩa lớp.
class Foo
class << self
def val
# I'm lexically scoped to Foo!
@@val
end
end
end
Trong ví dụ không hoạt động, mã truy cập biến lớp không thuộc phạm vi từ vựng cho lớp.
class << Bar
def val
# Foo is nowhere in sight.
@@val
end
end
Nhưng nếu nó không thuộc phạm vi từ vựng cho lớp học, thì nó được xác định phạm vi là gì? Cảnh báo mà Ruby in ra cung cấp cho chúng ta một manh mối:warning: class variable access from toplevel
.
Trong ví dụ không hoạt động, nó chỉ ra rằng biến lớp có phạm vi từ vựng đến đối tượng cấp cao nhất. Điều này có thể gây ra một số hành vi thực sự kỳ lạ.
Ví dụ:nếu chúng tôi đã cố gắng đặt một biến lớp từ mã có phạm vi từ vựng thành chính, thì biến lớp sẽ được đặt trên Object
.
class Bar
end
class << Bar
def val=(n)
# This code is lexically scoped to the top level object.
# That means, that `@@val` will be set on `Object`, not `Bar`
@@val = i
end
end
Bar.val = 100
# Whaa?
Object.class_variables
# => [:@@foo]
Các ví dụ khác
Có rất nhiều cách để tham chiếu đến các biến lớp bên ngoài phạm vi từ vựng của lớp. Tất cả chúng sẽ mang đến cho bạn những rắc rối.
Dưới đây là một vài ví dụ để bạn thưởng thức:
class Foo
@@foo = :foo
end
# This won't work
Foo.class_eval { puts @@foo }
# neither will this
Foo.send :define_method, :x do
puts @@foo
end
# ..and you weren't thinking about using modules, were you?
module Printable
def foo
puts @@foo
end
end
class Foo
@@foo = :foo
include Printable
end
Foo.new.foo
Và bây giờ bạn đã biết tại sao mọi người đều nói không bao giờ sử dụng các biến lớp trong Ruby. :)