本篇是在我接触了 Ruby 很短一段时间后有幸捧起的一本书,下面结合本身的一些思考,来输出一下本身的读书笔记git
学习一门新的编程语言一般须要通过两个阶段:程序员
《Effictive Ruby》就是一本致力于让你在第二阶段更加深刻和全面的了解 Ruby,编写出更具可读性、可维护性代码的书,下面我就着一些我认为的重点和本身的思考来进行一些精简和说明github
# 将 false 放在左边意味着 Ruby 会将表达式解析为 FalseClass#== 方法的调用(该方法继承自 Object 类) # 这样咱们能够很放心地知道:若是右边的操做对象也是 false 对象,那么返回值为 true if false == x ... end # 换句话说,把 false 置为有操做对象是有风险的,可能不一样于咱们的指望,由于其余类可能覆盖 Object#== 方法从而改变下面这个比较 class Bad def == (other) true end end irb> false == Bad.new ---> false irb> Bad.new == false ---> true
在 Ruby 中倡导接口高于类型,也就是说预期要求对象是某个给定类的实例,不如将注意力放在该对象能作什么上。没有什么会阻止你意外地把 Time 类型对象传递给接受 Date 对象的方法,这些类型的问题虽然能够经过测试避免,但仍然有一些多态替换的问题使这些通过测试的应用程序出现问题:数据库
undefined method 'fubar' for nil:NilClass (NoMethodError)
当你调用一个对象的方法而其返回值恰好是讨厌的 nil 对象时,这种状况就会发生···nil 是类 NilClass 的惟一对象。这样的错误会悄然逃过测试而仅在生产环境下出现:若是一个用户作了些超乎寻常的事。编程
另外一种致使该结果的状况是,当一个方法返回 nil 并将其做为参数直接传给一个方法时。事实上存在数量惊人的方式能够将 nil 意外地引入你运行中的程序。最好的防范方式是:假设任何对象均可觉得 nil,包括方法参数和调用方法的返回值。c#
# 最简单的方式是使用 nil? 方法 # 若是方法接受者(receiver)是 nil,该方法将返回真值,不然返回假值。 # 如下几行代码是等价的: person.save if person person.save if !person.nil? person.save unless person.nil? # 将变量显式转换成指望的类型经常比时刻担忧其为 nil 要容易得多 # 尤为是在一个方法即便是部分输入为 nil 时也应该产生结果的时候 # Object 类定义了几种转换方法,它们能在这种状况下派上用场 # 好比,to_s 方法会将方法接受者转化为 string: irb> 13.to_s ---> "13" irb> nil.to_s ---> "" # to_s 如此之棒的缘由在于 String#to_s 方法只是简单返回 self 而不作任何转换和复制 # 若是一个变量是 string,那么调用 to_s 的开销最小 # 但若是变量期待 string 而刚好获得 nil,to_s 也能帮你扭转局面: def fix_title (title) title.to_s.capitalize end
这里还有一些适用于 nil 的最有用的例子:api
irb> nil.to_a ---> [] irb> nil.to_i ---> 0 irb> nil.to_f ---> 0.0
当须要同时考虑多个值的时候,你可使用类 Array 提供的优雅的讨巧方式。Array#compact 方法返回去掉全部 nil 元素的方法接受者的副本。这在将一组可能为 nil 的变量组装成 string 时很经常使用。好比:若是一我的的名字由 first、middle 和 last 组成(其中任何一个均可能为 nil),那么你能够用下面的代码组成这个名字:数组
name = [first, middle, last].compact.join(" ")
nil 对象的嗜好是在你不经意间偷偷溜进正在运行的程序中。不管它来自用户输入、无约束数据库,仍是用 nil 来表示失败的方法,意味着每一个变量均可能为 nil。缓存
$LOAD_PATH
替代 $:
)。大多数长的名字须要在加载库 English 以后才能使用。# 这段代码中有两个 Perl 语法。 # 第一个:使用 String#=~ 方法 # 第二个:在上述代码中看起来好像是使用了一个全局变量 $1 导出第一个匹配组的内容,但其实不是... def extract_error (message) if message =~ /^ERROR:\s+(.+)$/ $1 else "no error" end end # 如下是替代方法: def extract_error (message) if m = message.match(/^ERROR:\s+(.+)$/) m[1] else "no error" end end
最开始接触 Ruby 时,对于常量的认识大概可能就是由大写字母加下划线组成的标识符,例如 STDIN、RUBY_VERSION。不过这并非故事的所有,事实上,由大写字母开头的任何标识符都是常量,包括 String 或 Array,来看看这个:安全
module Defaults NOTWORKS = ["192.168.1","192.168.2"] end def purge_unreachable (networks=Defaults::NETWORKS) networks.delete_if do |net| !ping(net + ".1") end end
若是调用方法 unreadchable 时没有加参数的话,会意外的改变一个常量的值。在 Ruby 中这样作甚至都不会警告你。好在有一种解决这个问题的方法——freeze 方法:
module Defaults NOTWORKS = ["192.168.1","192.168.2"].freeze end
加入你再想改变常量 NETWORKS 的值,purge_unreadchable 方法就会引入 RuntimeError 异常。根据通常的经验,老是经过冻结常量来阻止其被改变,然而不幸的是,冻结 NETWORKS 数组还不够,来看看这个:
def host_addresses (host, networks=Defaults::NETWORKS) networks.map {|net| net << ".#{host}"} end
若是第二个参数没有赋值,那么 host_addresses 方法会修改数组 NETWORKS 的元素。即便数组 NETWORKS 自身被冻结,可是元素仍然是可变的,你可能没法从数组中增删元素,但你必定能够对存在的元素加以修改。所以,若是一个常量引用了一个集合,好比数组或者是散列,那么请冻结这个集合以及其中的元素:
module Defaults NETWORKS = [ "192.168.1", "192.168.2" ].map(&:freeze).freeze end
甚至,要达到防止常量被从新赋值的目的,咱们能够冻结定义它的那个模块:
module Defaults TIMEOUT = 5 end Defaults.freeze
# test.rb def add (x, y) z = 1 x + y end puts add 1, 2 # 使用不带 -w 参数的命令行 irb> ruby test.rb ---> 3 # 使用带 -w 参数的命令行 irb< ruby -w test.rb ---> test.rb:1: warning: parentheses after method name is interpreted as an argument list, not a decomposed argument ---> test.rb:2: warning: assigned but unused variable - z ---> 3
让咱们直接从代码入手吧:
class Person def name ... end end class Customer < Person ... end irb> customer = Customer.new ---> #<Customer> irb> customer.superclass ---> Person irb> customer.respond_to?(:name) ---> true
上面的代码几乎就和你预想的那样,当调用 customer 对象的 name 方法时,Customer 类会首先检查自身是否有这个实例方法,没有那么就继续搜索。
顺着集成体系向上找到了 Person 类,在该类中找到了该方法并将其执行。(若是 Person 类中没有找到的话,Ruby 会继续向上直到到达 BasicObject)
可是若是方法在查找过程当中直到类树的根节点仍然没有找到匹配的办法,那么它将从新从起点开始查找,不过这一次会查找 method_missing 方法。
下面咱们开始让事情变得更加有趣一点:
module ThingsWithNames def name ... end end class Person include(ThingsWithNames) end irb> Person.superclass ---> Object irb> customer = Customer.new ---> #<Customer> irb> customer.respond_to?(:name) ---> true
这里把 name 方法从 Person 类中取出并移到一个模块中,而后把模块引入到了 Person 类。Customer 类的实例仍然能够如你所料响应 name 方法,可是为何呢?显然,模块 ThingsWithNames 并不在集成体系中,由于 Person 类的超类仍然是 Object 类,那会是什么呢?其实,Ruby 在这里对你撒谎了!当你 include 方法来将模块引入类时,Ruby 在幕后悄悄地作了一些事情。它建立了一个单例类并将它插入类体系中。这个匿名的不可见类被链向这个模块,所以它们共享了实力方法和常量。
当每一个模块被类包含时,它会当即被插入集成体系中包含它的类的上方,之后进先出(LIFO)的方式。每一个对象都经过变量 superclass 连接,像单链表同样。这惟一的结果就是,当 Ruby 寻找一个方法时,它将以逆序访问访问每一个模块,最后包含的模块最早访问到。很重要的一点是,模块永远不会重载类中的方法,由于模块插入的位置是包含它的类的上方,而 Ruby 老是会在向上检查以前先检查类自己。
(好吧······这不是所有的事实。确保你阅读了第 35 条,来看看 Ruby 2.0 中的 prepend 方法是如何使其复杂化的)
要点回顾:
class Parent def initialize (name) @name = name end end class Child < Parent def initialize (grade) @grade = grade end end # 你能看到上面的窘境,Ruby 没有提供给子类和其超类的 initialize 方法创建联系的方式 # 咱们可使用通用意义上的 super 关键字来完成继承体系中位于高层的办法: class Child < Parent def initialize (name, grade) super(name) # Initialize Parent. @grade = grade end end
这是一条关于 Ruby 可能会戏弄你的另外一条提醒,要点在于:Ruby 在对变量赋值和对 setter 方法调用时的解析是有区别的!直接看代码吧:
# 这里把 initialize 方法体中的内容当作第 counter= 方法的调用也不是毫无道理 # 事实上 initialize 方法会建立一个新的局部变量 counter,并将其赋值为 0 # 这是由于 Ruby 在调用 setter 方法时要求存在一个显式接受者 class Counter attr_accessor(:counter) def initialize counter = 0 end ... end # 你须要使用 self 充当这个接受者 class Counter attr_accessor(:counter) def initialize self.counter = 0 end ... end # 而在你调用非 setter 方法时,不须要显式指定接受者 # 换句话说,不要使用没必要要的 self,那会弄乱你的代码: class Name attr_accessor(:first, :last) def initialize (first, last) self.first = first self.last = last end def full self.first + " " + self.last # 这里没有调用 setter 方法使用 self 多余了 end end # 就像上面 full 方法里的注释,应该把方法体内的内容改成 first + " " + last
看代码吧:
# 假设你要对一个保存了年度天气数据的 CSV 文件进行解析并存储 # 在 initialize 方法后,你会得到一个固定格式的哈希数组,可是存在如下的问题: # 1.不能经过 getter 方法访问其属性,也不该该将这个哈希数组经过公共接口向外暴露,由于其中包含了实现细节 # 2.每次你想在类内部使用该哈希时,你不得不回头来看 initialize 方法 # 由于你不知道CSV具体的对应是怎样的,并且当类成熟状况可能还会发生变化 require('csv') class AnnualWeather def initialize (file_name) @readings = [] CSV.foreach(file_name, headers: true) do |row| @readings << { :date => Date.parse(row[2]), :high => row[10].to_f, :low => row[11].to_f, } end end end # 使用 Struct::new 方法的返回值赋给一个常量并利用它建立对象的实践: class AnnualWeather # Create a new struct to hold reading data. Reading = Struct.new(:date, :high, :low) def initialize (file_name) @readings = [] 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 # Struct 类自己比你第一次使用时更增强大。除了属性列表,Struct::new 方法还能接受一个可选的块 # 也就是说,咱们能在块中定义实例方法和类方法。好比,咱们定义一个返回平均每个月平均温度的 mean 方法: Reading = Struct.new(:date, :high, :low) do def mean (high + low) / 2.0 end end
# good class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end # better class Person < Struct.new(:first_name, :last_name) end
# good class Person attr_accessor :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end # better Person = Struct.new(:first_name, :last_name) do end
看看下面的 IRB 回话而后自问一下:为何方法 equal? 的返回值和操做符 “==” 的不一样呢?
irb> "foo" == "foo" ---> true irb> "foo".equal?("foo") ---> false
事实上,在 Ruby 中有四种方式来检查对象之间的等价性,下面来简单总个结吧:
要记住在 Ruby 语言中,二元操做符最终会被转换成方法调用的形式,左操做数对应着方法的接受者,右操做数对应着方法第一个也是惟一的那个参数。
# Ruby 语言中,私有方法的行为和其余面向对象的编程语言中不太相同。Ruby 语言仅仅在私有方法上加了一条限制————它们不能被显式接受者调用 # 不管你在继承关系中的哪一级,只要你没有使用接受者,你均可以调用祖先方法中的私有方法,可是你不能调用另外一个对象的私有方法 # 考虑下面的例子: # 方法 Widget#overlapping? 会检测其自己是否和另外一个对象在屏幕上重合 # Widget 类的公共接口并无将屏幕坐标对外暴露,它们的具体实现都隐藏在了内部 class Widget def overlapping? (other) x1, y1 = @screen_x, @screen_y x2, y2 = other.instance_eval {[@screen_x, @screen_y]} ... end end # 能够定义一个暴露私有屏幕坐标的方法,但并不经过公共接口来实现,其实现方式是声明该方法为 protected # 这样咱们既保持了原有的封装性,也使得 overlapping? 方法能够访问其自身以及其余传入的 widget 实例的坐标 # 这正式设计 protected 方法的缘由————在相关类之间共享私有信息 class Widget def overlapping? (other) x1, y1 = @screen_x, @screen_y x2, y2 = other.screen_coordinates ... end protected def screen_coordinates [@screen_x, @screen_y] end end
在 Ruby 中多数对象都是经过引用而不是经过实际值来传递的,当将这种类型的对象插入容器时,集合类实际存储着该对象的引用而不是对象自己。
(值得注意的是,这条准则是个例如:Fixnum 类的对象在传递时老是经过值而不是引用传递)
这也就意味着当你把集合做为参数传入某个方法并进行修改时,原始集合也会所以被修改,有点间接,不过很容易看到这种状况的发生。
Ruby 语言自带了两个用来复制对象的方法:dup 和 clone。
它们都会基于接收者建立新的对象,可是与 dup 方法不一样的是,clone 方法会保留原始对象的两个附加特性。
首先,clone 方法会保留接受者的冻结状态。若是原始对象的状态是冻结的,那么生成的副本也会是冻结的。而 dup 方法就不一样了,它永远不会返回冻结的对象。
其次,若是接受这种存在单例方法,使用 clone 也会复制单例类。因为 dup 方法不会这样作,因此当使用 dup 方法时,原始对象和使用 dup 方法建立的副本对于相同消息的响应多是不一样的。
# 也可使用 Marshal 类将一个集合及其所持有的元素序列化,而后再反序列化: irb> a = ["Monkey", "Brains"] irb> b = Marshal.load(Marshal.dump(a)) irb> b.each(&:upcasel); b.first ---> "MONKEY" irb> a.last ---> "Brains"
# 考虑下面这样一个订披萨的类: class Pizza def initialize (toppings) toppings.each do |topping| add_and_price_topping(topping) end end end # 上面的 initialize 方法期待的是一个 toppings 数组,但咱们能传入单个 topping,甚至是在没有 topping 对象的时候直接传入 nil # 你可能会想到使用可变长度参数列表来实现它,并将参数类型改成 *topping,这样会把全部的参数整合成一个数组。 # 尽管这样作可让咱们传入单个 topping 对象,担当传入一组对象给 initialize 方法的时候必须使用 "*" 显式将其拓展成一个数组。 # 因此这样作仅仅是拆东墙补西墙罢了,一个更好的解决方式是将传入的参数转换成一个数组,这样咱们就明确地知道我要作的是什么了 # 先对 Array() 作一些探索: irb> Array('Betelgeuse') ---> ["Betelgeuse"] irb> Array(nil) ---> [] irb> Array(['Nadroj', 'Retep']) ---> ["Nadroj", "Retep"] irb> h = {pepperoni: 20,jalapenos: 2} irb> Array(h) ---> [[:pepperoni, 20], [:jalapenos, 2]] # 若是你想处理一组哈希最好采用第 10 条的建议那样 # 回答订披萨的问题上: # 通过一番改造,它如今可以接受 topping 数组、单个 topping,或者没有 topping(nil or []) class Pizza def initialize (toppings) Array(toppings).each do |topping| add_and_price_topping(topping) end end ... end
(书上对于这一条建议的描述足足有 4 页半,但其实能够看下面结论就ok,结尾有实例代码)
# 原始版本 class Role def initialize (name, permissions) @name, @permissions = name, permissions end def can? (permission) @permissions.include?(permission) end end # 版本1.0:使用 Hash 替代 Array 的 Role 类: # 这样作基于两处权衡,首先,由于哈希只存储的键,因此数组中的任何重复在转换成哈希的过程当中都会丢失。 # 其次,为了可以将数组转换成哈希,须要将整个数组映射,构建出一个更大的数组,从而转化为哈希。这将性能问题从 can? 方法转移到了 initialize 方法 class Role def initialize (name, permissions) @name = name @permissions = Hash[permissions.map {|p| [p, ture]}] end def can? (permission) @permissions.include?(permission) end end # 版本2.0:引入 Set: # 性能几乎和上一个哈希版本的同样 require('set') class Role def initialize (name, permissions) @name, @permissions = name, Set.new(permissions) end def can? (permission) @permissions.include?(permission) end end # 最终的例子 # 这个版本自动保证了集合中没有重复的记录,且重复条目是很快就能被检测到的 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
尽管可能有点云里雾里,但仍是考虑考虑先食用代码吧:
# reduce 方法的参数是累加器的起始值,块的目的是建立并返回一个适用于下一次块迭代的累加器 # 若是原始集合为空,那么块永远也不会被执行,reduce 方法仅仅是简单地返回累加器的初始值 # 要注意块并无作任何赋值。这是由于在每一个迭代后,reduce 丢弃上次迭代的累加器并保留了块的返回值做为新的累加器 def sum (enum) enum.reduce(0) do |accumulator, element| accumulator + element end end # 另外一个快捷操做方式对处理块自己很方便:能够给 reduce 传递一个符号(symbol)而不是块。 # 每一个迭代 reduce 都使用符号做为消息名称发送消息给累加器,同时将当前元素做为参数 def sum (enum) enum.reduce(0, :+) end # 考虑一下把一个数组的值所有转换为哈希的键,而它们的值都是 true 的状况: Hash[array.map {|x| [x, true]}] # reduce 可能会提供更加完美的方案(注意此时 reduce 的起始值为一个空的哈希): array.reduce({}) do |hash, element| hash.update(element => true) end # 再考虑一个场景:咱们须要从一个存储用户的数组中筛选出那些年龄大于或等于 21 岁的人群,以后咱们但愿将这个用户数组转换成一个姓名数组 # 在没有 reduce 的时候,你可能会这样写: users.select {|u| u.age >= 21}.map(&:name) # 上面这样作固然能够,但并不高效,缘由在于咱们使用上面的语句时对数组进行了屡次遍历 # 第一次是经过 select 筛选出了年龄大于或等于 21 岁的人,第二次则还须要映射成只包含名字的新数组 # 若是咱们使用 reduce 则无需建立或遍历多个数组: users.reduce([]) do |names, user| names << user.name if user.age >= 21 names end
引入 Enumerable 模块的类会获得不少有用的实例方法,它们可用于对对象的集合进行过滤、遍历和转化。其中最为经常使用的应该是 map 和 select 方法,这些方法是如此强大以致于在几乎全部的 Ruby 程序中你都能见到它们的影子。
像数组和哈希这样的集合类几乎已是每一个 Ruby 程序不可或缺的了,若是你还不熟悉 Enumberable 模块中定义的方法,你可能已经本身写了至关多的 Enumberable 模块已经具有的方法,知识你还不知道而已。
Enumberable 模块
戳开 Array 的源码你能看到 include Enumberable 的字样(引入的类必须实现 each 方法否则报错),咱们来简单阐述一下 Enumberable API:
irb> [1, 2, 3].map {|n| n + 1} ---> [2, 3, 4] irb> %w[a l p h a b e t].sort ---> ["a", "a", "b", "e", "h", "l", "p", "t"] irb> [21, 42, 84].first ---> 21上面的代码中:
- 首先,咱们使用了流行的 map 方法遍历每一个元素,并将每一个元素 +1 处理,而后返回新的数组;
- 其次,咱们使用了 sort 方法对数组的元素进行排序,排序采用了 ASCII 字母排序
- 最后,咱们使用了查找方法 select 返回数组的第一个元素
reduce 方法到底干了什么?它为何这么特别?在函数式编程的范畴中,它是一个能够将一个数据结构转换成另外一种结构的折叠函数。
让咱们先从宏观的角度来看折叠函数,当使用如 reduce 这样的折叠函数时你须要了解以下三部分:
此时了解了这三部分你能够回头再去看一看代码。
试着回想一下上一次使用 each 的场景,reduce 可以帮助你改善相似下面这样的模式:
hash = {} array.each do |element| hash[element] = true end
我肯定你是一个曾经在块的语法上徘徊许久的 Ruby 程序员,那么请告诉我,下面这样的模式在代码中出现的频率是多少?
def frequency (array) array.reduce({}) do |hash, element| hash[element] ||= 0 # Make sure the key exists. hash[element] += 1 # Then increment it. hash # Return the hash to reduce. end end
这里特意使用了 "||=" 操做符以确保在修改哈希的值时它是被赋过值的。这样作的目的其实也就是确保哈希能有一个默认值,咱们能够有更好的替代方案:
def frequency (array) array.reduce(Hash.new(0)) do |hash, element| hash[element] += 1 # Then increment it. hash # Return the hash to reduce. end end
看上去还真是那么一回事儿,可是当心,这里埋藏着一个隐蔽的关于哈希的陷阱。
# 先来看一下这个 IRB 会话: irb> h = Hash.new(42) irb> h[:missing_key] ---> 4二、 irb> h.keys # Hash is still empty! ---> [] irb> h[:missing_key] += 1 ---> 43 irb> h.keys # Ah, there you are. ---> [:missing_key] # 注意,当访问不存在的键时会返回默认值,但这不会修改哈希对象。 # 使用 "+=" 操做符的确会像你想象中那般更新哈希,但并不明确,回顾一下 "+=" 操做符会展开成什么可能会颇有帮助: # Short version: hash[key] += 1 # Expands to: hash[key] = hash[key] + 1 # 如今赋值的过程就很明确了,先取得默认值再进行 +1 的操做,最终将其返回的结果以一样的键名存入哈希 # 咱们并无以任何方式改变默认值,固然,上面一段代码的默认值是数字类型,它是不能修改的 # 可是若是咱们使用一个能够修改的值做为默认值并在以后使用了它状况将会变得更加有趣: irb> h = Hash.new([]) irb> h[:missing_key] ---> [] irb> h[:missing_key] << "Hey there!" ---> ["Hey there!"] irb> h.keys # Wait for it... ---> [] irb> h[:missing_key] ---> ["Hey there!"] # 看到上面关于 "<<" 的小骗局了吗?我从没有改变哈希对象,当我插入一个元素以后,哈希并么有改变,可是默认值改变了 # 这也是 keys 方法提示这个哈希是空可是访问不存在的键时却反悔了最近修改的值的缘由 # 若是你真想插入一个元素并设置一个键,你须要更深刻的研究,但另外一个不明显的反作用正等着你: irb> h = Hash.new([]) irb> h[:weekdays] = h[:weekdays] << "Monday" irb> h[:months] = h[:months] << "Januray" irb> h.keys ---> [:weekdays, :months] irb> h[:weekdays] ---> ["Monday", "January"] irb> h.default ---> ["Monday", "Januray"] # 两个键共享了同一个默认数组,多数状况你并不想这么作 # 咱们真正想要的是当咱们访问不存在的键时能返回一个全新的数组 # 若是给 Hash::new 一个块,当须要默认值时这个块就会被调用,并友好地返回一个新建立的数组: irb> h = Hash.new{[]} irb> h[:weekdays] = h[:weekdays] << "Monday" ---> ["Monday"] irb> h[:months] = h[:months] << "Januray" ---> ["Januray"] irb> h[:weekdays] ---> ["Monday"] # 这样好多了,但咱们还能够往前一步。 # 传给 Hash::new 的块能够有选择地接受两个参数:哈希自己和将要访问的键 # 这意味着咱们若是想去改变哈希也是可的,那么当访问一个不存在的键时,为何不将其对应的值设置为一个新的空数组呢? irb> h = Hash.new{|hash, key| hash[key] = []} irb> h[:weekdays] << "Monday" irb> h[:holidays] ---> [] irb> h.keys ---> [:weekdays, :holidays] # 你可能发现上面这样的技巧存在着重要的不足:每当访问不存在的键时,块不只会在哈希中建立新实体,同时还会建立一个新的数组 # 重申一遍:访问一个不存在的键会将这个键存入哈希,这暴露了默认值存在的通用问题: # 正确的检查一个哈希是否包含某个键的方式是使用 hash_key? 方法或使用它的别名,可是深感内疚的是一般状况下默认值是 nil: if hash[key] ... end # 若是一个哈希的默认值不是 nil 或者 false,这个条件判断会一直成功:将哈希的默认值设置成非 nil 可能会使程序变得不安全 # 另外还要提醒的是:经过获取其值来检查哈希某个键存在与否是草率的,其结果也可能和你所预期的不一样 # 另外一种处理默认值的方式,某些时候也是最好的方式,就是使用 Hash#fetch 方法 # 该方法的第一个参数是你但愿从哈希中查找的键,可是 fetch 方法能够接受一个可选的第二个参数 # 若是指定的 key 在当前的哈希中找不到,那么取而代之,fetch 的第二个参数会返回 # 若是你省略了第二个参数,在你试图获取一个哈希中不存在的键时,fetch 方法会抛出一个异常 # 相比于对整个哈希设置默认值,这种方式更加安全 irb> h = {} irb> h[:weekdays] = h.fetch(:weekdays, []) << "Monday" ---> ["Monday"] irb> h.fetch(:missing_key) keyErro: key not found: :missing_key
因此看过上面的代码框隐藏的内容后你会发现:
这一条也能够被命名为“对于核心类,优先使用委托而非继承”,由于它一样适用于 Ruby 的全部核心类。
Ruby 的全部核心类都是经过 C语言 来实现的,指出这点是由于某些类的实例方法并无考虑到子类,好比 Array#reverse 方法,它会返回一个新的数组而不是改变接受者。
猜猜若是你继承了 Array 类并调用了子类的 reverse 方法后会发生什么?
# 是的,LikeArray#reverse 返回了 Array 实例而不是 LikeArray 实例 # 但你不该该去责备 Array 类,在文档中有写的很明白会返回一个新的实例,因此达不到你的预期是很天然的 irb> class LikeArray < Array; end irb> x = LikeArray.new([1, 2, 3]) ---> [1, 2, 3] irb> y = x.reverse ---> [3, 2, 1] irb> y.class ---> Array
固然还不止这些,集合上的许多其余实例方法也是这样,集成比较操做符就更糟糕了。
好比,它们容许子类的实例和父类的实例相比较,这说得通嘛?
irb> LikeArray.new([1, 2, 3]) == [1, 2, 3,] ---> true
继承并非 Ruby 的最佳选择,从核心的集合类中继承更是毫无道理的,替代方法就是使用“委托”。
让咱们来编写一个基于哈希但有一个重要不一样的类,这个类在访问不存在的键时会抛出一个异常。
实现它有不少不一样的方式,但编写一个新类让咱们能够简单的重用同一个实现。
与继承 Hash 类后为保证正确而处处修修补补不一样,咱们这一次采用委托。咱们只须要一个实例变量 @hash,它会替咱们干全部的重活:
# 在 Ruby 中实现委托的方式有不少,Forwardable 模块让使用委托的过程很是容易 # 它将一个存有要代理的方法的链表绑定到一个实例变量上,它是标准库的一部分(不是核心库),这也是须要显式引入的缘由 require('forwardable') class RaisingHash extend(Forwardable) include(Enumerbale) def_delegators(:@hash, :[], :[]=, :delete, :each, :keys, :values, :length, :empty?, :hash_key?) end
(更多的探索在书上.这里只是简单给一下结论.感兴趣的童鞋再去看看吧!)
因此要点回顾一下:
class TemperatureError < StandardError attr_reader(:temperature) def initialize(temperature) @temperature = temperature super("invalid temperature: #@temperature") end end
垃圾收集器是个复杂的软件工程。从很高的层次看,Ruby 垃圾收集器使用一种被称为 标记-清除(mark and sweep)的过程。(熟悉 Java 的童鞋应该会感到一丝熟悉)
首先,遍历对象图,能被访问到的对象会被标记为存活的。接着,任何未在第一阶段标记过的对象会被视为垃圾并被清楚,以后将内存释放回 Ruby 或操做系统。
遍历整个对象图并标记可访问对象的开销太大。Ruby 2.1 经过新的分代式垃圾收集器对性能进行了优化。对象被分为两类,年轻代和年老代。
分代式垃圾收集器基于一个前提:大多数对象的生存时间都不会很长。若是咱们知道了一个对象能够存活好久,那么就能够优化标记阶段,自动将这些老的对象标记为可访问,而不须要遍历整个对象图。
若是年轻代对象在第一阶段的标记中存活了下来,那么 Ruby 的分代式垃圾收集器就把它们提高为年老代。也就是说,他们依然是可访问的。
在年轻代对象和年老代对象的概念下,标记阶段能够分为两种模式:主要标记阶段(major)和次要标记阶段(minor)。
在主要标记阶段,全部的对象(不管新老)都会被标记。该模式下,垃圾收集器不区分新老两代,因此开销很大。
次要标记阶段,仅仅考虑年轻代对象,并自动标记年老代对象,而不检查可否被访问。这意味着年老代对象只会在主要标记阶段以后才会被清除。除非达到了一些阈值,保证整个过程所有做为主要标记以外,垃圾收集器倾向于使用次要标记。
垃圾收集器的清除阶段也有优化机制,分为两种模式:即便模式和懒惰模式。
在即便模式中,垃圾收集器会清除全部的未标记的对象。若是有不少对象须要被释放,那这种模式开销就很大。
所以,清除阶段还支持懒惰模式,它将尝试释放尽量少的对象。
每当 Ruby 中建立一个新对象时,它可能尝试触发一次懒惰清除阶段,去释放一些空间。为了更好的理解这一点,咱们须要看看垃圾收集器如何管理存储对象的内存。(简单归纳:垃圾收集器经过维护一个由页组成的堆来管理内存。页又由槽组成。每一个槽存储一个对象。)
咱们打开一个新的 IRB 会话,运行以下命令:
`IRB``> ``GC``.stat` `---> {``:count``=>``9``, ``:heap_length``=>``126``, ...}`
GC::stat 方法会返回一个散列,包含垃圾收集器相关的全部信息。请记住,该散列中的键以及它们对应垃圾收集器的意义可能在下一个版本发生变化。
好了,让咱们来看一些有趣的键:
键名 | 说明 |
---|---|
count | 垃圾收集器运行的总次数 |
major_gc_count | 主要模式下的运行次数 |
minor_gc_count | 次要模式下的运行次数 |
total_allocated_object | 程序开始时分配的对象总数 |
total_freed_object | Ruby 释放的对象总数。与上面之差表示存活对象的数量,这能够经过 heap_live_slot 键来计算 |
heap_length | 当前堆中的页数 |
heap_live_slot 和 heap_free_slot | 表示所有页中被使用的槽数和未被使用的槽数 |
old_object | 年老代的对象数量,在次要标记阶段不会被处理。年轻代的对象数量能够用 heap_live_slot 减去 old_object 来得到 |
该散列中还有几个有趣的数字,但在介绍以前,让咱们来学习垃圾收集器的最后一个要点。还记得对象是存在槽中的吧。Ruby 2.1 的槽大小为 40 字节,然而并非全部的对象都是这么大。
好比,一个包含 255 个字节的字符串对象。若是对象的大小超过了槽的大小,Ruby 就会额外向操做系统申请一块内存。
当对象被销毁,槽被释放后,Ruby 会把多余的内存还给操做系统。如今让咱们看看 GC::stat 散列中的这些键:
键名 | 说明 |
---|---|
malloc_increase | 全部超过槽大小的对象所占用的总比特数 |
malloc_limit | 阈值。若是 malloc_increase 的大小超过了 malloc_limit,垃圾收集器就会在次要模式下运行。一个 Ruby 应用程序的生命周期里,malloc_limit 是被动调整的。它的大小是当前 malloc_increase 的大小乘以调节因子,这个因子默认是 1.4。你能够经过环境变量 RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR 来设定这个因子 |
oldmalloc_increase 和 oldmalloc_limit | 是上面两个对应的年老代值。若是 oldmalloc_increase 的大小超过了 oldmalloc_limit,垃圾收集器就会在主要模式下运行。oldmalloc_limit 的调节因子more是 1.2。经过环境变量 RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR 能够设定它 |
做为最后一部分,让咱们来看针对特定应用程序进行垃圾收集器调优的环境变量。
在下一个版本的 Ruby 中,GC::stat 散列中的值对应的环境变量可能会发生变化。好消息是 Ruby 2.2 将支持 3 个分代,Ruby 2.1 只支持两个。这可能会影响到上述变量的设定。
有关垃圾收集器调优的环境变量的权威信息保存在 "gc.c" 文件中,是 Ruby 源程序的一部分。
下面是 Ruby 2.1 中用于调优的环境变量(仅供参考):
环境变量名 | 说明 |
---|---|
RUBY_GC_HEAP_INIT_SLOTS | 初始槽的数量。默认为 10k,增长它的值可让你的应用程序启动时减小垃圾收集器的工做效率 |
RUBY_GC_HEAP_FREE_SLOTS | 垃圾收集器运行后,空槽数量的最小值。若是空槽的数量小于这个值,那么 Ruby 会申请额外的页,并放入堆中。默认值是 4096 |
RUBY_GC_HEAP_GROWTH_FACTOR | 当须要额外的槽时,用于计算须要增长的页数的乘数因子。用已使用的页数乘以这个因子算出还须要增长的页数、默认值是 1.8 |
RUBY_GC_HEAP_GROWTH_MAX_SLOTS | 一次添加到堆中的最大槽数。默认值是0,表示没有限制。 |
RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR | 用于计算出发主要模式垃圾收集器的门限值的乘数因子。门限由前一次主要清除后年老代对象数量乘以该因子获得。该门限与当前年老代对象数量成比例。默认值是 2.0。这意味着若是年老代对象在上次主要标记阶段事后的数量翻倍的话,新一轮的主要标记过程将被出发。 |
RUBY_GC_MALLOC_LIMIT | GC::stat 散列中 malloc_limit 的最小值。若是 malloc_increase 超过了 malloc_limit 的值,那么次要模式垃圾收集器就会运行一次。该设定用于确保 malloc_increase 不会小于特定值。它的默认值是 16 777 216(16MB) |
RUBY_GC_MALOC_LIMIT_MAX | 与 RUBY_GC_MALLOC_LIMIT 相反的值,这个设定保证 malloc_limit 不会变得过高。它能够被设置成 0 来取消上限。默认值是 33 554 432(32MB) |
RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR | 控制 malloc_limit 如何增加的乘数因子。新的 malloc_limit 值由当前 malloc_limit 值乘以这个因子来得到,默认值为 1.4 |
RUBY_GC_OLDMALLOC_LIMIT | 年老代对应的 RUBY_GC_MALLOC_LIMIT 值。默认值是 16 777 216(16MB) |
RUBY_GC_OLDMALLOC_LIMIT_MAX | 年老代对应的 RUBY_GC_MALLOC_LIMIT_MAX 值。默认值是 134 217 728(128MB) |
RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR | 年老代对应的 RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR 值。默认值是 1.2 |
周末学习了两天才勉强看完了一遍,对于 Ruby 语言的有一些高级特性仍是比较吃力的,须要本身反反复复的看才能理解一二。不过好在也是有收获吧,没有白费本身的努力,特意总结一个精简版方便后面的童鞋学习。
另外这篇文章最开始是使用公司的文档空间建立的,发现 Markdown 虽然精简易于使用,可是功能性上比一些成熟的写文工具要差上不少,就好比对代码的支持吧,用公司的代码块还支持自定义标题、显示行号、是否能缩放、主题等一系列自定义的东西,写出来的东西也更加友好...
按照惯例黏一个尾巴:
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz 欢迎关注公众微信号:wmyskxz 分享本身的学习 & 学习资料 & 生活 想要交流的朋友也能够加qq群:3382693