Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เปิดเผยคลาส อินสแตนซ์ และ Metaclasses ใน Ruby

ยินดีต้อนรับสู่ตอนใหม่ของ 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 เพื่อรับอีเมลเมื่อเราเผยแพร่บทความใหม่เดือนละครั้ง