Ruby元编程(蛋人)

01 Ruby元编程介绍和使用场景
02 Ruby的类结构
03 Singleton Method单例方法以及super/self的深入讲解
04 Block的进阶使用和面试问题讲解
05 method_missing & define_method
06 class_eval & instance_eval以及深入使用
07 设计模式之Module Mixin
08 append_features和ActiveSupport::Concern的实现
09 Ruby元编程闲话

jayzen补充:
10 class_eval的用法
11 方法和块对象的参数传递以及call使用
12 yield的惯用法
13 define_method和extend结合使用场景
14 inject(reduce)的用法

01 Ruby元编程介绍和使用场景
元编程,类似DSL,一种高度抽象的接口

02 Ruby的类结构
ruby中所有的类都是Class类的实例

A.class #=>Class
Class.class #=>Class

在方法中使用super不仅仅是调用父类的中的方法,实际上是调用其所在类的ancestors类的上一层级的相同方法。

module B 
  def hi
    p 'hi b' 
  end
end

class A 
  include B 
  def hi 
    super 
    p 'hi a' 
  end
end

A.new.hi #=>hi b; hi a;
p A.ancestors #=>[A, B, Object, Kernel, BasicObject]

include extend pretend方法的区别

#include extend的区别
include 模块中的实例方法作为类对象实例方法
extend 模块中的实例方法作为类的类方法

#include pretend
相同点都是讲模块的实例方法引入作为类对象的实例方法
不同点是include方法在ancestors中模块是在类的上一层,而pretend是处于下一层

03 Singleton Method单例方法以及super/self的深入讲解
单例方法的几种形式

# 定义单例方法的方式
# 1
class A 
  def self.hi 
    p 'hi' 
  end
end

# 2
class A 
  class << self 
    def hello 
      p 'hello' 
    end 
  end
end

# 3
def A.hey 
  p 'hey'
end

# 4
(class << A; self; end).class_eval do 
  def you 
    p 'you' 
  end
end

# 5
A.singleton_class.class_eval do 
  def folk 
    p 'folk' 
  end
end

# 6
A.define_singleton_method(:hello) do |*args| 
  p args
end

self的用法

class A 
  p self  #当前的A类

  class << self 
    p self  #A类的单例类
  end 

  def hello 
    p self #A类的实例化对象
  end
end

变量:类的类变量以及类实例变量,实例的实例变量以及类变量

class B
  #这两个变量在类B的作用域中 
  @a = 1 
  @@b = 2 

  def initialize 
    #这两个变量在类B的实例化对象的作用域中
    @c = 3 
    @@d = 4 
  end 

  class << self 
    #这两个变量在单例作用域中
    @e = 5 
    @@f = 6 
  end 
end

b = B.new
p B.instance_variables #=>[:@a]
#说明在B作用域内,只要使用@@符号定义的变量,均为B类的类变量
p B.class_variables #=>[:@@b, :@@f, :@@d] 
p b.instance_variables #=>[:@c]
p B.singleton_class.instance_variables #=>[:@e]
#并没有类变量
p B.singleton_class.class_variables #=>[]

变量的一般写法

class B
  #类中定义类变量
  @@a

  def initialize
    #实例方法中定义实例变量,同时在实例中可以访问类变量
    @b
    @@c
  end
end

04 Block的进阶使用和面试问题讲解
第一个问题:实现一个计数器功能,每次call这个方法,计数器增加一个数值。

def counter(n)
  #代码块会保留当前作用域中的变量
  proc { n +=1 }
end

a = counter(10)
a.call #=>11
a.call #=>12

第二题

#测试题, 请实现以下类EnuTest的功能
enu = EnuTest.new do |x|
  x << 1
  x << 3
  x << proc { 'hello' }
end

enu.next # => 1
enu.next # => 3
enu.next # => 'hello'
enu.next # => raise error 'EOF'

# 答案
class EnuTest

  def initialize &block
    @eb = EnuBlock.new
    yield @eb
  end

  def next
    @eb.next
  end

end

class EnuBlock

  def initialize
    @blocks = []
  end

  def << obj
    if obj.is_a? Proc
      @blocks << obj
    else
      @blocks << proc { obj }
    end
  end

  def next
    if @blocks.empty?
      raise "EOF"
    else
      @blocks.shift.call
    end
  end

