Ruby 在 Class 中混入 Module 的实例变量

simlegate · 2013年07月24日 · 最后由 blackanger 回复于 2013年07月26日 · 7533 次阅读
module N
  attr_accessor :v
end
class C
  include N

#  def initialize
#    @v = 1
# end
  def p_var
     p @v
  end
end

我想在N中初始化我@v而不是C中 应该怎么做?

首先你应该明白,module 是 Ruby 中实现多继承的一个方式。 既然是一个继承,那么可以这样:

module N attr_accessor :v

def initialize @v = "hello" end end

class C include N def initialize super end end

c = C.new c.v

class C 里面的 initialize 方法可省,这里是为了说明,你还可以在 class C 中添加其他的变量。

#1 楼 @blackanger 这样实现有个弊端:

  1. 当 C 中没有 initialize 时,这样实现可行
  2. 当 C 中有 initialize 是,必须 super 一下 这样感觉@v的初始化太依赖 C,我想实现 C 和 N 的之间不要有太紧密的联系。
module N
    attr_accessor :v
    def self.included(base)
      base.instance_eval do
        define_method :initialize do
          @v = 'hello'
        end
      end
    end
end

class C
   include N
end

c = C.new
puts c.v

@simlegate 你看看 这样能不能满足你的 需求..

#3 楼 @meeasyhappy 目前看这种可以满足需求。 对 instance_eval 和 class_eval 了解的还是不够深入啊。

多看、多用,就会深入了...

呵呵,不用谢,共同进步...

#7 楼 @meeasyhappy 我发现你的实现方式和#1 的没有差别,就是动态为 C 定义了 initialize 方法。

# 当我
class C
   include N

   def initialize
   end 
end

c = C.new
puts c.v  # => nil

对,只是 将代码实现 放到了 module N 中。

#9 楼 @meeasyhappy 所以当我在 C 中定义 initialize,动态定义的 initialize 就会被覆盖掉。

确实是 这样的,怎么设计 要看你的应用场景了。

@simlegate module 和 class 的关系就是 class 继承了 module,不太明白你说的“不想 module 和 class 之间有很紧密的关系”,而且我的方法,也不依赖于 C class 啊,你可以把初始变量都放到 module 里面,C class 里面可以放只是和 C 相关的初始变量,M 里面放公共的初始变量,假如你还有个 Class D,include M 之后,也可以使用@v

「补充」:如果你用@meeasyhappy那种方式,利用了 included 回调来动态定义方法,除外。

@simlegate 如果你不想 module 和 class 之间有很紧密的关系,只能重新开发设计一款 Ruby 了。XD

[修改] @meeasyhappy 的方法挺妙。

毕竟 mixin 不是继承,当又有 include 又有继承的时候,super 调到哪里就比较难弄清楚了

笨办法:把 N 的初始化改个名字

module N
  attr_accessor :v
  def init_n
    @v = ...
  end
end

class C
  include N
  def initialize
    ...
    init_n
    ...
  end
end

#15 楼 @luikore 哥们,Mix-in 是 Ruby 实现多继承的方式,super 的调用肯定是上一层的同名方法了(include 的那个模块),怎么会不知道调用到哪呢?在同时有继承的时候,可以查看一下那个类的 ancestors 方法,就可以知道继承关系了。

#16 楼 @blackanger

当有多个 "上一层" 的时候,super 的语义就很牵强了

class C < B
  include N
  include M
end

mixin 更倾向于用组合 (composition) 而非继承的方式去理解问题,例如:

module Hand
end

module Leg
end

module Doll
  include Hand
  include Leg
end

Hand 是 Doll 的一部分,你偏要说 Doll 继承了 Hand 不就很奇怪么...

#17 楼 @luikore 奇怪啥,再复杂的组合,使用 ancestors 就知道 super 继承自哪了。

module Hand end

module Leg end

module Doll include Hand include Leg
end

class A end

class B < A include Doll end

