16.在改变作为参数的集合之前复制它们

17.使用Array方法将nil及标量对象转化成数组

18.考虑使用集合高效检查元素的包含性

19.了解使用通过reduce方法折叠集合

20.考虑使用默认hash值

21.对集合优先使用委托而非继承

16.在改变作为参数的集合之前复制它们

多数对象传递是通过引用而非实际值来进行传递的(Integer是通过值来进行传递),如果使用引用来进行传递,任何一个拥有该对象的引用的代码都可以完成对这个对象的修改。

class Tuner
  def initialize(presents)
    @presents = presents
    clean
  end

  private
    def clean
      @presents.delete_if{|f| f[-1].to_i.even?}
    end
end

presents = %w(90.1 106.2 88.5)
tuner = Tuner.new(presents)
p presents #["90.1", "88.5"]

注意上面的代码,delete_if会改变原始值,还包括类似的方法,reject!方法,因此可以使reject方法不改变原始值,但是返回改变后的值。

#delete_if,reject!方法会改变原始值,reject不会改变原始值,但是返回改变后的值
presents = %w(90.1 106.2 88.5)
presents.reject{|c| c[-1].to_i.even? } #["90.1", "88.5"]
presents #["90.1", "106.2", "88.5"]

presents = %w(90.1 106.2 88.5)
presents.reject!{|c| c[-1].to_i.even? } #["90.1", "88.5"]
presents #["90.1", "88.5"]

presents = %w(90.1 106.2 88.5)
presents.delete_if!{|c| c[-1].to_i.even? } #["90.1", "88.5"]
presents #["90.1", "88.5"]

使用reject方法对上面的代码进行改进

class Tuner
  def initialize(presents)
    @presents = clean(presents)
  end

  private
    def clean(presnets)
      presents.delete_if{|f| f[-1].to_i.even?}
    end
end

presents = %w(90.1 106.2 88.5)
tuner = Tuner.new(presents)
p presents #["90.1", "106.2", "88.5"],没有改变传递进来的变量

实际上,如果用参数的副本来传递变量更为合理

class Tuner
  def initialize(presents)
    @presents = presents.dup
    clean
  end

  private
    def clean
      @presents.delete_if{|f| f[-1].to_i.even?}
    end
end

presents = %w(90.1 106.2 88.5)
tuner = Tuner.new(presents)
p presents #["90.1", "106.2", "88.5"]

上述代码也是存在问题,因为如果对于Array元素进行复制,仅仅只是复制了容器本身,而不会复制里面的元素,即是对元素的增减是不会互相影响的,如果是改变元素的本身,则会互相影响,详细见赋值和拷贝中内容叙述。

17.使用Array方法将nil及标量对象转化成数组

这个章节的内容是使用Array()方法将其他一些对象转化成数组

#代码示例
class Pizza
  #在下面的方法中,是预期toppings按照数组的对象来进行操作,这里可能是nil对象,所以会出错
  def initialize(toppings)
    toppings.each do |topping|
      add_some_method(topping)
    end
  end
end

下面的是一些其他对象通过Array()方法转化成数组

#数组返回本身
Array(["hello", "world"]) #["hello", "world"]

#nil返回空数组
Array(nil) #[]

#hash返回二维数组
demo = Array({hello: "world", thank: "you"}) #[[:hello, "world"], [:thank, "you"]]
#通过Hash[二维数组]的形式生成hash
Hash[demo] #{hello: "world", thank: "you"}

#range会展开为数组
Array(1..5) #[1,2,3,4,5]

#string对象返回单个数组对象
Array("demo") #["demo"]

18.考虑使用集合高效检查元素的包含性

使用Array的include?方法来计算时间复杂度,其是O(n), 使用Hash来计算时间复杂度是O(log n),对比下面的计算式

#使用Array#include?
class Role
  def initialize(name, permissions)
    @name, @permissions = name, permissions
  end

  def can?(permission)
    @permissions.include?(permission)
  end
end
#使用Hash的include?
class Role
  def initialize(name, permissions)
    @name = name
    #其中value值是true, 是一个不可变的全局变量
    @permissions = Hash[permissions.map{ |p| [p, true]}]
  end

  def can?(permission)
    @permissions.include?(permission)
  end
end

Set可以由任何集合对象或枚举器来构建,实现也是基于hash,使用include的时间复杂度是O(log n),使用方式如下

#使用Set的include?
class Role
  def initialize(name, permissions)
    @name,@permissions = name, Set.new(permissions)
  end

  def can?(permission)
    @permissions.include?(permission)
  end
end

Set基于hash实现,如果将对象放置在Set中,需要和hash一样,实现eql?方法和hash方法。

#代码示例
require 'set'
require 'csv'

class AnnualWeather
  Reading = Struct.new(:date, :high, :low) do
    def eql? other
      date.eql? other.date
    end

    def hash
      date.hash
    end
  end

  def initialize(file_name)
    @readings = Set.new
    CSV.foreach(file_name, headers: true) do |row|
      @readings << Reading.new(Date.parse(row[2]), row[10].to_f, row[11].to_f)
    end
  end
end

19.了解使用通过reduce方法折叠集合

reduce方法

20.考虑使用默认hash值

