Ruby中include和extend有什么区别?

483

我正在努力理解Ruby元编程。混合模块总是让我感到困惑。

  • include:将指定的模块方法作为实例方法混入目标类
  • extend:将指定的模块方法作为类方法混入目标类

那么主要区别只是这个吗?还是有更大的问题潜藏?例如:

module ReusableModule
  def module_method
    puts "Module Method: Hi there!"
  end
end

class ClassThatIncludes
  include ReusableModule
end
class ClassThatExtends
  extend ReusableModule
end

puts "Include"
ClassThatIncludes.new.module_method       # "Module Method: Hi there!"
puts "Extend"
ClassThatExtends.module_method            # "Module Method: Hi there!"

也可以查看这个链接:http://juixe.com/techknow/index.php/2006/06/15/mixins-in-ruby/ - Daniel Viglione
8个回答

399

extend 方法 - 将指定模块的方法和常量添加到目标元类(即单例类)中。例如:

  • 如果你调用 Klazz.extend(Mod),那么现在 Klazz 拥有了 Mod 的方法(作为类方法)。
  • 如果你调用 obj.extend(Mod),那么现在 obj 拥有了 Mod 的方法(作为实例方法),但是 obj.class 的其他实例没有增加这些方法。
  • extend 是一个公共方法。

include 方法 - 默认情况下,它将指定模块的方法作为实例方法混合进目标模块/类中。例如:

  • 如果你调用 class Klazz; include Mod; end;,那么 Klazz 的所有实例都可以访问 Mod 的方法(作为实例方法)。
  • include 是一个私有方法,因为它本意是要从容器类/模块内部调用。

然而,很多时候模块会通过猴子补丁重写 include 的行为。这在旧版 Rails 代码中非常突出。Yehuda Katz 的博客 提供了更多细节。

下面是 include 的一些详细信息,假设你已经执行了以下代码:

class Klazz
  include Mod
