ตัวแปรคลาสของ Ruby ทำให้เกิดความสับสน แม้แต่ผู้ใช้ Ruby ที่เชี่ยวชาญก็พบว่าพวกเขาเข้าใจยาก ตัวอย่างที่ชัดเจนที่สุดเกี่ยวกับความเฉื่อย:
class Fruit
@@kind = nil
def self.kind
@@kind
end
end
class Apple < Fruit
@@kind = "apple"
end
Apple.kind
# => "apple"
Fruit.kind
# => "apple"
เปลี่ยน kind
ตัวแปรในคลาสลูกจะเปลี่ยนบนพาเรนต์ด้วย มันค่อนข้างเลอะเทอะ แต่นี่เป็นเพียงวิธีการทำงานของภาษา มันเป็นการตัดสินใจในการออกแบบมานานแล้วเพื่อเลียนแบบ Smalltalk
ยิ่งแย่ลงไปอีก
มีตัวอย่างอื่น ๆ ของความแปลกประหลาดของตัวแปรคลาสที่ดูเหมือนจะไม่ใช่ตัวเลือกทางสถาปัตยกรรมมากเท่ากับความไม่ชอบมาพากลในการใช้งาน วันนี้ผมจะมาพูดถึงเรื่องหนึ่งที่น่าสนใจบ้าง
เราจะเปรียบเทียบโค้ดสองส่วนในตอนนี้ ดูเหมือนว่าควรให้ผลลัพธ์ที่เหมือนกันแต่ไม่เป็นเช่นนั้น
ในตัวอย่างแรกของเรา เราตั้งค่าตัวแปรคลาสและสร้างวิธีการส่งคืน ไม่มีอะไรแฟนซีเกิดขึ้นที่นี่อย่างแน่นอน และทุกอย่างก็เป็นไปตามคาด
class Foo
@@val = 1234
# This is shorthand for declaring a class method.
class << self
def val
@@val
end
end
end
Foo.val
# => 1234
บางทีคุณอาจไม่รู้เรื่องนี้ แต่ class << self
ไม่จำเป็นต้องอยู่ในคำจำกัดความของคลาส ในตัวอย่างด้านล่าง เราได้ย้ายออกแล้ว มีการเพิ่มเมธอดของคลาสแต่ไม่สามารถเข้าถึงตัวแปรคลาสได้
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
เมื่อเราพยายามเข้าถึงตัวแปรคลาสจากฟังก์ชันของเรา เราจะได้รับคำเตือนและข้อยกเว้น เกิดอะไรขึ้น?
ป้อนขอบเขตคำศัพท์
ฉันมั่นใจมากขึ้นเรื่อยๆ ว่าขอบเขตคำศัพท์เป็นที่มาของ 99% ของแง่มุมที่แปลกประหลาดและน่าสับสนของ Ruby
ในกรณีที่คุณไม่คุ้นเคยกับคำศัพท์ การกำหนดขอบเขตศัพท์หมายถึงการจัดกลุ่มสิ่งต่าง ๆ เข้าด้วยกันโดยพิจารณาจากตำแหน่งที่ปรากฏในโค้ด ไม่ใช่ตำแหน่งที่อยู่ในโมเดลวัตถุนามธรรม ง่ายกว่ามากที่จะแสดงตัวอย่าง:
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
คลาสถูกกำหนดแบบ Lexically
ขอบเขตคำศัพท์ในตัวอย่างตัวแปรคลาสของเราเป็นอย่างไร
ในการดึงตัวแปรคลาส Ruby ต้องรู้ว่าคลาสใดที่จะรับมัน ใช้การกำหนดขอบเขตคำศัพท์เพื่อค้นหาคลาส
หากเราดูตัวอย่างการทำงานอย่างใกล้ชิดมากขึ้น เราจะเห็นว่ารหัสที่เข้าถึงตัวแปรคลาสนั้นมีอยู่จริงในคำจำกัดความของคลาส
class Foo
class << self
def val
# I'm lexically scoped to Foo!
@@val
end
end
end
ในตัวอย่างที่ไม่ทำงาน โค้ดที่เข้าถึงตัวแปรคลาสไม่ได้กำหนดขอบเขตตามคำศัพท์สำหรับคลาส
class << Bar
def val
# Foo is nowhere in sight.
@@val
end
end
แต่ถ้ามันไม่ได้กำหนดขอบเขตศัพท์ตามคลาส มันกำหนดขอบเขตไว้เพื่ออะไร คำเตือนที่ Ruby พิมพ์ทำให้เรามีเงื่อนงำ:warning: class variable access from toplevel
.
ในตัวอย่างที่ไม่ทำงาน ปรากฎว่าตัวแปรคลาสถูกกำหนดขอบเขตตามคำศัพท์ไปยังออบเจกต์ระดับบนสุด ซึ่งอาจทำให้เกิดพฤติกรรมแปลก ๆ ได้
ตัวอย่างเช่น หากเราพยายามตั้งค่าตัวแปรคลาสจากโค้ดที่มีการกำหนดขอบเขตศัพท์เป็น main ตัวแปรคลาสจะถูกตั้งค่าเป็น 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]
ตัวอย่างเพิ่มเติม
มีหลายวิธีในการอ้างอิงตัวแปรคลาสนอกขอบเขตคำศัพท์ของคลาส พวกเขาทั้งหมดจะทำให้คุณลำบาก
นี่คือตัวอย่างบางส่วนเพื่อความเพลิดเพลินของคุณ:
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
และตอนนี้คุณก็รู้แล้วว่าทำไมทุกคนถึงบอกว่าอย่าใช้ตัวแปรคลาสใน Ruby :)