end

05 method_missing & define_method

#实现class A的代码
class A
end

A.title= "hh"
A.title #=>"hh"

实现方案一:
class A
  @@attributes = {}

  def self.title=(value)
    @@attributes[:title] = value
  end

  def self.title
    @@attributes[:title]
  end
end

A.title = "xx"
puts A.title #=> "xx"

实现方案二: 使用method_missing
如果没有从ancestors中寻找到对应的方法,会依次执行从ancestors中的method_missing方法。
class A
  @@attributes = {}

  class << self
    def method_missing method_name, *params
      method_name = method_name.to_s

      if method_name =~ /=$/
        @@attributes[method_name.sub('=', '')] = params.first
      elsif @@attributes[method_name]
         @@attributes[method_name]
      else
         "no assignment"
      end
    end
  end
end

实现方案三:define_method
在代码的运行过程当中定义代码
class User < ActiveRecord::Base 
  STATUS = %w[pending activated suspended] 

  STATUS.each do |status| 
    define_method "is_#{status}?" do
      self.status == status 
    end 
  end
end
上面的代码中define_method定义的方法是在类中进行定义,其方法为实例方法。
如果需要定义类方法,可以在单例类中进行定义,使用class<<self形式。

06 class_eval & instance_eval以及深入使用
instance_eval定义的是一个单例作用域,定义是方法是单例方法。任何一个类都是Class类的实例对象,因此在普通类上定义,在其中获得的是类方法,在普通对象上定义,获得的是该对象的单例方法。class_eval获得的是一个类作用域,在其中定义方法,获得是类的实例化对象的实例方法,如果要定义类方法,可以在类作用域中使用def self.method的形式定义类方法。

class A
end

A.instance_eval do
  def hello
    puts "hello"
  end
end

puts A.hello

A.class_eval do
  def hello
    puts "hello"
  end
end

puts A.new.hello

#在class_eval中定义类方法
A.class_eval do
  def self.hello
    puts "hello"
  end
end

对应关系

# class_eval => class_exec( module_eval => module_exec)
# instance_eval => instance_exec
class_eval #支持代码块(不支持改变常量值),也支持字符串,可以针对变量进行操作
class_exec #只支持代码块的形式
A.class_eval "@@a = 2"
A.class_eval { @@= 2} #报错

在目标类中触发方法

module B
  def self.included base
    p 'included...'

    base.class_eval do
      #在base类中执行下面这个方法
      set_logger :hi
    end
  end

  def hi
    p 'hi'
  end
end

class C
  def self.set_logger method_name
    # .....
  end

  include B
end

7 设计模式之 Module Mixin

#最原始的代码
module ActsAsField
  def self.included base
    base.include InstanceMethods
    base.extend ClassMethods

    base.class_eval do
      @@acts_as_fields = []
    end
  end

  module ClassMethods
    def field name, path
      #在用到module mixin时候的类变量,必须通过如下的方法进行定义和获取
      result = class_variable_get(:@@acts_as_fields)
      result << name.to_sym
      class_variable_set(:@@acts_as_fields, result)

      #define_method在类作用域中进行定义,定定义的方法为实例方法
      define_method(name) do
        case path
        when String
          #这里面的self是类Devise的实例对象
          path.split(".").inject(self.latest_data) { |data, key| data[key] }
        when Proc
          path.call(self)
        end
      end
    end
  end

  module InstanceMethods
    def acts_as_fields
      self.class.class_variable_get :@@acts_as_fields
    end
  end

end

class Device
  include ActsAsField

  field :device_type, "device_type"
  field :battery, "data.battery"
  field :node_info, "data.node_info"

  field :battery_to_text, proc { |device|
    "#{device.battery}%"
  }

  def latest_data
    {
      "data" => {
        "node_info" => "this is a sensor",
        "battery" => 90
      },
      "device_type" => "Sensor"
    }
  end

end

d = Device.new
p d.node_info
p d.battery_to_text
p d.acts_as_fields

使用activesupport进行改写

require 'active_support/concern'