B.ancestors #=> [B, Doll, Leg, Hand, A, Object, Kernel]

不是继承是啥?请问哪里牵强?Matz 自己都说了,Mix-in 是实现了多继承,啥叫组合? 请问你组合的思维如何理清他们的关系?

#17 楼 @luikore 当然,我可以理解,你在 15 楼的方案,以“组合”的概念解决问题很漂亮。 我比较认同你,“组合”思维来组织代码,来使用 Mix-in。 但 Mix-in 的本质还是多继承。

#18 楼 @blackanger

Ruby 里说到的继承就是指 class A < B 这种语法结构,和 mixin 不同... 当多继承用只是 mixin 的一个用途而已,但是这个 "多继承" 和 Ruby 语法里的 "继承" 不是一回事,这么讲会产生混淆的...

super 的一个问题就是菱形继承

module A
  def initialize
    puts 'a'
  end
end

module B
  include A
end

module C
  include A
  def initialize
    puts 'c'
  end
end

class D
  include C
  include B
  def initialize
    super # 明显? 谁记住父类方法查找是深度优先还是广度优先?
  end
end

另一个隐患是 open class, 在上面的代码之外,再加上这个:

module E
  def initialize
    puts 'e'
  end
end

class D
  include E
end

如果这两段代码载入顺序不同,结果也不同...

#20 楼 @luikore 首先,我说的是多重继承,不是继承(单一),是两个概念。

D.ancestors => [D, B, C, A, Object, PP::ObjectMixin, Kernel, BasicObject]

使用 ancestors 方法,可以看到方法查找的顺序,super 肯定是调用 C 的方法。

如果再加一个 E module,同样,D.ancestors 可以看到: [D, E, B, C, A, Object, PP::ObjectMixin, Kernel, BasicObject]

这次是调用 E 类的方法。

这和载入顺序没有关系,是和 ancestors 以及方法查找有关系的。

当你看到 ancestors 继承树,难道还能被所谓的菱形问题困扰吗? 这方面,可以参考《松本行弘的程序世界》一书中,Matz 对 Mix-in 的论述。

我明白你说的,咱俩说的是两个层面:一个是语言实现的本质,mixin 要解决的问题,一个是你作为程序员该如何使用,毕竟你把 initialize 方法换个名字 init_n 和 super 也没啥区别。但是如果了解这个实质,大家也不会有那种困扰了,但是为了代码的可读,使用你的方法很好。总的来说,和你讨论,受益良多。

#21 楼 @blackanger

class B < A
  include C
end

这个里用 super 就已经很混乱了 而且 C 里可能用了ActiveSupport::Concern之类的东西,难道你每次调用都翻遍 ancestors

从语义上 mixin 不是继承,不要从实现来理解

@jjym 好吧,随便你怎么理解吧。我也不是和谁去辩论和抬杠。

那你告诉我语义上 MixIn 是什么,组合?组合该怎么理解,你如何写出一个组合的代码?组合如何求解 @luikore 所说的那种菱形问题?

再一个,我说的是多重继承,不是继承(单继承那种继承,Ruby 里说的继承是指单继承)。是 Matz 的《松本行弘的程序世界》里告诉我的,Matz 不会写书瞎说吧?

所谓的混乱,只是在使用的时候,我们自己把自己搞混。你们说的组合,只是编程方法而已。把 initiliaze 方法换成 init_n,只是在可读性,维护性上更好了,我也认同@luikore的那种写法。我只是在说明 Mix-in 的行为是什么,而@luikore说不是多继承,如果不是多继承,他的例子里 class C 的 initialize 方法中调用的 init_n 哪来的?本身多继承和单继承其实也是同一个概念,就是继承嘛。

ActiveSupport::Concern 这种东西是 Rails 里实现的语法糖。

#24 楼 @jjym 讨论没有对错。只要能把正确的东西传达出来就行。

@luikore 的那种解决方案在避免 moudle 混入时候出现对于开发者的困扰。 而我是从另一个角度来说 Mix-in。

