ยินดีต้อนรับสู่ตอนใหม่ของ Ruby Magic! ฉบับเดือนนี้มีเนื้อหาเกี่ยวกับเมตาคลาส ซึ่งเป็นหัวข้อที่จุดประกายจากการสนทนาระหว่างนักพัฒนาสองคน (สวัสดี ม็อด!)
เราจะเรียนรู้วิธีการทำงานของคลาสและอินสแตนซ์ใน Ruby ผ่านการตรวจสอบเมตาคลาส ระหว่างทาง ค้นพบความแตกต่างระหว่างการกำหนดวิธีการโดยผ่าน "definee" ที่ชัดเจนและการใช้ class << self
หรือ instance_eval
. ไปกันเถอะ!
อินสแตนซ์ของคลาสและวิธีการอินสแตนซ์
เพื่อให้เข้าใจว่าเหตุใดจึงใช้ metaclasses ใน Ruby เราจะเริ่มต้นด้วยการพิจารณาว่าเมธอดของอินสแตนซ์และคลาสแตกต่างกันอย่างไร
ใน Ruby คลาส เป็นวัตถุที่กำหนดพิมพ์เขียวเพื่อสร้างวัตถุอื่นๆ คลาสกำหนดวิธีการที่มีอยู่ในอินสแตนซ์ของคลาสนั้น
การกำหนดเมธอดภายในคลาสจะสร้าง เมธอดอินสแตนซ์ ในชั้นเรียนนั้น อินสแตนซ์ในอนาคตของคลาสนั้นจะมีวิธีนั้นอยู่
class User
def initialize(name)
@name = name
end
def name
@name
end
end
user = User.new('Thijs')
user.name # => "Thijs"
ในตัวอย่างนี้ เราสร้างคลาสชื่อ User
ด้วย วิธีอินสแตนซ์ ชื่อ #name
ที่ส่งคืนชื่อผู้ใช้ เมื่อใช้คลาส เราจะสร้าง ตัวอย่างคลาส และเก็บไว้ในตัวแปรชื่อ user
. ตั้งแต่ user
เป็นตัวอย่างของ User
คลาสก็มี #name
วิธีการใช้ได้
คลาสเก็บเมธอดของอินสแตนซ์ไว้ใน ตารางเมธอด . อินสแตนซ์ใดๆ ของคลาสนั้นอ้างอิงถึงตารางเมธอดของคลาสเพื่อเข้าถึงเมธอดของอินสแตนซ์
คลาสออบเจ็กต์
วิธีการเรียน เป็น method ที่เรียกใช้ได้โดยตรงใน class โดยไม่ต้องสร้าง Instance ก่อน วิธีการเรียนถูกสร้างขึ้นโดยนำหน้าชื่อด้วย self.
เมื่อกำหนด
คลาสนั้นเป็นวัตถุ ค่าคงที่หมายถึงอ็อบเจ็กต์คลาส ดังนั้นเมธอดของคลาสที่กำหนดไว้จึงสามารถเรียกได้จากทุกที่ในแอปพลิเคชัน
class User
# ...
def self.all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]
เมธอดที่กำหนดด้วย self.
- คำนำหน้าจะไม่ถูกเพิ่มลงในตารางวิธีการของคลาส พวกมันจะถูกเพิ่มใน metaclass ของคลาสแทน
เมตาคลาส
นอกเหนือจากคลาส แต่ละอ็อบเจ็กต์ใน Ruby มี metaclass ที่ซ่อนอยู่ Metaclasses เป็น singletons ซึ่งหมายความว่าเป็นของวัตถุเดียว หากคุณสร้างหลายอินสแตนซ์ของคลาส คลาสเหล่านั้นจะแชร์คลาสเดียวกัน แต่ทั้งหมดจะมีเมตาคลาสแยกกัน
thijs, robert, tom = User.all
thijs.class # => User
robert.class # => User
tom.class # => User
thijs.singleton_class # => #<Class:#<User:0x00007fb71a9a2cb0>>
robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>>
tom.singleton_class # => #<Class:#<User:0x00007fb71a9a2c10>>
ในตัวอย่างนี้ เราจะเห็นว่าแม้ว่าแต่ละอ็อบเจ็กต์จะมีคลาส User
คลาสซิงเกิลตันมีรหัสอ็อบเจ็กต์ต่างกัน ซึ่งหมายความว่าเป็นออบเจ็กต์ที่แยกจากกัน
ด้วยการเข้าถึงเมตาคลาส Ruby อนุญาตให้เพิ่มเมธอดไปยังอ็อบเจ็กต์ที่มีอยู่ได้โดยตรง การทำเช่นนี้จะไม่เพิ่มวิธีการใหม่ให้กับคลาสของวัตถุ
robert = User.new("Robert")
def robert.last_name
"Beekman"
end
robert.last_name # => "Beekman"
User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>)
ในตัวอย่างนี้ เราเพิ่ม #last_name
ให้กับผู้ใช้ที่เก็บไว้ใน robert
ตัวแปร. แม้ว่า robert
เป็นตัวอย่างของ User
, อินสแตนซ์ที่สร้างขึ้นใหม่ของ User
จะไม่สามารถเข้าถึง #last_name
เมธอด เนื่องจากมีเฉพาะใน robert
metaclass ของ
อะไรคือ self.
?
เมื่อกำหนดเมธอดและส่งตัวรับ เมธอดใหม่จะถูกเพิ่มในเมตาคลาสของตัวรับ แทนที่จะเพิ่มลงในตารางเมธอดของคลาส
tom = User.new("Tom")
def tom.last_name
"de Bruijn"
end
ในตัวอย่างข้างต้น เราได้เพิ่ม #last_name
โดยตรงบน tom
วัตถุ โดยส่ง tom
เป็นผู้รับเมื่อกำหนดวิธีการ
นี่เป็นวิธีการทำงานของคลาสด้วย
class User
# ...
def self.all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
ที่นี่เราส่งผ่าน self.
. อย่างชัดเจน เป็นผู้รับเมื่อสร้าง .all
กระบวนการ. ในนิยามคลาส self.
หมายถึงคลาส (User
ในกรณีนี้) ดังนั้น .all
เพิ่มเมธอดใน User
เมตาคลาสของ
เพราะ user
เป็นอ็อบเจ็กต์ที่เก็บไว้ในค่าคงที่ เราจะเข้าถึงอ็อบเจ็กต์เดียวกัน—และ metaclass เดียวกัน—ทุกครั้งที่เราอ้างอิงถึงมัน
การเปิด Metaclass
เราได้เรียนรู้ว่าเมธอดของคลาสคือเมธอดในเมตาคลาสของอ็อบเจ็กต์คลาส เมื่อรู้อย่างนี้แล้ว เราจะมาดูเทคนิคอื่นๆ ในการสร้างวิธีการเรียนที่คุณอาจเคยเห็นมาก่อน
class << self
แม้ว่ามันจะดูออกแนวไปหน่อย แต่บางไลบรารีก็ใช้ class << self
เพื่อกำหนดวิธีการเรียน เคล็ดลับไวยากรณ์นี้เปิด metaclass ของคลาสปัจจุบันและโต้ตอบกับคลาสโดยตรง
class User
class << self
self # => #<Class:User>
def all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]
ตัวอย่างนี้สร้างวิธีการเรียนชื่อ User.all
โดยเพิ่ม method เข้าไปที่ User
เมตาคลาสของ แทนที่จะส่งผ่านผู้รับสำหรับเมธอดดังที่เราเห็นก่อนหน้านี้ เราตั้งค่า self.
ถึง user
metaclass ของแทน User
นั่นเอง
ดังที่เราได้เรียนรู้ก่อนหน้านี้ นิยามเมธอดใดๆ ที่ไม่มีตัวรับที่ชัดเจน จะถูกเพิ่มเป็นเมธอดอินสแตนซ์ของคลาสปัจจุบัน ภายในบล็อกคลาสปัจจุบันคือ User
metaclass ของ (#<Class:User>
)
instance_eval
อีกทางเลือกหนึ่งคือการใช้ instance_eval
ซึ่งทำสิ่งเดียวกันกับความแตกต่างที่สำคัญอย่างหนึ่ง แม้ว่า metaclass ของคลาสจะได้รับเมธอดที่กำหนดไว้ในบล็อก self
ยังคงเป็นข้อมูลอ้างอิงถึงคลาสหลัก
class User
instance_eval do
self # => User
def all
[new("Thijs"), new("Robert"), new("Tom")]
end
end
end
User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]
ในตัวอย่างนี้ เรากำหนดวิธีการอินสแตนซ์บน User
metaclass เหมือนเดิม แต่ self.
ยังคงชี้ไปที่ User
. แม้ว่าโดยทั่วไปจะชี้ไปที่วัตถุเดียวกัน แต่ "default definee" และ self
สามารถชี้ไปที่วัตถุต่างๆ ได้
สิ่งที่เราได้เรียนรู้
เราได้เรียนรู้ว่าคลาสเป็นอ็อบเจ็กต์เดียวที่สามารถมีเมธอดได้ และเมธอดของอินสแตนซ์นั้นเป็นเมธอดบน metaclass ของอ็อบเจ็กต์ เรารู้ว่า class << self
เพียงสลับ self.
เพื่อให้คุณสามารถกำหนดเมธอดบน metaclass ได้ และเรารู้ว่า instance_eval
ทำสิ่งเดียวกันเป็นส่วนใหญ่ (แต่ไม่แตะ self.
)
แม้ว่าคุณจะไม่ได้ทำงานกับ metaclasses อย่างชัดแจ้ง แต่ Ruby ก็ใช้มันอย่างกว้างขวางภายใต้ประทุน การรู้ว่าจะเกิดอะไรขึ้นเมื่อคุณกำหนดเมธอดจะช่วยให้คุณเข้าใจว่าทำไม Ruby ถึงมีพฤติกรรมแบบนั้น (และทำไมคุณต้องเติมเมธอดของคลาสด้วย self.
)
ขอบคุณที่อ่าน. หากคุณชอบสิ่งที่คุณอ่าน คุณอาจต้องการสมัครรับ Ruby Magic เพื่อรับอีเมลเมื่อเราเผยแพร่บทความใหม่เดือนละครั้ง