module ActsAsField
  extend ActiveSupport::Concern

  included do
    @@acts_as_fields = []
  end
  
  class_methods do
    def field name, path
      result = class_variable_get(:@@acts_as_fields)
      result << name.to_sym
      class_variable_set(:@@acts_as_fields, result)

      define_method(name) do
        case path
        when String
          path.split(".").inject(self.latest_data) { |data, key| data[key] }
        when Proc
          path.call(self)
        end
      end
    end
  end


  def acts_as_fields
    self.class.class_variable_get :@@acts_as_fields
  end
end

class Device
  include ActsAsField

  field :device_type, "device_type"
  field :battery, "data.battery"
  field :node_info, "data.node_info"

  field :battery_to_text, proc { |device|
    "#{device.battery}%"
  }

  def latest_data
    {
      "data" => {
        "node_info" => "this is a sensor",
        "battery" => 90
      },
      "device_type" => "Sensor"
    }
  end

end

d = Device.new
p d.node_info
p d.battery_to_text
p d.acts_as_fields

9 append_features和ActiveSupport::Concern的实现

module EggConcern
  def append_features base
    super
    base.instance_eval(&@_class_methods)
    base.class_eval(&@_class_eval)
  end

  def included base = nil, &block
    super
    @_class_eval = block
  end

  def class_methods &block
    @_class_methods = block
  end

end

module ActsAsField
  extend EggConcern

  included do    
    @@acts_as_fields = []
  end

  class_methods do
    def field name, path
      result = class_variable_get(:@@acts_as_fields)
      result << name.to_sym
      class_variable_set(:@@acts_as_fields, result)

      define_method(name) do
        case path
        when String
          path.split(".").inject(self.latest_data) { |data, key| data[key] }
        when Proc
          path.call(self)
        end
      end
    end
  end

  def acts_as_fields
    self.class.class_variable_get :@@acts_as_fields
  end

end

class Device
  include ActsAsField

  field :device_type, "device_type"
  field :battery, "data.battery"
  field :node_info, "data.node_info"

  field :battery_to_text, proc { |device|
    "#{device.battery}%"
  }

  def latest_data
    {
      "data" => {
        "node_info" => "this is a sensor",
        "battery" => 90
      },
      "device_type" => "Sensor"
    }
  end

end

d = Device.new
p d.node_info
p d.battery_to_text
p d.acts_as_fields

使用active_support扩展类属性

require 'active_support/all'

#类属性
class A
  class_attribute :title
  #下面也可以
  cattr_accessor :ttile
end

A.title = "dd"
A.title

#模块属性
module B
  mattr_accessor :title
end

10 class_eval的用法
参考api中的用法,接受代码块和以及接受String作为参数。

#接受 do..end形式的代码块
class Thing
end

Thing.class_eval do
  def demo
    puts "this is the demo test"
  end
end

Thing.new.demo #=>"this is the demo test"

#接受{}形式的代码块也是一样
class Thing
end

Thing.class_eval {
  def demo
    puts "this is the demo test"
  end
}

Thing.new.demo #=>"this is the demo test"

#接受参数形式的代码块
class Thing
end

a = proc {def demo; puts "this is the demo test"; end}
Thing.class_eval(&a)
Thing.new.demo

#接受参数形式的String对象
class Thing
end
a = %q{def hello; puts "Hello there!"; end}
Thing.class_eval(a)
Thing.new.hello()

11 方法和块对象的参数传递以及call使用

#方法中的参数可以直接传递到块对象中
def demo(n)
  proc { puts "this is #{n}"}
end

a = demo(10)
a.call #=> this is 10

#使用call调用参数
def demo
  proc {|n| puts "this is #{n}"}
end

a = demo
a.call(10) #=> this is 10

12 yield的惯用法
yield的作用在于方法调用的过程中执行块的内容。

def test &block
  yield
end

test do
  puts "this is the test"
end
#"this is the test"

介绍yield中使用参数的情况

def test &block
  yield "demo"  #"demo"作为参数值传递进入下面的块中,并进行返回
end

test do |x|
  puts "this is #{x}"
end
#"this is demo"

13 define_method和extend结合使用场景
在类中调用方法,其方法所定义的作于在这个类作用域中,而不是这个类的单例类作用域中,因此这个方法是实例方法,而不是类方法。

