Computer >> Máy Tính >  >> Lập trình >> Ruby

Phạm vi Lexical và các biến lớp Ruby

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. :)