也没有谁对谁错。看大家怎么理解吧。

#3 楼 @meeasyhappy 请教这段代码

def self.included(base)
    base.instance_eval do
      define_method :initialize do
        @v = 'hello'
      end
    end
 end

为什么使用 instance_eval 和 class_eval 结果是一样的?C.instance_eval 不是定义的类方法?

#26 楼 @alan90121 define_method定义的是实例方法。

28 楼 已删除

@simlegate @alan90121 你俩人的回答真诡异。 [修改] define_method 是定义的 initialize 方法啊。 instance_eval 可以在 api 里去看看说明:http://ruby-doc.org/core-2.0/BasicObject.html#method-i-instance_eval Evaluates a string containing Ruby source code, or the given block, within the context of the receiver (obj).

是在 receiver 的上下文执行一段文本。

在 module 里用那个 instance_eval 实际上只是用了 included 回调方法,而 initialize 方法是定义到 Class C 里面的。这样的话,就和我 1 楼的例子不是同一个概念了,因为@meeasyhappy的方法是在 include 的时候利用回调函数动态定义了方法。

不得不再一次感慨,Ruby 真灵活!

#30 楼 @blackanger 好吧,我承认我把实例方法打成实例变量了。

在使用 include Mix-in 模块的时候,确实存在顺序问题,但也无法改变 Mix-in 是多重继承这个事实啊。

在 Ruby2.0 里新增的 prepend 方法(组合的概念怎么理解这个?),是可以把一个 module 变成类的子类的(在继承树关系上)。

所以,说的更精确一点就是: Ruby 中用 Mix-in 操作继承树来解决多继承的问题,正如 Matz 所说,Mix-in 让多继承的类结构变的简单了可以很确定的说,Matz 设计 Mix-in 的这种机制,就是为了让程序员可以像单继承那样去使用多继承

至于你怎么利用 Mix-in,那就是你自己的事情了。

[增加 prepend 的代码]



class Foo
  def do_stuff
    puts "doing stuff"
  end
end

module Wrapper
  def do_stuff
    puts "before stuff"
    super
    puts "after stuff"
  end
end

class Foo
  prepend Wrapper
end

Foo.new.do_stuff 

#32 楼 @blackanger 我问点的是下面这段代码与@simlegate 的区别:

def self.included(base)
     base.instance_eval do
       def initialize do
         @v = 'hello'
       end
     end
  end

具体是这个帖子:http://ruby-china.org/topics/7854 忘记 define_method 确实是定义实例方法的,@simlegate 提醒了我。

#33 楼 @alan90121 嗯。刚开始 simlegate 打错字了,实例方法打成了实例变量,所以我看着有点奇怪。 😄

比较倾向于把 module 单纯的当做 methods 的集合,Mixin 只是混入一些操作,至于维护状态的事情还是留给 Class 来做就。 如果实在需要 Module 维护状态,就显式定义一个 class macro, 在被混入的类里面明确调用,比如act_as_taggable 多继承的说法,让我想起了 C++ 里面的多继承关系,那叫一个纠结啊....

#35 楼 @nickelchen 去看 Matz 的《松本行弘的程序世界》一书吧,从 Matz 如何设计 Ruby 里面可以看出,Mix-in 正好解决了传统的多继承的问题,降低了复杂性,就像我楼上说的,Matz 设计 Mix-in 的这种机制,就是为了让程序员可以像单继承那样去使用多继承。所以不要和 C++ 的多继承去比,如果一定要比,Ruby 的实现和 Java 的更类似。

只有理解了这个实质,写代码的时候才不容易被 Mix-in 搞晕啊。

#36 楼 @blackanger 我觉得你是一个很认真的,也很仔细的人。凡是都会知根知底,这种精神值得我们学习啊。

#37 楼 @simlegate 因为 Ruby 太灵活了啊,如果不把握一些实质,很容易被 Ruby 搞晕。所以我推荐去看 Matz 的书,看他设计 Ruby 的初衷和原则,对于我们理解 Ruby 里面的概念很有帮助。这可比看源码实际多了。

