後設程式設計¶
Crystal 中的後設程式設計與 Ruby 中不同。此頁面上的連結有望提供對這些差異的一些見解,以及如何克服它們。
Ruby 和 Crystal 之間的差異¶
Ruby 大量使用 send
、method_missing
、instance_eval
、class_eval
、eval
、define_method
、remove_method
等方法,以便在執行階段修改程式碼。它也支援 include
和 extend
,將模組新增至其他模組,以便在執行階段建立新的類別或實例方法。這就是這兩種語言之間最大的差異:Crystal 不允許在執行階段產生程式碼。所有 Crystal 程式碼都必須在執行最終二進位檔之前產生和編譯。
因此,上面列出的許多機制甚至不存在。在上面列出的方法中,Crystal 僅透過巨集機制支援 method_missing
。請閱讀關於巨集的官方文件以了解它們,但請注意,巨集用於在編譯步驟期間定義有效的 Crystal 方法,因此所有接收者和方法名稱必須事先知道。您不能從字串或符號建立方法名稱並將其 send
至接收者;不支援 send
,並且編譯將會失敗。
Crystal 確實支援 include
和 extend
。但是所有包含或擴展的程式碼都必須是有效的 Crystal 程式碼才能編譯。
如何將一些 Ruby 技巧轉換為 Crystal¶
但是,對於勇敢的後設程式設計師來說,一切並非絕望!Crystal 仍然具有強大的編譯時期程式碼產生功能。我們只需要稍微調整我們的 Ruby 技術,使其能在 Crystal 環境下運作。
透過 extend
覆寫 #new¶
在 Ruby 中,我們可以透過覆寫類別上的 new
方法來完成一些強大的事情。
module ClassMethods
def new(*args)
puts "Calling overridden new method with args #{args.inspect}"
# Can do arbitrary setup or calculations here...
instance = allocate
instance.send(:initialize, *args) # need to use #send since #initialize is private
instance
end
end
class Foo
def initialize(name)
puts "Calling Foo.new with arg #{name}"
end
end
foo = Foo.new('Quxo') # => Calling Foo.new with arg Quxo
p foo.class # => Foo
class Foo
extend ClassMethods
end
foo = Foo.new('Quxo')
# => Calling overridden new method with args ["Quxo"]
# => Calling Foo.new with arg Quxo
p foo.class # => Foo
如上面的範例所示,Foo
實例會呼叫其正常的建構函式。當我們 extend
它並覆寫 new
時,我們可以將各種東西注入到過程中。上面的範例顯示了最小的干擾,只是分配物件的實例並初始化它。此實例會從建構函式傳回。
在下一個範例中,我們會覆寫 new
並傳回完全不同類型的類別!
class Bar
def initialize(foo)
puts "This arg was an instance of class #{foo.class}"
end
end
module ClassMethods
def new(*args)
puts "Calling overridden new method with args #{args.inspect}"
Bar.new(allocate) # return a completely different class instance
end
end
class Foo
extend ClassMethods
def initialize(name)
puts "Calling Foo.new with arg #{name}"
end
end
foo = Foo.new('Quxo')
# => Calling overridden new method with args ["Quxo"]
# => This arg was an instance of class Foo
p foo.class # => Bar
這允許在執行階段進行非常強大的後設程式設計。我們可以將類別包裝在另一個類別中作為代理,並傳回對此新代理物件的參考。
同樣的魔法在 Crystal 中也有可能嗎?如果不可能,我不會寫這一節。但它確實有一些我們稍後會談到的注意事項。
以下是 Crystal 中的原始類別和預期的行為。
module ClassMethods
macro extended
def self.new(number : Int32)
puts "Calling overridden new added from extend hook, arg is #{number}"
instance = allocate
instance.initialize(number)
instance
end
end
end
class Foo
extend ClassMethods
@number : Int32
def initialize(number)
puts "Foo.initialize called with number #{number}"
@number = number
end
end
foo = Foo.new(5)
# => Calling overridden new added from extend hook, arg is 5
# => Foo.initialize called with number 5
puts foo.class # Foo
此範例使用了 macro extended
鉤子。每當類別主體執行 extend
方法時,就會呼叫此鉤子。我們可以使用此巨集來撰寫替代的 new
方法。
(需要釐清方法簽章的詳細資訊。從 Foo 中移除 @number 類型宣告會導致覆寫靜默失敗。將 "number : Int32" 新增至 Foo 類別初始化簽章也會導致覆寫失敗。這裡有一些我遺漏的方法重載細節。需要更多實驗。但上面的範例仍然有效...)
透過 method_missing
巨集產生方法¶
以下是一個非常簡單的範例,示範如何使用 method_missing
巨集,根據接收者 JSON 物件的鍵是否存在來建立遺失的方法
class Hashr
getter obj
def initialize(json : Hash(String, JSON::Any) | JSON::Any)
@obj = json
end
macro method_missing(key)
def {{ key.id }}
value = obj[{{ key.id.stringify }}]
Hashr.new(value)
end
end
def ==(other)
obj == other
end
end
如何使用 record
和產生的查找表來模擬 send
¶
範例程式碼 + 解釋
Crystal 的 alias_method
方法¶
有時我們想重新開啟一個類別並重新定義先前定義的方法,使其具有一些新的行為。此外,我們可能也希望原始方法仍然可以存取。在 Ruby 中,我們使用 alias_method
來達到此目的。範例
class Klass
def salute
puts "Aloha!"
end
end
Klass.new.salute # => Aloha!
class Klass
def salute_with_log
puts "Calling method..."
salute_without_log
puts "... Method called"
end
alias_method :salute_without_log, :salute
alias_method :salute, :salute_with_log
end
Klass.new.salute
# => Calling method...
# => Aloha!
# => ... Method called
在 Crystal 中執行相同的工作相當簡單。Crystal 提供了一個名為 previous_def
的方法,它可以存取先前定義的方法版本。為了讓相同的範例在 Crystal 中運作,它看起來會類似這樣
class Klass
def salute
puts "Aloha!"
end
end
# Reopen the class...
class Klass
def salute
puts "Calling method..."
previous_def
end
end
# Reopen it again for kicks!
class Klass
def salute
previous_def
puts "... Method called"
end
end
Klass.new.salute
# => Calling method...
# => Aloha!
# => ... Method called
每次我們重新開啟類別時,previous_def
都會設定為先前的定義方法,因此我們可以像在 Ruby 中一樣,在編譯時期使用它來建立別名方法鏈。但是,每次我們擴展鏈時,我們都會失去對原始方法定義的存取權。不像在 Ruby 中,我們為舊方法提供了一個明確的名稱,我們可以在其他地方引用它,Crystal 沒有提供這種功能。
一般資源¶
Ary Borenszweig (@asterite 在 gitter 上) 在 2016 年的會議上發表了一篇關於巨集的演講。可以在這裡觀看。