Scope (context) trong ruby
Bài đăng này đã không được cập nhật trong 9 năm
I. Scope là gì
- Scope mà tôi muốn nhắc đến không phải là class method của ActiveRecord
scope :red, -> { where(color: 'red') } # không phải cái này
- Scope ở đây bạn có hình dung đến 2 vấn đề thứ nhất đó là các biến, thứ 2 đó là khả năng truy cập. Hiểu được Scope bạn sẽ biết được các biến, hằng số, các phương thức có khả dụng tại một thời điểm nào đó hay hay ngữ cảnh nào đó hay không.
- Scope ở đây nó giải quyết vấn đề naming trong code của bạn, Giả sử trong ruby không có khái niệm scope, tất cả các biến có thể được sử dụng như nhau ở mọi nơi, ôi không điều đó thật nguy hiểm các biến đó sẽ được thay đổi ở bất cứ đâu bất cứ model nào, quá khó để biết được biến nào được sử dụng biến nào chưa => bạn sẽ không kiểm soát được nó.
- Scope sẽ thay đổi bắt cứ khi nào nó gặp từ khóa class, module, method.
II. Ruby variable scope
Ruby có các kiểu biến khác nhau:
- Class variable: Được bắt đầu bằng 2 ký từ @, nó được dùng trong class và các class con
@@class_variable = :best
- Instance variable: Được bắt đầu bằng ký tự @, nó chỉ được dùng trong một object cụ thể thông qua các phương thức của một object. Nó không được dùng trực tiếp trong class definitions.
@instance_variable = :best
- Globel variable: Được bắt đầu bằng ký tự
$
, nó dùng ở bất cứ mọi nơi - Local variable: Phạm vi sử dụng của nó phụ thuộc vào scope
locale_variable = :best
- Local variable trong Scope
Giả sử trong context của method tôi có ví dụ như sau:
class A
def first_method
if false
a = :best
end
puts a
end
end
Nếu tôi gọi A.new.first_method
thì điều gì sẽ xảy ra? sẽ có người nghĩ nó sẽ bị chết ở dòng puts a
vì biến a chưa được đinh nghĩa giờ hãy chạy trên console xem sao
2.1.5 :016 > A.new.first_method
nil
=> nil
Không có lỗi gì ở đây cả, tại sao vậy? Điều này là vi trong quá trình biên dịch của ruby trong cùng một scope, local variable sẽ tự động được gán giá trị gì đó cho dù code đó có chạy hay không.
Giờ hãy thử với một ví dụ khác
class A
def first_method
[1,2].each do |c|
puts c
x = 1
end
puts x
end
end
Tương tự như trên ta sẽ chạy câu lệnh A.new.first_method
Kết quả
2.1.5 :010 > A.new.first_method
1
2
NameError: undefined local variable or method `x' for #<A:0x0000000233bdf8>
from (irb):7:in `first_method'
from (irb):10
Biến x
trong vòng lặp khác biến x
bên ngoài vòng lặp, 2 biến ở 2 scope khác nhau. nên nó không được gán trong qúa trình biên dịch và gây ra lỗi.
III. Scope Gates
1. Khái niệm
- Method, module, class definition được xem như là scope gates, vì khi định nghĩa một phương thức mới một class hay module mới thì một scope mới sẽ được tạo ra.
Xét ví dụ sau:
var1 = 1
class A # start new scope
def initialize
var2 = 2
p local_variables
end
def first_method # clean old scope and start new scope
var3 = 3
p local_variables
end # end scope
end # end scope
Thực hiện lệnh sau
a = A.new
a.first_method
Kết quả sẽ như sau:
2.1.5 :046 > a = A.new
[:var2]
=> #<A:0x00000002208be8>
2.1.5 :047 > a.first_method
[:var3]
=> [:var3]
2.1.5 :048 >
- Trong hàm init chỉ in ra biến local là
var2
mà không in ra[var1, var2]
là vì var1 ở scope khác top level scope cònvar2
nằm trong scope của methodinit
, tương tự khi gọia.first_method
chỉ cóvar3
mà không cóvar2
. - Khi bắt đầu một class mới thì các local variable của top level sẽ bị tạm thời mất(không phải mất vĩnh viễn) đề vào một scope mới đi cho đến khi gặp từ khóa kết thúc
end
của class, kết thúc một sope.
2. Break Scope gates
Vậy làm thế nào để không có rào cản các biến local giữa các Scope gates giữa module, class, method? Có thể dùng một trong những cách sau:
- class:
Class.new
- module:
Module.new
- method:
define_method
Xét ví dụ sau:
var1 = 1
Student = Class.new do
var2 = 2
p local_variables
define_method(:info) do
var3 = 3
p local_variables
end
end
2.0.0-p598 :010 > st = Student.new
=> #<Student:0x000000017dd8f8>
2.0.0-p598 :011 > st.info
[:var3, :var2, :var1, :_]
=> [:var3, :var2, :var1, :_]
Có thể thấy được 3 biến local đã được in ra khác với ví dụ trước, Đó là do trong ví dụ ta đã sử dụng break scope gates
3. Block
Block cũng là Scope Gates. Xét ví dụ sau:
2.0.0-p598 :001 > temp = :some_thing
=> :some_thing
2.0.0-p598 :002 > [1,2,3].each do |i| # bắt đầu block scope
2.0.0-p598 :003 > puts temp
2.0.0-p598 :004?> new_var = 'new varialbe'
2.0.0-p598 :005?> end
some_thing
some_thing
some_thing
=> [1, 2, 3]
2.0.0-p598 :006 > p temp
:some_thing
=> :some_thing
2.0.0-p598 :007 > p new_var
NameError: undefined local variable or method `new_var' for main:Object
from (irb):7
Có thể thấy new_var
là một biến local của block và nó chỉ được sử dụng ở bên trong block.
Xét biến temp
nó được in ra và không có một lỗi nào, vậy biến temp
có thể được thay đổi giá trị bên trong scope của block.
xét ví dụ tiếp
2.0.0-p598 :008 > t2 = 1
=> 1
2.0.0-p598 :009 > [1,2,3].each do |i|
2.0.0-p598 :010 > t2 += i
2.0.0-p598 :011?> end
=> [1, 2, 3]
2.0.0-p598 :012 > t2
=> 7
Làm thế nào để các biến bên trong block không thay đổi các biến bên ngoài scope đó? block-local variable có thể giải quyết vấn đề đó
Để định nghĩa block-local variable chỉ cần đặt các biến đó sau dấu ;
của block parameters
2.0.0-p598 :017 > t2 = 1
=> 1
2.0.0-p598 :018 > [1,2,3].each do |i; t2|
2.0.0-p598 :019 > t2 = 10
2.0.0-p598 :020?> end
=> [1, 2, 3]
2.0.0-p598 :021 > t2
=> 1
Lúc này t2 không làm thay đổi giá trị của các biến local bên ngoài scope của block.
IV. Các ví dụ
1 class Student
2 p self
3 def info
4 p self
5 end
6 end
self
ở dòng 2 là Student
vì nó nằm trong scope của class Student
còn self
ở dòng 4 là một thể hiện của class Student
Ví dụ tiếp theo
1 outside = :hello
2 class Student
3 ag = 10
4 def info
5 name = :my_name
6 end
7 end
8 outside1 = :world
9 p local_variables
10
Kết quả [:outside, :outside1]
tại sao lại như vậy? là vì khi đi vào class Student
Scope mới được mở ra đồng thời scope của class Student
sẽ tạm thời bị clean đi, sau khi kết thúc class Student lúc này sẽ đóng scope của Student
lại đồng thời mở lại scope của top level
Ví dụ tiếp
Hãy sửa lại sao cho đoạn code này hết lỗi
name = "Maria"
def info
"Her name is #{name}"
end
info()
Áp dụng break scope gates để giải quyết
name = "Maria"
define_method(:info) do
"Her name is #{name}"
end
info()
Tương tự với ví dụ này bạn có thể dùng break scope để refactor codes Class.new
her_name = "Maria"
class Student
p her_name
end
References
- Metaprogramming ruby
- http://sitepoint.com
All rights reserved