元编程¶
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
,编译将失败。
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
方法。
(需要澄清方法签名细节。删除 @number 类型声明 Foo 会导致覆盖静默失败。在 Foo 类初始化签名中添加 "number : Int32" 也会导致覆盖失败。这里有一些方法重载的细微差别是我没有注意到的。需要更多实验。上面的示例仍然有效......)
通过 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 年的一次会议上发表了关于宏的演讲。可以在这里观看。