#38 楼 @blackanger 对,这本书我也大概看了一下,但是没有去仔细研究。 但不主要是讲 Ruby。

#39 楼 @simlegate 多看几遍吧,尤其是前几章讲他设计 Ruby 的过程。这本书不是教你 Ruby 语法的。比如第 30 页和 31 页,你就可以看到 Mix-in 和多重继承的关系,引导你解决一些问题。你就不会在 module 和 class 的关系上感到迷惑了。「原文:对 Mix-in 的理解是,Mix-in 只不过是实现多重继承的一个技巧而已。Ruby 只支持 Mix-in 形式的多重继承。Matz 也说了,曾有一个著名的青年学者因为 Ruby 的原因而误认为多重继承和 Mix-in 是不同的概念,这个让 Matz 有点惭愧,他觉得这些误解部分是他造成的,哈哈。」

还有很多地方: 「Mix-in 是使类结构变的简单的优秀技术。Mix-in 使多重继承的类形成非常清晰的树结构,没有变成网状结构。」如果非要使用组合的概念去理解 Mix-in,正好和 Matz 设计 Mix-in 的初衷相悖,让 Matz 情以何堪。

module N
  attr_accessor :v
  @v = 'xx' #这样可以吗?  
end

#21 楼 @blackanger

和载入顺序当然有关系了,先载入 E 的话,ancestors 顺序就不同了。我说的是编程时不好确定调用到哪里,你说的是运行时这个东西是确定的,不是一回事...

你看,当多继承去理解的话,首先得澄清"继承"的概念,要说明计算机里的"继承"和分类学中的概念不同,它还可以泛化到整体 < 部分之类的用途。然后还要澄清多继承的概念,区分好规格继承和实现继承。再然后要解释为什么 Ruby 被分类为单继承的语言 (因为 Ruby 规格单继承,实现多继承). 再然后要解释清楚为什么不直接用 mixin 代替单继承的语法 (mixin 的四个限制)... matz 的书就用了大半章去澄清这里的坑...

同样是 matz 的书里写的 (35 页):

多重继承相当于语言功能支持模块组合

所以绕了一个圈又跑回来变成 composition 啦...

我觉得这里没有谁更本质的地方,你可以把 mixin 中各部分态射到多继承中的对应概念,表明两者实质上没有不同,但就组合去理解对写出来的代码没影响,而且对面向对象设计提供了便利的准则:分类就用规格继承,差分/组合就用 mixin

#41 楼 @sevk 那是类实例变量

#42 楼 @luikore 是的,载入顺序影响了 ancestors 的继承树。这样讲也行。但对我来说,理解为多重继承要比组合更为清晰。看大家怎么理解了。

#45 楼 @blackanger

继承这个词含义太多了,我还是希望避免增加它的外延... 就像这个帖子的讨论一样,我说 mixin 不是 (规格) 继承,你说 mixin 是 (实现) 继承,没意义...

module Foo
  attr_accessor :foo
  def foo
    @foo ||= 'zzz'
  end
end
class Bar
  include Foo
end

#46 楼 @luikore 本来就是继承嘛,为什么要避免呢, 《松本行弘的程序世界》31 页里面 Matz 解释的很清楚嘛。或者说,Mix-in 是 Ruby 里实现多重继承的方式,而且通过 Mix-in 这种方式,降低了传统多重继承的复杂性,更进一步说,Mix-in 是经过 Matz 改良的 Ruby 里实现多重继承的方式。

你说 35 页里写:多重继承相当于语言功能支持模块组合。

原文里还有前面一句话,假如把类当模块来看的话。而且,那句话的章节标题就是:动态编程语言也需要多重继承。

现在是模块是类,Mix-in 模块算是一种抽象类。它只是有限制,都是 Class 类的子类。

Matz 那本书里第二章基本通篇在说继承和多重继承与 Mix-in 的。

