Scope (context) trong ruby

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òn var2 nằm trong scope của method init, tương tự khi gọi a.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


All Rights Reserved