Chào mừng đến với một tập mới của Ruby Magic! Ấn bản của tháng này là về kính thiên văn, một chủ đề được khơi mào bởi cuộc thảo luận giữa hai nhà phát triển (Xin chào Maud!).
Thông qua việc kiểm tra metaclasses, chúng ta sẽ tìm hiểu cách thức hoạt động của các phương thức class và instance trong Ruby. Trong quá trình này, hãy khám phá sự khác biệt giữa việc xác định một phương thức bằng cách chuyển một "definee" rõ ràng và sử dụng class << self
hoặc instance_eval
. Đi thôi!
Phiên bản lớp và Phương thức phiên bản
Để hiểu lý do tại sao metaclasses được sử dụng trong Ruby, chúng ta sẽ bắt đầu bằng cách kiểm tra sự khác biệt giữa các phương thức instance- và class.
Trong Ruby, một lớp là một đối tượng xác định một bản thiết kế để tạo ra các đối tượng khác. Các lớp xác định những phương thức nào có sẵn trên bất kỳ phiên bản nào của lớp đó.
Việc xác định một phương thức bên trong một lớp sẽ tạo ra một phương thức thể hiện trên lớp đó. Bất kỳ phiên bản nào trong tương lai của lớp đó sẽ có sẵn phương thức đó.
class User
def initialize(name)
@name = name
end
def name
@name
end
end
user = User.new('Thijs')
user.name # => "Thijs"
Trong ví dụ này, chúng tôi tạo một lớp có tên là User
, với một phương pháp phiên bản được đặt tên là #name
trả về tên của người dùng. Sử dụng lớp, sau đó chúng tôi tạo một cá thể lớp và lưu trữ nó trong một biến có tên user
. Kể từ khi user
là một bản sao của User
lớp, nó có #name
phương pháp có sẵn.
Một lớp lưu trữ các phương thức thể hiện của nó trong bảng phương thức . Bất kỳ phiên bản nào của lớp đó đều tham chiếu đến bảng phương thức của lớp đó để có quyền truy cập vào các phương thức cá thể của nó.
Đối tượng lớp
A phương thức lớp là một phương thức có thể được gọi trực tiếp trên lớp mà không cần phải tạo một thể hiện trước. Phương thức lớp được tạo bằng cách thêm tiền tố vào tên của nó bằng self.
khi định nghĩa nó.
Bản thân một lớp là một đối tượng. Một hằng số tham chiếu đến đối tượng lớp, vì vậy các phương thức lớp được định nghĩa trên nó có thể được gọi từ bất kỳ đâu trong ứng dụng.
class User
# ...
def self.all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]
Các phương thức được xác định bằng một self.
-prefix không được thêm vào bảng phương thức của lớp. Thay vào đó, chúng được thêm vào siêu kính của lớp.
Kính đồng hồ
Ngoài một lớp, mỗi đối tượng trong Ruby có một siêu kính ẩn. Metaclasses là những chiếc kính đơn, có nghĩa là chúng thuộc về một đối tượng duy nhất. Nếu bạn tạo nhiều bản sao của một lớp, chúng sẽ chia sẻ cùng một lớp, nhưng tất cả chúng sẽ có kính đo riêng biệt.
thijs, robert, tom = User.all
thijs.class # => User
robert.class # => User
tom.class # => User
thijs.singleton_class # => #<Class:#<User:0x00007fb71a9a2cb0>>
robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>>
tom.singleton_class # => #<Class:#<User:0x00007fb71a9a2c10>>
Trong ví dụ này, chúng ta thấy rằng mặc dù mỗi đối tượng có lớp User
, các lớp singleton của chúng có các ID đối tượng khác nhau, nghĩa là chúng là các đối tượng riêng biệt.
Bằng cách có quyền truy cập vào một siêu kính, Ruby cho phép thêm các phương thức trực tiếp vào các đối tượng hiện có. Làm như vậy sẽ không thêm một phương thức mới vào lớp của đối tượng.
robert = User.new("Robert")
def robert.last_name
"Beekman"
end
robert.last_name # => "Beekman"
User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>)
Trong ví dụ này, chúng tôi thêm một #last_name
cho người dùng được lưu trữ trong robert
Biến đổi. Mặc dù robert
là một bản sao của User
, mọi phiên bản mới được tạo của User
sẽ không có quyền truy cập vào #last_name
, vì nó chỉ tồn tại trên robert
Của siêu kính.
self
là gì ?
Khi xác định một phương thức và chuyển một bộ thu, phương thức mới sẽ được thêm vào siêu kính của bộ thu, thay vì thêm nó vào bảng phương thức của lớp.
tom = User.new("Tom")
def tom.last_name
"de Bruijn"
end
Trong ví dụ trên, chúng tôi đã thêm #last_name
trực tiếp trên tom
đối tượng, bằng cách chuyển tom
với tư cách là người nhận khi xác định phương thức.
Đây cũng là cách nó hoạt động cho các phương thức lớp.
class User
# ...
def self.all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
Ở đây, chúng tôi chuyển self
một cách rõ ràng với tư cách là người nhận khi tạo .all
phương pháp. Trong định nghĩa lớp, self
đề cập đến lớp (User
trong trường hợp này), vì vậy .all
phương thức được thêm vào User
của siêu kính.
Bởi vì User
là một đối tượng được lưu trữ trong một hằng số, chúng tôi sẽ truy cập vào cùng một đối tượng — và cùng một siêu kính — bất cứ khi nào chúng tôi tham chiếu đến nó.
Mở Metaclass
Chúng tôi đã biết rằng các phương thức lớp là các phương thức trong siêu kính của đối tượng lớp. Biết được điều này, chúng ta sẽ xem xét một số kỹ thuật tạo phương thức lớp khác mà bạn có thể đã thấy trước đây.
class << self
Mặc dù nó đã lỗi thời một chút, một số thư viện sử dụng class << self
để định nghĩa các phương thức của lớp. Thủ thuật cú pháp này sẽ mở ra siêu kính của lớp hiện tại và tương tác trực tiếp với nó.
class User
class << self
self # => #<Class:User>
def all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]
Ví dụ này tạo một phương thức lớp có tên User.all
bằng cách thêm một phương thức vào User
của metaclass. Thay vì chuyển một cách rõ ràng một bộ thu cho phương thức như chúng ta đã thấy trước đây, chúng ta đặt self
tới User
siêu kính của thay vì User
chính nó.
Như chúng ta đã học trước đây, bất kỳ định nghĩa phương thức nào không có bộ thu rõ ràng sẽ được thêm vào như một phương thức thể hiện của lớp hiện tại. Bên trong khối, lớp hiện tại là User
metaclass của (#<Class:User>
).
instance_eval
Một tùy chọn khác là sử dụng instance_eval
, thực hiện điều tương tự với một điểm khác biệt chính. Mặc dù siêu lớp của lớp nhận các phương thức được xác định trong khối, self
vẫn là một tham chiếu đến lớp chính.
class User
instance_eval do
self # => User
def all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]
Trong ví dụ này, chúng tôi xác định một phương thức phiên bản trên User
Metaclass giống như trước đây, nhưng self
vẫn trỏ tới User
. Mặc dù nó thường trỏ đến cùng một đối tượng, nhưng "xác định mặc định" và self
có thể trỏ đến các đối tượng khác nhau.
Những gì chúng tôi đã học được
Chúng ta đã biết rằng các lớp là đối tượng duy nhất có thể có các phương thức và các phương thức thể hiện đó thực sự là các phương thức trên siêu kính của một đối tượng. Chúng tôi biết rằng class << self
chỉ cần hoán đổi self
xung quanh để cho phép bạn xác định các phương thức trên siêu kính và chúng tôi biết rằng instance_eval
hầu hết đều giống nhau (nhưng không chạm vào self
).
Mặc dù bạn sẽ không làm việc rõ ràng với kính thiên văn, nhưng Ruby sử dụng chúng một cách rộng rãi. Biết điều gì sẽ xảy ra khi bạn định nghĩa một phương thức có thể giúp bạn hiểu tại sao Ruby lại hoạt động như nó (và tại sao bạn phải đặt tiền tố các phương thức lớp bằng self.
).
Cảm ơn vì đã đọc. Nếu bạn thích những gì bạn đọc, bạn có thể đăng ký Ruby Magic để nhận e-mail khi chúng tôi xuất bản một bài báo mới mỗi tháng một lần.