12/08/2018, 17:41

instance_eval, class_eval và module_eval trong Ruby

instance_eval Thêm các phương thức cho một instance hay class, module một cách "tốc hành" ("on-the-fly") là một pattern khá phổ biến trong Ruby(activerecord, activesupport, ...) Để thực hiện điều đó, chúng ta có thể dùng các phương thức BasicObject#instance_eval, Module#class_eval, ...

instance_eval

Thêm các phương thức cho một instance hay class, module một cách "tốc hành" ("on-the-fly") là một pattern khá phổ biến trong Ruby(activerecord, activesupport, ...) Để thực hiện điều đó, chúng ta có thể dùng các phương thức BasicObject#instance_eval, Module#class_eval, Module#module_eval.

Thêm phương thức

Trong Ruby, khi chúng ta muốn thêm 1 method vào một instance, chúng ta có thể dùng phương thức BasicObject#instance_eval.Phương thức này nhận đối số là một String hoặc một block

array_second = <<-RUBY
def second
  self[1]
end
RUBY

a = [1, 2, 3]
a.instance_eval(array_second)

a.second  # returns: 2

str = "ruby.devscoop.fr"
str.instance_eval do
  def /(delimiter)
    split(delimiter)
  end
end

str / '.' # returns: ["ruby", "devscoop", "fr"]

cách gọi a.instance_eval(array_second) đã add method second vào biến a- 1 instance của Array

Việc gọi a.instance_eval(array_second) đã thêm phương thức second vào biến a - một instance của Array - bằng cách truyền một String mà sẽ được đánh giá trong context của instance a.

Việc gọi đến str.instance_eval với một block sẽ đánh giá nội dung của block trong context của str - một instance của String.

Ở đây nó sẽ thêm một phương thức /(delimiter)- mà chúng ta có thể sử dụng làm toán tử cho instance str.

Truy cập biến inner

Một feature khác của instance_eval là nó có khả năng truy cập biến inner

class User
  def initialize(email)
    @email = email
  end
end

u = User.new('ruby@devscoop.fr')

u.instance_eval('@email')  # returns: "ruby@devscoop.fr"
u.instance_eval { @email } # returns: "ruby@devscoop.fr"

Ở đây, chúng ta truy cập biến inner @email bằng cách gọi nó như một đối số String của u.instance_eval cũng như bên trong một block mà chúng ta truyền vào instance_eval.

Điều này là có thể bởi vì instance_eval được thực thi trong context của đối tượng đóng vai trò là người nhận - trong trường hợp này là biến u.

Điều này có nghĩa là tất cả các mã được truyền dưới dạng đối số sẽ được thực thi trong scope của instance của User có tên u.

bỏ qua phương thức điều khiển truy cập

Bằng cách sử dụng instance_eval, Điều này có thể gọi một method private hoặc một method protectedcủa một object nhất định từ bên ngoài của đối tượng này.

class User
  def initialize(email)
    @email = email
  end

  private
  def secret_key
    'XX-XXXX-XXXX-XXXX'
  end
end

u = User.new('ruby@devscoop.fr')

u.instance_eval('secret_key')  # returns: "XX-XXXX-XXXX-XXXX"
u.instance_eval { secret_key } # returns: "XX-XXXX-XXXX-XXXX"

Ở đây, chúng ta truy cập private method User#secret_key bằng cách gọi nó như là một đối số kiểu String của u.instance_eval cũng như bên trong một block mà chúng ta truyền vào instance_eval.

class_eval

Trong Ruby, khi chúng ta muốn thêm một method vào một class, chúng ta có thể sử dụng phương thức Module#class_eval. Phương thức này nhận một String hoặc một block làm đối số.

array_second = <<-RUBY
def second
  self[1]
end
RUBY
Array.class_eval(array_second)
String.class_eval do
  def /(delimiter)
    split(delimiter)
  end
end
$> [1,2,3].second
 => 2
$> "1,2,3" / ','
  => ["1", "2", "3"]

Việc gọi tới Array.class_eval(array_second) đã thêm method second vào bất kỳ instance nào của Array bằng cách chuyển một String mà sẽ được đánh giá trong context của class Array .

Cuộc gọi đến String.class_evalvới một khối sẽ đánh giá nội dung của khối trong ngữ cảnh của lớp String. Ở đây nó sẽ thêm một String#/(delimiter)phương thức - mà chúng ta có thể sử dụng làm toán tử - cho bất kỳ cá thể nào của String.

module_eval

các Module#module_eval là tương đương với Module#class_eval đối với các module:

module Commentable
  def add_comment(comment)
    self.comments << comment
  end
  def comments
    @comments ||= []
  end
end
Commentable.module_eval do
  def comment_count
    comments.count
  end
end
class Post
  include Commentable
end
$> post = Post.new
 => #<Post:0x00007fd9ac0238b0> 
$> post.add_comment("Very nice !")
 => ["Very nice !"] 
$> post.comment_count
 => 1

hai lớp vỏ của cùng một lõi

Chúng ta nói rằng class_eval được sử dụng để thêm các method và thuộc tính vào một class hiện có.

Và module_eval được sử dụng để thêm các phương thức và thuộc tính vào một module hiện có.

Trong thực tế, class_eval là một alias đối với module_eval.

Chúng ta hãy xem xét mã nguồn Ruby để xác nhận lời khẳng định trước đó.

trong ruby/vm_eval.c:

rb_define_method(rb_cModule, "module_eval", rb_mod_module_eval, -1); 
rb_define_method(rb_cModule, "class_eval",  rb_mod_module_eval, -1);

Như chúng ta có thể thấy module_eval và class_eval chia sẻ cùng một function C có tên rb_mod_module_eval().

Khi chỉ định hàm C này bao gồm module_eval chúng ta có thể nói rằng class_eval là bí danh đối với module_eval.

Tham khảo

https://medium.com/@farsi_mehdi/ruby-instance-eval-a49fd4afa268

https://medium.com/@farsi_mehdi/ruby-class-eval-vs-module-eval-6c3cc24a070

0