美文网首页
《Ruby 元编程》中的那些干货

《Ruby 元编程》中的那些干货

作者: FFCP | 来源:发表于2017-05-07 20:33 被阅读252次

    1. 对象模型

    1.1 打开类

    从某种意义上说,Ruby 的 class 关键字更像是一个作用域操作符而不是类声明语句。它的确可以创建一个还不存在的类,不过也可以把这看成一种副作用。对于 class 关键字,其核心任务是把你带到类的上下文中,让你可以在其中定义方法。

    示例1:

    你的代码中需要定义一个函数去掉字符串中的标点符号和特殊字符,只保留字母、数字和空格。

    在像 Java 这样的编程语言中,你必须这样定义这个方法:

    public class StringUtil {
        public static String to_alphanumeric(s) {
            return s.replaceAll("[^\\w\\s]", "");
        }
    }
    

    而在 Ruby 中你可以将这个 to_alphanumeric 方法直接定义为 String 类的实例方法:

    class String
        def to_alphanumeric
            self.gsub /[^\w\s]/, ''
        end
    end
    

    示例2:

    class D
        def x; 'x'; end
    end
    
    class D
        def y; 'y'; end
    end
    
    obj = D.new
    obj.x        # => 'x'
    obj.y        # => 'y'
    

    使用“打开类”技术时,需要注意的问题:在打开一个类向其中添加一个新的方法时,需要注意这个类中是否已经存在一个同名的方法,以免不小心覆盖了类中原有的同名方法。

    1.2 几个 Reflection 方法

    1.2.1 Object#instance_variables()

    Returns an array of instance variable names for the receiver. Note that simply defining an accessor does not create the corresponding instance variable.

    1.2.2 Object#methods(all=true)

    Returns a list of the names of public and protected methods of obj. This will include all the methods accessible in obj’s ancestors. If the all parameter is set to false, only those methods in the receiver will be listed.

    1.2.3 Module#instance_methods(include_super=true)

    Returns an array containing the names of the public and protected instance methods in the receiver. For a module, these are the public and protected methods; for a class, they are the instance (not singleton) methods. With no argument, or with an argument that is false, the instance methods in mod are returned, otherwise the methods in mod and mod’s superclasses are returned.

    1.2.4 Module#ancesotrs()

    Returns a list of modules included in mod (including mod itself).

    class MyClass
        def my_methods; 'my_method()'; end    
    end
    
    class MySubclass < MyClass
    end
    
    MySubclass.ancestors()        # => [MySubclass, MyClass, Object, Kernel, BasicObject]
    

    1.3 常量的作用域和路径

    常量的作用域不同于变量,它有自己独特的规则。例如:

    module MyModule
        MyConstant = 'Outer Constant'
        class MyClass
            MyConstant = 'Inner Constant'
        end
    end
    

    这段代码中的所有常量像文件系统一样组织成树形结构:

    MyModule
    ├── MyClass
    │   └── MyConstant
    └── MyConstant
    
    

    1.4 几个和常量有关的方法

    1.4.1 Module#constants(inherit=true)

    Returns an array of the names of the constants accessible in mod. This includes the names of constants in any included modules (example at start of section), unless the inherit parameter is set to false.

    1.4.2 Module.constants()Module.constants(inherit)

    In the first form, returns an array of the names of all constants accessible from the point of call. This list includes the names of all modules and classes defined in the global scope.

    The second form calls the instance method constants.

    1.4.3 Module.nesting()

    获取当前常量的路径。例如:

    moudle M
        class C
            module M2
                Module.nesting        # => [M::C::M2, M::C, M]
            end
        end
    end        
    

    1.5 修剪常量树

    若想在网上找到一个 motd.rb 文件用来在控制台上显示“当天的消息”,且想把这段代码集成到最新的程序中去,那么使用load执行该文件来显示消息:

    load('motd.rb')
    

    不过,使用 load() 方法有一个副作用。motd.rb 文件很可能定义了变量和类。尽管变量在加载完成后会落在当前作用域之外,但常量不会。这样,motd.rb可能会通过它的常量(尤其是类名)污染当前程序的命名空间。

    可以通过使用第二个可选参数来控制其常量仅在自身范围内有效:

    load('motd.rb', true)
    

    通过这种方式加载的文件,Ruby 会创建一个匿名模块,使用它作为命名空间来容纳 motd.rb 中定义的所有常量,加载完成后,该模块会被销毁。

    require()方法与load()方法颇为相似,但是它的目的不同。通过load()方法可以执行代码,而require()则是用来导入类库。这就是require()方法没有第二个可选参数的原因。在这些类库中的类名通常是你导入这些库时所希望得到的,因此没有理由在加载后销毁它们。

    1.6 关于 Kernel 模块

    如果需要定义一个工具函数,这个函数的作用很广泛,以至于它使用起来更像语言内核的一部分,那么这个方法最好定义 Kernel 模块的方法。

    1.7 private 究竟意味着什么

    私有方法服从一个简单的规则:不能明确指定一个接受者来调用一个私有方法。换言之,每次调用一个私有方法时,只能调用于隐含的接受者 —— self 上。下面看一个极端例子:

    class C
        def public_method
            self.private_method
        end
        
        private
        
            def private_method; end
    end
    
    C.new.public_method
    
     # => NoMethodError: private method `private_method' called […]
    

    在这段代码中,如果去掉 self 关键字,它就可以正常运行。

    这个人为制造的例子演示了私有方法是由两条规则一起控制的:第一条:如果调用方法的接受者不是你自己,则必须明确指明一个接受者;第二条,私有方法只能被隐含接受者调用。把这两条规则糅合在一起,你会发现只能在自身中调用一个私有方法。你可以把这个糅合后的规则成为“似有规则”。

    1.8 混乱的模块

    有如下代码:

    module Printable
        def print; end
    end
    
    module Document
        def print; end
    end
    
    class Book
        include Document
        include Printable
    end
    
    book = Book.new
    b.print
    

    问:b.print 调用的是 Printable 还是 Document 中定义的 print 方法?

    答:Book.ancestors 的输出:[Book, Printable, Document, Object, .. ..],因此调用的是 Printable 中定义的 print 方法。

    1.9 完整的类和对象模型图

    下列代码:

    class MyClass
        def method_a; end
    end
    
    obj1 = MyClass.new
    obj2 = MyClass.new
    obj3 = MyClass.new
    
    class << obj3
        def method_b; end
    end
    

    对应的模型图如:

    1.png

    2. 方法

    2.1 动态方法

    2.1.1 动态方法所依赖的基础

    • 动态派发:Object#send(symbol_or_string [, args…])

      Invokes the method identified by symbol, passing it any arguments specified. You can use __send__ if the name send clashes with an existing method in obj. When the method is identified by a string, the string is converted to a symbol.

    • 动态定义:Module#define_method(symbol, method)Module#define_method(symbol){ block }

      Defines an instance method in the receiver. The method parameter can be a Proc, a Method or an UnboundMethod object. If a block is specified, it is used as the method body. This block is evaluated using instance_eval. define_method is a private method.

    2.1.2 示例

    有这样一个程序:

     # data_source.rb:
    class DS
      def initialzie # connect to data source…
      def get_mouse_info(workstation_id)  # …
      def get_mouse_price(workstation_id)  # …
      def get_keyboard_info(workstation_id)  # …
      def get_keyboard_price(workstation_id)  # …
      def get_cpu_info(workstatiob_id)  #…
      def get_cpu_price(workstation_id)  #…
      def get_display_info(workstation_id)  # …
      def get_display_price(workstation_id)  #...
    end
    
     # computer.rb
    class Computer
      def initialzie(id, data_source)
        @id = id
        @data_source = data_source
      end
      
      def mouse
        info = @data_souce.get_mouse_info(@id)
        price = @data_source.get_mouse_price(@id)
        result = "Mouse: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result
      end
      
      def cpu
        info = @data_souce.get_cpu_info(@id)
        price = @data_source.get_cpu_price(@id)
        result = "Mouse: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result  
      end
      … … 
    end  
    
    2.1.2.1 使用动态派发重构 computer.rb
    class Computer
      def initialize(id, data_source)
        @id = id
        @data_source = data_source
      end
      
      def mouse
        component :mouse
      end
      
      def cpu
        component :cpu
      end
      … …
      
      def component(name)
        info = @data_source.send("get_#{name}_info", @id)
        price = @data_source.send("get_#{name}_price", @id)
        result = "#{name.to_s.capitalize}: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result
      end
    end
    
    2.1.2.2 使用动态定义重构 computer.rb
    class Computer
      def initialzie(id, data_source)
        @id = id
        @data_source = data_source
      end
    
      def self.component(name)
        define_method name do 
          info = @data_source.send "get_#{name}_info", @id
          price = @data_source.send "get_#{name}_price", @id
          result = "#{name.to_s.capitalize}: #{info} ($#{price})"
          return "* #{result}" if price > 100
          result
        end
      end
      
      component :mouse
      component :cpu
      … …
    end
    
    2.1.2.3 使用内省重构 computer.rb
    class Computer
      def initialzie(id, data_source)
        @id = id
        @data_source = data_source
        @data_source.methods.grep(/^get_(\w+)_info/) {self.class.component $1}
      end
    
      def self.component(name)
        define_method name do 
          info = @data_source.send "get_#{name}_info", @id
          price = @data_source.send "get_#{name}_price", @id
          result = "#{name.to_s.capitalize}: #{info} ($#{price})"
          return "* #{result}" if price > 100
          result
        end
      end 
    end
    

    2.2 method_missing 幽灵方法(Ghost Method)

    2.2.1 method_missing(symbol [, *args] ) 方法

    Invoked by Ruby when obj is sent a message it cannot handle. symbol is the symbol for the method called, and args are any arguments that were passed to it. By default, the interpreter raises an error when this method is called. However, it is possible to override the method to provide more dynamic behavior. If it is decided that a particular method should not be handled, then super should be called, so that ancestors can pick up the missing method.

    2.2.2 使用 method_missing 重构 computer.rb

    class Computer
      def initialzie(id, data_source)
        @id = id
        @data_source = data_source
      end
      def method_missing(name, *args)
        super unless @data_source.respond_to? "get_#{name}_info"
        info = @data_source.send "get_#{name}_info", @id
        price = @data_source.send "get_#{name}_price", @id
        result "#{name.to_s.capitalzie}: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result    
      end
    end
    

    2.2.3 覆盖 respond_to?

    class Computer
      def initialzie(id, data_source)
        @id = id
        @data_source = data_source
      end
      def method_missing(name, *args)
        super unless @data_source.respond_to? "get_#{name}_info"
        info = @data_source.send "get_#{name}_info", @id
        price = @data_source.send "get_#{name}_price", @id
        result "#{name.to_s.capitalzie}: #{info} ($#{price})"
        return "* #{result}" if price > 100
        result    
      end
      def respond_to?(name)
        @data_source.respond_to?("get_#{name}_info") || super
      end
    end
    

    2.2.4 白板类(BlankSlate)

    有时候所要代理的方法,已经存在于类或者祖先类种,这种情况下,调用那个方法时,就不会经过 method_missing 代理,因此这时,需要将从父类中继承来的那些不需要的方法都删除掉:

    class Computer
      instance_methods.each do |method|
        undef_method method unless method.to_s =~ /method_missing|respond_to\?/
      end
      … … 
    end
    

    用于从一个类中删除实例方法的方法:

    • Module#undef_method(symbol)

      Prevents the current class from responding to calls to the named method. Contrast this with remove_method, which deletes the method from the particular class; Ruby will still search superclasses and mixed-in modules for a possible receiver. String arguments are converted to symbols.

    • Module#remove_method(symbol)

      Removes the method identified by symbol from the current class. For an example, see Module.undef_method. String arguments are converted to symbols.

    BasicObject

    从 Ruby 1.9 开始,白板技术被集成到语言自身中,在过去的版本中,Object是类体系结构的根节点。在 Ruby 1.9 中,Object类有一个名叫 BasicObject 的超类,它只提供几个很基本的方法:

    p BasicObject.instance_methods
    
    => [:==, :requals?, :!, :!=, :instance_eval, :instance_exec, :__sned__]
    

    默认情况下,类还是会从 Object 继承,从 BasicObject 继承来的类会自动成为白板类。

    3. 代码块

    3.1 当前块

    在一个方法中,可以向Ruby询问当前的方法调用是否包含块。这可以通过 Kernel#block_given?() 方法来做到:

    def a_method
      return yield if block_given?
      'no block'
    end
    
    a_method
    a_method { "here's a block!" }
    

    3.2 使用 Ruby 实现 C# 的 using 关键字

    设想在写一个 C# 程序,这个程序会连接一个远程服务器,并有一个对象表示这个连接:

    RemoteConnection conn = new RemoteConnection("my_server");
    String stuff = conn.readStuff();
    conn.dispose();
    

    这段代码在使用了连接后,会正确的释放连接。然而�,它并没有处理异常。如果 readStuff() 方法抛出一个异常,那么 conn 对象将永远不会得到释放。代码需要将异常管理起来,以便不管是否发生异常都能正确地释放连接。幸运的是,C# 提供了一个叫做 using 的关键字,它能帮助你处理整个过程:

    RemoteConnection conn = new RemoteConnection("some_remote_server");
    using (conn)
    {
      conn.readSomeData();
      doSomeMoreStuff();
    }
    

    这个 using 关键字期望 conn 对象有一个名为 dispose() 的方法,当大括号中的代码执行完后,不管有没有异常抛出,这个方法都会被自动调用。

    OK,下面这个是用Ruby实现的 using 关键字:

    module Kernel
      def using(obj)
        begin
          yield
        rescue
          obj.dispose
        end 
      end
    end
    

    3.3 闭包

    2.png

    正如上图所示,块不仅仅是一段浮动的代码。你不可能在真空中运行代码。当代码运行时,它需要一个执行环境:局部变量、实例变量、self …… 既然这些东西是绑定在对象上的名字,就可以把它们简称为绑定(binding)。块的要点在于它们是完整的,可以立即运行。它们既包含代码,也包含一组绑定。

    那么块是从哪里获得它的绑定的呢?当定义一个块时,它会获取当时环境中的绑定,并且把它传给一个方法时,它会带着这些绑定一起进入该方法:

    def my_method
      x = "Goodbye"
      yield "cruel"
    end
    
    x = "Hello"
    my_method {|y| "#{x}, #{y} world" }  # => "Hello, cruel world"
    

    当创建块时会获取到局部绑定(比如上面的x),然后把块连同它自己的绑定传给一个方法。在上面的例子中,块的绑定中包括一个名为 x 的变量。虽然在方法中定义了一个变量 x,块看到的 x 也是在块定义时绑定的 x,但是方法中的 x 对这个块来说是不可见的。基于这样的特性,计算机科学家喜欢把块称为 闭包(Closure)

    3.3.1 块局部作用域

    块会在定义时获取周围的绑定。你可以在块的背部定义额外的绑定,但是这些绑定在快结束时就会消失:

    def my_method
      yield
    end
    
    top_level_variable = 1
    my_method do
      top_level_variable += 1
      local_to_block = 1
    end
    
    top_level_variable  # => 2
    local_to_block      # => Error!
    

    警告: 在 Ruby 1.8及之前的版本中,块参数对粗心者而言有一个陷阱。跟你期望的相反,块会覆盖具有相同名字的局部变量:

    def my_method
      yield(2)
    end
    
    x = 1
    my_method do |x|
      # 不错什么特殊的操作
    end
    x  # => 2
    

    当把块参数命名为 x 时,块发现当前上下文中已存在一个 x 变量,于是它把这个值传给了块。这种让人吃惊的行为在过去常常是引发 bug 的根源。有条好消息是,在 Ruby 1.9 中这个问题已经被修正了。

    3.4 作用域门

    Ruby 程序会在三个地方关闭前一个作用域,同时打开一个新的作用域:

    • 类定义
    • 模块定义
    • 方法定义

    只要程序进入类或魔窟开及方法的定义,就会发生作用域切换。这三个边界分别用 class、moduledef关键字作为标志。每一个关键字都充当了一个作用域门(Scope Gate)

    示例:

    v1 = 1
    class MyClass   # => 作用域门:进入 class
      v2 = 2
      local_variables   # => ["v2"]
      
      def my_method # => 作用域门:进入 def
        v3 = 3
        local_variables
      end         # => 作用域门:离开 def
      
      local_variables   # => ["v2"]
    end           # => 作用域门:离开 class
    obj = MyClass.new
    obj.my_method     # => [:v3]
    obj.my_method     # => [:v3]
    local_variables     # => [:v1, :obj]
    

    Kernel#local_variables

    Returns the names of the current local variables.

    3.5 全局变量和顶级实例变量

    全局变量可以在任何作用域中访问:

    def a_scope
      $var = "some value"
    end
    
    def another_scope
      $var
    end
    
    a_scope
    another_scope # => "some value"
    

    全局变量的问题在于系统的任何部分都可以修改它们。因此,你会立即发现几乎没法追踪谁把它们改成了什么。正因为如此,基本的原则是:如非必要,尽可能少使用全局变量:

    @var = "The top-level @var"
    
    def my_method
      @var
    end
    
    my_method  # => "The top-level @var"
    

    如上面的代码所示,只要 main 对象在扮演 self 的角色,就可以访问一个顶级实例变量。但当其他对象成为 self 时,顶级实例变量就退出作用域了:

    class MyClass
      def my_method
        @var = "This is not the top-level @var!"
      end
    end
    

    由于不像全局变量那么有全局性,一般认为顶级示例变量比全局变量更安全。

    3.6 扁平化作用域

    • 当想要使变量穿越过类定义时,使用 Class.new

    • 当想要使变量穿越过方法定义时,使用 Module#define_method

    示例:

    my_var = "Success"  
    class MyClass
      # 希望在这里打印 my_var
      
      def my_method
        # 还有这里 ……
      end 
    end
    

    ----------->

    my_var = "Success"
    MyClass = Class.new do
      p my_var
      define_method :my_method do
        p my_var
      end
    end
    

    3.7 上下文探针 Object#instant_eval()

    • instance_eval(string [, filename [, lineno]] )
    • instance_eval {| | block }

    Evaluates a string containing Ruby source code, or the given block, within the context of the receiver (obj). In order to set the context, the variable self is set to obj while the code is executing, giving the code access to obj’s instance variables. In the version of instance_eval that takes a String, the optional second and third parameters supply a filename and starting line number that are used when reporting compilation errors.

    示例:

    class MyClass
      def initialize
        @v = 1
      end
    end
    
    obj = MyClass.new
    obj.instance_eval do
      self      # => #<MyClass:0x3340dc @v=1>
      @v        # => 1
      @v += 1
      @v        # => 2
    end
    

    Ruby 1.9 中引入了一个名为 instance_exec() 的方法,它跟 instance_eval() 的功能相似,但它允许对块传入参数:

    class C
      def initialize
        @x, @y = 1, 2
      end
    end
    
    C.new.instance_exec(3) {|arg| (@x + @y) * arg}     # => 9
    

    3.8 可调用对象

    可调用对象分为三种:

    • proc
    • lambda
    • 方法

    3.8.1 proc 和 lambda 的区别

    使用 Kernel#lambda() 生成的 Proc 对象就是 lambda,通过其他方式生成的 Proc 对象都简称为 proc。 lambda 和 proc 主要有两个方面的区别:

    • 在 lambda 中调用 return 是从该 Proc 对象中返回,而不是从调用的方法中返回。proc 中调用 return 是从该 Proc 对象中返回
    • lambda 调用中对参数的个数会有严格的检查,而 proc 没有。

    综上所述,lambda 的行为更像是普通的方法/函数,因此一般都使用 lambda 对象,除非是想使用 proc 的某种特殊功能。

    另外,在 Ruby 1.8 中 Kernel#proc()Kernel#lambda() 的别名,而在 Ruby 1.9 中 Kernel#proc()Proc.new() 的别名。

    3.8.2 方法

    可以通过 Object#method() 获取一个 Method 对象:

    class MyClass
      def initialize(value)
        @x = value
      end
      
      def my_method
        @x
      end 
    end
    
    obj = MyClass.new(1)
    m = obj.method :my_method
    m.call()            # => 1
    

    可以用 Method#unbind() 把一个方法跟它所绑定的对象相分离,该方法再返回一个 UnboundMethod 对象。你不能执行 UnboundMethod 对象,但能把它绑定到一个对象上,使之再次成为一个 Method 对象:

    unbound = m.unbind
    another_obj = MyClass.new(2)  
    m = unbound.bind(another_obj)
    m.call()              # => 2
    

    3.9 RedFlag

    初版:

    def event(msg)
      puts msg if yield
    end
    
    Dir.glob("*events.rb").each {|file| load file}
    

    第2版:

    def event(name, &block)
        @events[name] = block
    end
    
    def setup(&block)
        @setups << block
    end
    
    Dir.glob("*events.rb").each do |file|
    
        @events = {}
        @setups = []
        load file
        @events.each_pair do |k, v|
            env = Object.new
            @setups.each do |su|
                env.instance_eval &su
            end
            puts k if env.instance_eval &v
        end
    end
    

    第3版:

    ->{
    
        events = {}
        setups = []
    
        Kernel.send :define_method, :event do |name, &block|
            events[name] = block
        end
    
        Kernel.send :define_method, :setup do |&block|
            setups << block
        end
    
        Kernel.send :define_method, :each_event do |&block|
            events.each_pair do |k, v|
                block.call(k, v)
            end
        end
    
        Kernel.send :define_method, :each_setup do |&block|
            setups.each do |su|
                block.call(su)
            end
        end
    
    }.call
    
    Dir.glob("*events.rb").each do |file|
        load file
        each_event do |name, event|
            env = Object.new
            each_setup do |su|
                env.instance_eval &su
            end
            puts name if env.instance_eval &event
        end
    end
    

    4. 类定义

    4.1 Module#class_eval() 方法

    class_eval 用于在一个类中定义实例方法:

    class Hello; end
    
    Hello.class_eval do
      def hello
        puts "hello world"
      end
    end
    
    h = Hello.new
    h.hello   # => hello world
    

    instance_eval 用于在一个类中定义类方法:

    class Hello; end
    
    Hello.instance_eval do
      def hello
        puts "hello world"
      end
    end
    
    Hello.hello   # => hello world
    

    4.2 类实例变量和类变量

    4.2.1 类实例变量

    Ruby 解释器假定所有的实例变量都属于当前对象 self。在类定义时也是如此:

    class MyClass
      @my_var = 1
    end
    

    在类定义的时候,self 的角色由类本身担任,因此实例变量 @my_var 属于这个类。类的实例变量不同于类的对象的实例变量。另外一个例子:

    class MyClass
      @my_var = 1
      def self.read; @my_var; end
      def write; @my_var = 2; end
      def read; @my_var; end
    end
    
    obj = MyClass.new
    obj.write
    obj.read      # => 2
    MyClass.read    # => 1
    

    4.2.2 类变量

    类变量与类示例变量不同,它们可以被自雷或类的实例所使用(在这个意义上,它们更像是 Java 的静态成员)。

    class D < C
      @@v = 1
      def my_method; @@v; end
    end
    
    D.new.my_method     # => 1
    

    不幸的是,类变量有一个很不好的怪癖。下面是一个例子:

    @@v = 1
    
    class MyClass
      @@v = 2
    end
    
    @@v     # => 2
    

    的搭配这样的结果是因为类变量并不真正属于类————它们属于类体系结构。由于 @@v 定义于 main 的上下文,它属于 main 的类 Object,所以也属于 Object 的所有后台。 MyClass 继承自 Object,因此它也共享了这个类变量。

    从技术上讲,尽管这种行为可以理解,但它还是很容易把你绊倒,因为可能会遇到上面所示的意外事件,现在绝大多数 Ruby 主义者都避免使用类变量,而尽量使用类实例变量。

    4.3 Class.new

    Class.new(super_class=Object) { |mod| … }

    Creates a new anonymous (unnamed) class with the given superclass (or Object if no parameter is given). You can give a class a name by assigning the class object to a constant.
    If a block is given, it is passed the class object, and the block is evaluated in the context of this class using class_eval.

    示例:

    Employee = Class.new(Person) do
      def hello
        ...
      end
    end
    
    e = Employee.new
    e.hello
    

    4.4 类方法的写法

    def MyClass.my_method; end
    
    class MyClass
      def self.my_method; end
    end
    
    class MyClass
      class << self
        def my_method; end
      end
    end
    

    在日常编程中应该使用哪种语法,这在很大程度上取决于个人喜好。大多数人认为 self 形式的语法可读性更高,而一些人则明确指出 eigenclass 才是方法的真正所用之处。专家级的 Ruby 程序猿都会不屑于使用“类名”方法的语法,因为这种方式重复了类的名字,给重构带来了不便。

    4.5 类方法和Module#include() 以及 Object#extend()

    使用 Module#inculde() 方法可以混入一个模块,使得在该模块中定义的方法变为调用类的示例方法:

    module Hello
      def hello
        "hello world"
      end
    end
    
    class Person
      include Hello
    end
    
    p = Person.new
    p.hello     # => "hello world"  
    

    结合 eigenclassModule#include() 可以将模块中定义的方法变为调用类的类方法:

    module Hello
      def hello
        "hello world"
      end
    end
    
    class Person
      class << self
        include Hello
      end
    end
    
    Person.hello      # => "hello world"
    

    为了简化上面的操作,Ruby 提供了 Object#extend 方法,例如:

    module Hello
      def hello
        "hello world"
      end
    end
    
    class Person; end
    Person.extend Hello
    Person.hello      # => "hello world"
    
    p = Person.new
    p.extend Hello
    p.hello         # => "hello world"
    

    4.6 环绕别名

    编写环绕别名的步骤:

    1. 给方法定义一个别名
    2. 重新定义这个方法
    3. 在新的方法中调用老的方法

    示例:

    class String
      alias old_length length
      def length
        if old_length > 5
          "long"
        else
          "short"
        end
      end
    end 
    

    警告: 永远不要把一个环绕别名加载两次!

    5. 编写代码的代码

    5.1 Binding 对象

    Binding 就是一个用对象表示的完整作用域。你可以通过创建 Binding 对象来捕获并带走当前的作用域。接下来,你还可以通过 eval() 方法、instance_eval() 方法或 class_eval() 方法,在 Binding 对象所携带的作用域中执行代码。

    可以使用 Kernel#binding() 方法来创建 Binding 对象:

    class MyClass
      def my_method
        @x = 1
        binding
      end
    end
    
    b = MyClass.new.my_method
    

    *eval() 方法家族,可以给它们传递一个 Binding 对象作为额外的参数,代码就可以在这个 Binding 对象所携带的作用域中执行:

    eval "@x", b      # => 1
    

    Ruby 还提供了一个名为 TOPLEVEL_BINDING 的预定义常量,它表示顶级作用域的 Binding 对象。你可以在程序的任何地方访问这个顶级作用域:

    class AnotherClass
      def my_method
        eval "self", TOPLEVEL_BINDING
      end
    end
    
    AnotherClass.new.my_method      # => main
    

    从某种意义上说,你可以把 Binding 对象看成是一个比块更“纯净”的闭包,因为它们只包含作用域而不包含代码。

    5.2 代码字符串

    代码字符串可以像块一样访问局部变量:

    array = ['a', 'b', 'c']
    x = 'd'
    array.instance_eval "self[0] = x"
    
    array   # => ['d', 'b', 'c']
    

    5.3 Kernel#eval 与 Ruby 安全级别

    使用 Kernel#eval() 的问题在于,这存在遭受代码注入攻击的危险。

    Ruby 会自动把不安全的对象——尤其是从外部传入的对象——标记为被污染的。污染对象包括程序从 Web 表单、文件和命令行读入的字符串,甚至包括系统变量。每次从污染字符串运算而来的新字符串,也是被污染的。下面的例子通过调用 tainted?() 方法来判断类是不是被污染了:

    user_input = "User input: #{gets()}"
    puts user_input.tainted?
    

    每次都要检查字符串是否被污染很麻烦,Ruby 提供了一种叫做安全级别的概念,它能很好地弥补污染对象的不足,当设置一个安全你级别(可以通过给 $SAFE 全局变量赋值来实现)时,就禁止了某些特定的潜在危险操作。

    有五个安全级别可供选择,从默认的 0(这里是一个“嬉皮士公社”,在这儿你可以不受约束,也可以格式化硬盘)到 4(这里是“军事管辖区”,在这儿你甚至不能自由地退出程序)。例如,在安全级别 2 上,会禁止绝大多数文件相关工作。值得注意的是,在任何大于 0 的安全级别上,Ruby 都会拒绝执行污染的字符串:

    $SAFE = 1
    user_input = "User input: #{gets()}"
    eval user_input
    

    为了调节安全性,可以在执行代码字符串之前显式去除它的污染小(通过调用 Object#untaint() 方法),然后可以依赖安全级别来禁止注入文件操作这样的危险动作。

    5.4 类扩展混入(Class Extension Mixins)

    编写类混入扩展的步骤:

    1. 定义一个模块,姑且叫做 MyMixin
    2. MyMixin 中定义一个内部模块(通常把它叫做 ClassMethods),并给它定义一些方法。这些方法最终会成为类方法。
    3. 覆盖 MyMixin#included() 方法来用 ClassMethods 扩展包含者(使用 extend() 方法)。

    示例:

    module MyMixin
      module ClassMethods
        def hello
          "hello world"
        end
      end
      
      def self.included(base)
        base.extend ClassMethods
      end
    end
    
    class Person
      include MyMixin
    end
    
    Person.hello      # => "hello world"
    

    5.5 CheckedAttributes

    module CheckedAttributes
      module ClassMethods
        def attr_checked name, &block
          define_method name do
            instance_variable_get "@#{name}"
          end
          
          define_method "#{name}=", value do
            raise "Invalid Attribute Value" unless block.call(value)
            instance_variable_set "@#{name}", value
          end
        end
      end
      def self.included(base)
        base.extend ClassMethods
      end
    end
    
    class Person
      attr_checked :age do |age|
        age > 18
      end
    end
    
    p = Person.new
    p.name = 20
    p.name        # => 20
    p.name = 10     # => raise "Invalid Attribute Value"
    

    相关文章

      网友评论

          本文标题:《Ruby 元编程》中的那些干货

          本文链接:https://www.haomeiwen.com/subject/hjlptxtx.html