module Demo
  def self.included(base)
    base.include InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def method_one
      puts "this is instance method"
    end
  end

  module ClassMethods
    def method_two
      define_method(:test) do
        puts "this is the instance method"
      end
    end
  end
end

class Test
  include Demo
  method_two
end

Test.new.test #=>this is the instance method

define_method方法只能被类私有调用,不能被实例化对象调用,因为define_method是Module类的私有实例方法,而Module类是Class类的父类。

module Demo
  def self.included(base)
    base.include InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def test
      define_method(:test) do
        puts "this is instance method"
      end
    end
  end
end

class Test
  include Demo
end

Test.new.test #=>undefined method `define_method'
即说明不能在实例中使用define_method方法。

14 inject(reduce)的用法
1.数字求和

[1,2,3,4].inject(0){ |result, element| result+element } #=>10
[1,2,3,4].inject{ |result, element| result+element } #=>10

从上面代码可以得出inject的参数是可选的,代码块是必须的,如果具有参数值,参数值作为首个result的值,被调用对象中的第一个值作为element,第一执行过程result和element的和作为第二次执行中result的值,即是块中的返回值作为下次执行的result值。
如果参数中没有值,则被调用对象的首个数值作为result,下面执行的情况和上面说叙述的类似。
2.创建hash

hash = [[:first_name, 'jay'], [:last_name, 'jayzen']].inject({}) do
  |result, element|
  result[element.first] = element.last
  result
end

创建hash的三种方式:

#第一种方法中,参数必须为偶数形式
Hash["a", 100, "b", 200] #=> {"a"=>100, "b"=>200}
Hash[ [ ["a", 100], ["b", 200] ] ] #=> {"a"=>100, "b"=>200}
Hash["a" => 100, "b" => 200] #=> {"a"=>100, "b"=>200}

3.创建Array
针对对象中存在两个属性,并且两个属性有关联。

TestResult = Struct.new(:status, :message)
results = [
  TestResult.new(:failed, "1 expected but was 2"),
  TestResult.new(:sucess),
  TestResult.new(:failed, "10 expected but was 20")
]

messages = results.inject([]) do |messages, test_result|
  messages << test_result.message if test_result.status == :failed
  messages
end
messages # => ["1 expected but was 2", "10 expected but was 20"]

创建Structs的两种方式:

#第一种方式
Struct.new("Customer", :name, :address)#=>Struct::Customer
Struct::Customer.new("Dave", "123 Main")#=> #<struct Struct::Customer name="Dave", address="123 Main">
#在第一种方式中直接使用Customer.new也是可以的。

#第二种方式
Customer = Struct.new(:name, :address) do
  def greeting 
    "Hello #{name}!" 
  end
end
Customer.new("Dave", "123 Main").greeting # => "Hello Dave!"

4.创建Hash
相同的状态值设为一组。

TestResult = Struct.new(:status, :message)
results = [
  TestResult.new(:failed, "1 expected but was 2"),
  TestResult.new(:sucess),
  TestResult.new(:failed, "10 expected but was 20")
]

grouped_results = results.inject({}) do |grouped, test_result|
  grouped[test_result.status] = [] if grouped[test_result.status].nil?
  grouped[test_result.status] << test_result
  grouped
end

grouped_results# >> {
  :failed => [# >> #<struct TestResult status=:failed, message="1 expected but was 2">,
              # >> #<struct TestResult status=:failed, message="10 expected but was 20">],
              # >> :sucess => [ #<struct TestResult status=:sucess, message=nil> ]# >> }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 对象模型 所有class定义之外的代码默认运行在顶级对象main中。 打开类 ruby的class更像是一个作用于...
    五月的约修亚阅读 3,467评论 0 4
  • 说起牙痛,真是让我彻夜难眠。经历这十天痛苦的煎熬,今早终于打上了点滴,人感觉也好多了。 原来总认为牙痛是上火,降火...
    王泽华wzh阅读 111评论 0 0
  • 6/21 马亚妮 成都【每日一结构】结构思考力21天思维改善训练营 G:【学习】显性思维结构化 1.结论先行。 2...
    夏目彩虹阅读 153评论 0 0