1. 打开类
可以重新打开已经存在的类(即使你不是这个类的创建者)并对之进行动态修改或者扩展功能,即使像Array和String这样标准库的类也不例外,这种行为称之为打开类
假设你想创建一个方法,功能是接受一个字符串参数,只保留字符串中数字和字母,代码如下
class String
def to_alphanumeric
#\w匹配单词,等同于 /[A-Za-z0-9_]/
#\s匹配空格,等同于 /[ \t\r\n\f]/
gsub(/[^\w\s]/, '')
end
end
puts "A&^ar$o%n&* (is&*&))) t&*(*he B0&*S**^S)".to_alphanumeric
# => "Aaron is the B0SS"
2.猴子补丁
如果你粗心地为某个类添加了新功能,同时覆盖了类原来的方法,进而影响到其他部分的代码,这种修改类的处理方法称之为猴子补丁(Monkeypatch)
我们写一个方法Array#replace(使用新字符替换数组中某个字符),但是Array类已经有replace方法(功能是用一个作为参数传入的数组替换当前数组的全部内容),这样就会把原有的方法覆盖掉,不太好,可能我们原本没打算覆盖原有的功能,代码如下
class Array
def replace(original, replacement)
self.map {|e| e == original ? replacement : e }
end
end
puts ['x', 'y', 'z'].replace('x', 'a')
# => a, y, z
3.对象模型
这看起来像一个复杂的图表,但是它清晰的表示出了 Ruby 中对象,类,模块之间的联系。有三个要点。
- 实例化的对象 (obj1,obj2,obj3) 是 MyClass 类的对象。
- MyClass 是属于 Class 的对象(这在 Ruby 里也是对象的意思,我知道,这让你很费解)。
- MyClass 是 Class 的类对象,同时它也继承自 Object。
我们还会在第二部分引用这个,现在我们继续看继承链。
继承链
当你调用一个方法时,Ruby 向右进入的接收者类,然后向上遍历继承链,直到找到被调用方法或者抵达尽头
在上图b是对象Book类的一个实例对象,Book类包含两个模块Printable 和 Document.同时Book类继承自Object类(ruby中几乎一切事物的基类),Object类又包含Kernel模块,同时继承自BasicObject类(ruby中一切对象的绝对父类)
prepend、include与祖先链
祖先链用于描述Ruby对象的继承关系,因为类与模块是父子关系,所以祖先链中也可以包含模块,prepend与include分别可以向链中添加模块,不同的是调用include方法,模块会被插入祖先链,当前类的正上方,而prepend同样是插入到祖先链,但位置却在当前类的正下方,另外通过Class.ancestors可以查看当前的祖先链
4. 动态定义方法(Module#define_method)
通过define_method方法取代了关键词def,其本质上都是相同的,只是在定义方式上,define_method的方式更加灵活一些,可以通过在编码中通过推导,完成函数的定义,增加了实现的灵活性。
示例代码如下:
class MyClass
define_method :my_method do |my_arg|
my_arg * 3
do
end
obj = MyClass.new
obj.my_method(2) #=> 6
ActivateRecord(rails中的默认ORM工具)中大量的使用了这种特性,例如
class Book < ActiveRecord::Base
end
b = Book.new
b.title
假定 Book 类是一个 Book 数据库表的一个 ORM 包装器,并且 title 就是这个表里的字段,那么我们就能得到由 b 表示的那个数据库里的 title 所指定的那一列。
原理是:即使我们在Book类中并没有定义title属性,当这个类的实例对象调用这个方法时应该报NoMethodError错误,但是ActiveRecord会动态添加这个方法,就和我们手动添加的一样
ActiveRecord源码就是一个把元编程运用到极致的例子
5. 动态调用方法(Object#send)
在Ruby中通过Object#send方法可以代替点标识调用对象的指定实例方法,好处是可以在编码中动态的决定方法的调用
由于在ruby中几乎所有的类都继承自Object对象,所以可以在任意对象上调用send方法来访问这个对象上的所有方法和属性.Object也允许你调用私有属性,但如果你的本意不是这样就需要注意,可以使用Object#public_send这种方式,作用与直接访问相同只是限制不能访问私有方法或私有成员
代码示例如下:
class MyClass
def my_method(my_arg)
my_arg * 2
end
end
obj = MyClass.new
obj.my_method(3) #=> 6
obj.send(:my_method, 3) #=> 6
%w(test1 test2 test3 test4 test5).each do |s|
# define_method动态定义方法
define_method(s) do
puts "#{s} was called"
end
end
# send动态调用方法
(1..5).each { |n| send("test#{n}") }
# => test1 was called
# => test2 was called
# => test3 was called
# => test4 was called
# => test5 was called
6. 动态删除方法(undef,undef_method和remove_method)
首先undef是关键字,remove_method和undef_method是方法
undef关键字和undef_method方法的作用一样
remove_method 移除当前类中的方法定义,当前类仍然能使用从父类继承过来的同名方法
undef_method 移除当前类中的方法定义,并阻止使用从父类继承过来的同名方法
2者对非当前类定义的方法没有影响,特别父类中定义的同名方法
class C
def say_hello
puts "hello C!"
end
end
class C1 < C
def say_hello
puts "hello C1!"
end
end
C.new.say_hello #hello C!
C1.new.say_hello #hello C1!
class C1 < C
remove_method :say_hello
end
C.new.say_hello #hello C!
C1.new.say_hello #hello C! 注意是C不是C1
class C1 < C
undef_method :say_hello
end
C.new.say_hello #hello C!
C1.new.say_hello #undefined method say_hello!
7. 魔术方法(BasicObject#method_missing)
可以创建一个在无此方法错误事件(但在之前是会有错误)前被自动触发调用的处理程序
method_missing利用的机制是,当一个对象进行某个方法调用的时候,会到其对应的类的实例方法中进行查找,如果没有找到,则顺着祖先链向上查找,直到找到BasicObject类为止。如果都没有则会最终调用一个BasicObject#method_missing抛出NoMethodError异常。
与method_missing类似,还有关于常量的const_missing方法,当引用一个不存在的常量时,Ruby会把这个常量名作为一个符号传递给const_missing方法。
当我们需要定义很多相似的方法时候,可以通过重写method_missing方法,对相似的方法进行统一做出回应,这样一来其行为就类似与调用定义过的方法一样。
代码示例如下:
class Roulette
def method_missing(name, *args)
person = name.to_s.capitalize
#如果不是这几个方法,走BasicObject类定义的method_missing方法
super unless %w[Bob Frank Bill Honda Eric].include? person
number = 0
3.times do
number = rand(10) + 1
puts "#{number}..."
end
"#{person} got a #{number}"
end
end
number_of = Roulette.new
puts number_of.bob
puts number_of.kitty
7. 补充:
动态方法与Method_missing的使用原则
当可以使用动态方法时候,尽量使用动态方法。除非必须使用method_missing方法(方法特别多的情况),否则尽量少使用它。
类与模块
在ruby中类与模块的区分不是太明显,完全可以将二者相互替代,之所以同时保留二者的原因是为了保持代码的清晰性,让代码意图更加明确。使用原则:
希望把自己代码包含(include,prepend)到别的代码中,应该使用模块
希望某段代码被实例化或被继承,应该使用类
模块机制可以用来实现类似其他语言中的命名空间概念
::符号
双冒号是定义 namespace 用的,或者叫 scope
当你使用 Foo::Bar 的时候,实际你是在找一个名字叫 Foo 的 namespace ,然后让它返回它里面的 Bar 参数 , 这个 Bar 可以是个常量,可以是个类,可以是个方法 (后两者在 Ruby 中可视为常量)
同理使用 FooBar::method1 的时候实际上是在要求返回 FooBar 这个 namespace 中 method1 这个「常量」的值。
使用 FooBar.method1 的时候则是在调用这个方法,当然返回结果是一样的,这里 :: 和 . 确实是可以互换不影响结果。但 :: 只能用来找 class method , instance method 就只能用 .
另外 :: 还能用来找真正的常量,比方这样
class Foo
Bar = "hello"
bar = "hello"
end
Foo::Bar # => "hello"
Foo::bar # => 出错
Foo.Bar # => 出错
Foo.bar # => 出错
另外 :: 在开始位置则表示回到 root namespace ,就是不管前面套了几个 Module ,都算你其实写在最外层。