我为什么要和你讨论呢,就是因为你说「Mix-in 不是继承」,可它明明就是继承,你为什么要避免增加它外延,我也没有增加其外延,我说的就是多重继承,多重继承不也是一种继承嘛。Ruby 里的继承一般是指类和类之间的关系,而类和多个模块的时候,就是 Mix-in,多重继承。

所以我说,Mix-in 的实质是多重继承的一个实现方式。这就是讨论的意义所在。😄

你说的这句,[我说的是编程时不好确定调用到哪里,你说的是运行时这个东西是确定的] ,我也认同,楼上就认同了,我说你阐述的是一种编程方法,但是你用编程方法来理解 Mix-in 的工作原理实现方式,我就不认同了。

而且,matz 也说了,继承就是利用模块的方法(34 页)。

我说 Mix-in 是一种继承,你说继承是一种组合方式。

关于这个问题,我不继续讨论了。毕竟每个人有每个人的理解,我只是传达了 Matz 在他书里的论述。大家能怎么理解就怎么理解吧。☺

@luikore 再补充一点:你在楼上举的那个例子,所谓的菱形问题,是个伪问题,Ruby 里根本不存在这样的问题。

多重继承是描述类之间的关系,而组合,描述的是对象之间的关系,当然,如果你只从方法集合这个层面来说,Mix-in 是一种组合方式,也能说的通。

继承是 is_a,组合是 has_a, 你把 Module 理解为一个方法集合,当然是组合了,这个类 include 了 module,它的对象就可以 has_a 这个方法,has_a 那个方法。

而且 Module 本身是 Class 的父类, Class.superclass #=> Module

include 也是在 Module 中被定义的私有方法,如果你 include 一个模块,那么会把这个模块加到这个类的继承树里,请问用组合的概念如何解释这一行为?

所以得出的结论是,如果你只把 Mix-in 当作一个方法集合,而不关注类和模块的关系,那可以理解为组合,但这只是表面,当然理解到这一层,不影响编写代码了。但可能会碰到奇怪的问题,比如你举的那种菱形问题,会让你迷惑。

真正理解了 Mix-in 是多重继承(不是传统的多重继承,继承关系是单一的,又有多重继承的共用方法,不要用 C++ 那种多重继承去理解)这个概念, 「你说的:编程时不好确定调用到哪里」,这种问题也不会存在了。

#48 楼 @blackanger "实质" 是你加的...

#51 楼 @luikore 是实质。你看 50 楼我留言吧。

#51 楼 @luikore 关键不在“实质”,可能咱俩对于“多重继承”在 Ruby 里的概念不同所致。Matz 实现的是,继承关系单一,又可以共用方法的多重继承。你可能用了 C++ 多重继承来理解我说的这个多重继承,所以你有菱形问题这么一说。

#50 楼 @blackanger

#20 楼 我举的两段代码如果分散在 a.rb 和 b.rb, 然后它们是由 c.rb 载入的,编程者就必须把这 3 个文件都看了然后想明白了才能知道真正的调用顺序,是真正影响程序的理解和可读性的问题。如果 c.rb 是用 Dir.glob '*.rb' 之类的方法来获取 a.rb 和 b.rb 的路径然后载入,甚至会因为文件系统的差别而导致行为的差别,是绝对不可忽视的坑...

如果像 rails 那样通过 const_missing 去载入,甚至会因为访问 url 的顺序不同而产生不同的结果,不是这么简单就能解决的事情。

而这种坑多多少少就是因为从继承的角度去设计模块造成的...

#54 楼 @luikore 恩,同意你说的这个坑。

其实,我说的就是,在 Ruby 里,你随便混入,他们都在一个继承树上,不会乱。 而你是提醒了别人,组合的时候注意载入顺序,或者使用定义可读的方法名来避免把程序员自己搞混。

「增加:」所以,我说的‘实质’就是,不管你怎么组合,他们都挂在一个继承树上。

剩下的,就看大家怎么去理解了。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号