end
  • 如果Mod已经包含在Klazz或其祖先中,则include语句不起作用
  • 它还会将Mod的常量包含在Klazz中,只要它们不冲突即可
  • 它使Klazz可以访问Mod的模块变量,例如@@foo@@bar
  • 如果存在循环包含,则引发ArgumentError
  • 将该模块附加为调用者的直接祖先(即它将Mod添加到Klazz.ancestors中,但是Mod不会添加到Klazz.superclass.superclass.superclass的链中。因此,在Klazz#foo中调用super方法将在查找Klazz实际的超类的foo方法之前先检查Mod#foo。有关详细信息,请参见RubySpec)。

当然,ruby核心文档始终是了解这些内容的最佳途径。RubySpec项目也是一个很好的资源,因为它们精确地记录了功能说明。


35
我知道这篇文章很旧了,但回复的清晰度让我忍不住想留言。非常感谢提供如此好的解释。 - MohamedSanaulla
3
@anwar 当然,但现在我可以发表评论并成功找到了那篇文章。它在这里可用:http://aaronlasseigne.com/2012/01/17/explaining-include-and-extend/,我仍然认为该模式使理解更容易。 - systho
2
这个响应的重点是extend如何根据使用情况将方法应用为类方法或实例方法。Klass.extend=类方法,objekt.extend=实例方法。我总是(错误地)认为类方法来自于extend,而实例方法来自于include - Frank Koehl
以上所有内容都是正确的,只有一个小细节:extend在单例类的继承链上包含一个模块(有关详细信息,请参见此处)。此外,在@systho的评论中,文章中也有显示。 - undefined

283

你所说的是正确的。但是,情况比这更复杂。

如果你有一个类 Klazz 和模块 Mod,在 Klazz 中包含 Mod 可以让 Klazz 的实例访问 Mod 的方法。或者你可以使用 Mod 扩展 Klazz,使得该 Klazz 能够访问 Mod 的方法。但是你也可以使用 o.extend Mod 扩展任意对象。在这种情况下,即使所有其他与 o 属于相同类别的对象没有,该单独对象也会获得 Mod 的方法。


1
简洁如孔子。 - lkahtz

17

没错。

在幕后,include 实际上是 append_features 的别名,而它(来自文档):

Ruby 的默认实现是将这个模块的常量、方法和模块变量添加到 aModule 中,如果这个模块还没有添加到 aModule 或它的祖先中的任何一个中。


14

当你将一个模块包含到一个类中时,模块方法被导入为实例方法。

然而,当你将一个模块扩展到一个类中时,模块方法被导入为类方法。

例如,如果我们有一个定义如下的模块Module_test

module Module_test
  def func
    puts "M - in module"
  end
end

现在,针对include模块。如果我们按照以下方式定义类A

class A
  include Module_test
end

a = A.new
a.func

输出结果为:M - in module

如果我们将include Module_test这一行替换为extend Module_test并再次运行代码,我们会收到以下错误:undefined method 'func' for #<A:instance_num> (NoMethodError)

将方法调用a.func更改为A.func后,输出结果变为:M - in module

从上述代码执行中可以清楚地看出,当我们include一个模块时,它的方法成为实例方法,而当我们extend一个模块时,它的方法成为类方法


实例方法包括在实例方法中,而模块方法则不包括...来自https://ruby-doc.org/3.2.1/Module.html - ultrajohn

3
我发现了一篇非常有用的文章,比较了在类内部使用的includeextendprepend方法: include将模块方法添加为类的实例方法,而extend将模块方法添加为类方法。被包含或扩展的模块必须相应地定义。

3

所有其他答案都很好,包括挖掘RubySpecs的提示:

https://github.com/rubyspec/rubyspec/blob/master/core/module/include_spec.rb

https://github.com/rubyspec/rubyspec/blob/master/core/module/extend_object_spec.rb

关于用例:

如果你在类ClassThatIncludes中包含模块ReusableModule,则会引用方法、常量、类、子模块和其他声明。

如果你用模块ReusableModule扩展类ClassThatExtends,那么方法和常量会被复制。显然,如果不小心,动态复制定义可能会浪费很多内存。

如果你使用ActiveSupport::Concern,.included()功能可以直接重写包含的类。Concern中的module ClassMethods会被扩展(复制)到包含的类中。


3

我希望解释一下它的运作机制。如果我理解有误请指正。

当我们使用include时,我们将类和包含某些方法的模块连接起来。

class A
include MyMOd
end

a = A.new
a.some_method

对象没有方法,只有类和模块才有。

因此,当 a 接收到消息 some_method 时,它会在 a 的特有类、A 类以及与 A 相关联的模块(如果有的话)中查找方法 some_method(顺序为相反的顺序,最后一个包含的模块优先)。

使用 extend 方法时,我们在对象的特有类中添加了一个模块的链接。因此,如果我们使用 A.new.extend(MyMod),我们将把我们的模块的链接添加到 A 实例的特有类或 a' 类中。如果我们使用 A.extend(MyMod),我们将向 A 的特有类(类也是对象)A' 添加链接。

因此,a 的方法查找路径如下:

a => a' => 与a'类相关联的模块 => A

还有一种名为 prepend 的方法可以更改查找路径:

a => a' => 在A上前置的模块 => A => 包含在A中的模块

非常抱歉我的英语不好。


0
include提供了一个类,可以将模块的方法作为实例进行访问。
methods
module Plant
  def poisonous?
    true
  end
end

class PlantKingdom
end

class Hemlock < PlantKingdom
  include Plant
end

obj = Hemlock.new()

obj.poisonous?
=> true

Hemlock.poisonous?
=> NoMethodError: undefined method `poisonous?' for Hemlock:Class

extend提供了一个类,可以通过类方法访问模块的方法。
module Plant
  def poisonous?
    true
  end
end

class PlantKingdom
end

class Hemlock < PlantKingdom
  extend Plant
end

obj = Hemlock.new()

obj.poisonous?
=> NoMethodError: undefined method `poisonous?' for #<Hemlock:0x00007fa84389c748>

Hemlock.poisonous?
=> true

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接