如果hash没有默认值的情况下,如果访问不存在key时,返回nil,如果添加了hash的默认值,则返回默认值

#没有默认值
a = {}
a["demo"] #nil

a = Hash.new(0)
a["demo"] #0
#没有默认值的情况下
array = [1,2,3]
def frequency(array)
  array.reduce({}) do |hash, element|
    hash[element] ||= 0
    hash[element] += 1
    hash
  end
end
p frequency(array) #{1=>1, 2=>1, 3=>1}

#使用默认值的情况下使用
array = [1,2,3]
def frequency(array)
  array.reduce(Hash.new(0)) do |hash, element|
    hash[element] += 1
    hash
  end
end
p frequency(array) #{1=>1, 2=>1, 3=>1}

上述的代码中出现“+=”,如果在hash存在默认值的情况下,使用的时候会出现多余的hash值,+=的扩展如下所示

#+=的扩展
h[:missing_key] += 1
h[:missing_key] = h[:missing_key] + 1
h = Hash.new(42)
h[:missing_key] #42
h.keys #[]
h[:missing_key] += h[:missing_key] #43
h.keys #[:missing_key]

1.上面的是使用Integer值作为hash的默认值

#hash使用Integer作为默认值,Integer是不可变的
a = Hash.new(0)
a["demo"] #0

2.使用数组[]作为hash的默认值

#使用[]作为默认值,[]值是可以改变的
a = Hash.new([])
a["demo"] #[]

#Hash.new使用空数组作为默认值,这其实是对可变数组的引用,a["demo"]返回的是对这个可变数组的引用
#如果默认值被修改了,当访问不存在的键时,返回的是修改后的值
a = Hash.new([])
a["demo"] << "hello"
a["cc"] #["hello"]

通过"<<"方式获得的hash会共享hash值

h = Hash.new([])
h[:weekdays] = h[:weekdays] << "monday"
h[:months] = h[:months] << "january"
h.keys #[:weekdays, :months]

h[:weekdays] #["monday", "january"]
h[:months] #["monday", "january"]
h.default ##["monday", "january"]

上面的代码示例中,通过<<方式产生的key, weekdays和months共享一个默认数组。

3.使用块作为hash的默认值

希望访问一个不存在的键时能返回一个全新的数组,那么选择默认值为块

h = Hash.new{[]}
h[:weekdays] = h[:weekdays] << "monday"
h[:months] = h[:months] << "january"

h[:weekdays] #["monday"]
h[:months] #["months"]

注意点:

1.如果设置hash生成的默认值,则hash[key]返回不是nil或者false

2.fetch用法

h = { "a" => 100, "b" => 200 }
#1.fetch有两个参数,如果第一参数没有值,则返回第二个参数的内容
h.fetch("a")                            #=> 100
h.fetch("z", "go fish")                 #=> "go fish"

#2.如果fetch的第二个参数为块,如果第一参数没有值,则返回块中内容,块参数为key值
h.fetch("z") { |el| "go fish, #{el}"}   #=> "go fish, z"

#3.如果只有第一个参数,那么没有相应的key,则返回error
h.fetch("z") #key error

21.对集合优先使用委托而非继承

使用继承的最大的缺点是耦合,同时又有一个副作用调用reverse方法之后返回的不再是该子类的对象

class ListArray < Array
end

demo = ListArray.new([1,2,3])
demo.class #ListArray
obj = demo.reverse
obj.class #Array

使用代理的好处是对需要的方法开放,对不需要的对象不必使用undef method消除

class Demo
  extend Forwardable
  include Enumerable
  def_delegators :@hash, :[], :[]=, :keys, :values, :length

  def initialize
    @hash = Hash.new do |hash, key|
      raise KeyError, "invalid key #{key}"
    end
  end

  def invert
    other = self.class.new
    #使用protected方法可以改变实例变量的值
    other.replace!(@hash.invert)
    other
  end

  def initialize_copy(other)
    @hash = @hash.dup
  end

  #freeze, taint, untaint方法先传递消息给委托目标,之后调用super方法
  def freeze
    @hash.freeze
    super
  end

  def taint
    @hash.taint
    super
  end

  def untaint
    @hash.untaint
    super
  end

  protected
    def replace!(hash)
      #保存default_proc到revert之后的对象中
      hash.default_proc = @hash.default_proc
      @hash = hash
    end
end

demo = Demo.new
demo[:hello] = "world"
p demo[:hello]
p demo.invert
p demo.invert.class #Demo

使用回调方法来优化上面的代码

#定义SuperForwardable模块
module SuperForwardable
  def self.extended(klass)
    require 'forwardable'
    klass.extend Forwardable
  end

  def def_delegator_with_super(target, *methods)
    methods.each do |method|
      target_method = "#{method}_without_super".to_sym
      def_delegator(target, method, target_method)

      define_method(method) do |*args, &block|
        send(target_method, *args, &block)
        super(*args, &block)
      end
    end
  end
end

#重新打开RaisingHash类
class RaisingHash
  extend SuperForwardable
  def_delegators(:@hash, :[], :[]=)
  def_delegators_with_super(:@hash, :freeze, :taint, :untaint)

  def initialize
  ..
  end
end

results matching ""

    No results matching ""