让我们阅读 – 精炼 Ruby 编程 – 第 1 章 – 充分利用核心类

2025-06-11

阅读指南 – 精炼 Ruby 编程 – 第 1 章

第 1 章 — — 充分利用核心课程

《精炼 Ruby 编程》是Jeremy Evans最近出版的一本书,他是一位知名的 Ruby 程序员,参与了 Ruby 核心团队、Roda、Sequel 以及其他多个项目的开发。由于了解 Jeremy 以及他丰富的经验,我立刻就买了这本书,并且非常期待这本书的内容。

您可以在这里找到这本书:

https://www.packtpub.com/product/polished-ruby-programming/9781801072724

这篇书评与以往其他“一起阅读”系列一样,将逐章逐节逐行解读,并添加评论、补充说明和对内容的总体看法。请记住,书籍在每页纸上所能容纳的信息量是有限的,不可能涵盖所有内容。

话虽如此,让我们继续开始吧。

第 1 章 — — 充分利用核心课程

本书首先概述了核心课程以及以下主题:

  • 了解何时使用核心类
  • true、false 和 nil 对象的最佳用途
  • 满足不同需求的不同数字类型
  • 了解符号与字符串的区别
  • 学习如何最好地使用数组、哈希和集合
  • 使用 Struct —— 未被充分重视的核心类之一

我们将逐一介绍这些内容。乍一看,这是对 Ruby 中常见令人困惑的主题的一个很好的概述。

了解何时使用核心类

我们从两个示例开始,一个使用Array,另一个使用自定义类ThingList

things = ["foo", "bar", "baz"]
things.each do |thing|
  puts thing
end

things = ThingList.new("foo", "bar", " baz")
things.each do |thing|
  puts thing
end
Enter fullscreen mode Exit fullscreen mode

这里要指出的是,第一种比第二种更清晰。ThingList与更为人熟知的相比,使用 会带来很多不确定性Array,尤其是因为正如前面提到的,为什么有人会用 而不是 呢Array

关于扩展核心类以及其中可能发生的一些不良后果,有很多讨论,其中一篇特别的讨论是Michael Herold 的“让我们子类化哈希——最糟糕的情况是什么?”。简而言之,这个Hashiegem 尝试实现点访问(hash[:a]也可以称为 as hash.a),但随之而来的是各种各样的问题。

杰里米在这里的观点是正确的:只有当你知道风险和所得利益大于风险时,才可以采用定制。

性能、直观理解、可维护性等风险经常出现,应该予以考虑。

truefalsenil对象的最佳用途

真假

truefalse是相当通用的概念,正如前文所述,如果它们满足你的需求,就应该使用它们。然而,需要注意的是,它们是 和 的实例,TrueClass除非你使用像 Steep 或 Sorbet 这样的语法,FalseClass否则 Ruby 实际上并没有 的概念Boolean

第一种使用它们的情况是谓词方法,或者?在 Ruby 中以 结尾的方法:

1.kind_of?(Integer)
# => true
Enter fullscreen mode Exit fullscreen mode

给出的其他例子涉及等式和不等式:

1 > 2
# => false

1 == 1
# => true
Enter fullscreen mode Exit fullscreen mode

注意:===在 Ruby 中的行为非常不同,但这是以后讨论的主题

对我来说,关键在于你是否在回答一个问题。对于谓词方法来说,答案很明确,但对于等式和不等式来说,答案可能不那么明确。另一个常见的用法是状态更新,比如某件事是成功了还是失败了?当然,这些用法通常更多地使用元组类型的对,比如[true, response]or [false, error],但这个话题留到以后再说。

接下来他介绍nil一些常见的用法:

[].first
# => nil

{1=>2}[3]
# => nil
Enter fullscreen mode Exit fullscreen mode

nil应该理解为什么都没有,当没有东西可返回时,我们就返回它。在第一种情况下,没有 的第一个元素Array;在第二种情况下,没有 的键3

注意: Hash可以通过Hash.new(0)或分配一个默认值Hash.new { |h, k| h[k] = [] },以覆盖“没有”的想法,但这超出了这里的观点。

棘手的部分(也是之前提到的部分!niltrue!1false

!nil
# => true

!1
# => false
Enter fullscreen mode Exit fullscreen mode

这让我们得到像这样的模式来“强制”Boolean类似的价值观:

!!nil
Enter fullscreen mode Exit fullscreen mode

一般来说nil,除非真的“什么都没有”,否则应该避免这种情况。考虑一下这种情况:

[1, 2, 3].select { |v| v > 4 }
# => []
Enter fullscreen mode Exit fullscreen mode

当然,我们发现“什么也没有”,但更好的答案是空,Array也就是这个特殊情况下的“什么也没有”。如果我们返回nil并尝试这样做,你认为会发生什么?

[1, 2, 3].select { |v| v > 4 }.map { |v| v * 2 }
Enter fullscreen mode Exit fullscreen mode

你可能会遇到一些错误。在这种特殊情况下,[1, 2, 3]那里没有“任何东西”,但在其他情况下,例如[4, 5, 6]?,这是有效的。你可能会注意到这里有一些带有“空”或“无”值的模式,但这很容易进入函数式编程的领域,这是一个非常有趣的想法,如果你特别喜欢冒险,可以在这里阅读更多内容。

重点是,返回合理的默认值,而不是nil有意义的值。

Bang( !) 方法和 Nil

接下来是 Ruby 中一些比较令人困惑的部分,特别是围绕 bang ( !) 方法:

"a".gsub!('b', '')
# => nil

[2, 4, 6].select!(&:even?)
# => nil

["a", "b", "c"].reject!(&:empty?)
# => nil
Enter fullscreen mode Exit fullscreen mode

Jeremy 提到,这样做是为了优化,确保接收方不会进行修改。对我来说,这也是我避免!频繁使用某些方法的原因,因为我不止一次遇到过这种情况,而且很多时候你真的不需要它们。我的一般规则是,除非绝对必要,否则避免修改和修改方法,因为这会破坏链式调用以及对 Ruby 工作原理的很多直觉理解。

使用 false 和 nil 进行缓存

在提供的两个示例中:

@cached_value ||= some_expression

# or

cache[:key] ||= some_expression
Enter fullscreen mode Exit fullscreen mode

如果some_expressionfalse,则nil它会重新评估,而不是“缓存”以供以后使用。建议的替代方案是使用defined?

if defined?(@cached_value)
  @cached_value
else
  @cached_value = some_expression
end
Enter fullscreen mode Exit fullscreen mode

就我个人而言,我倾向于使用基于方法的缓存的守卫式语句,但这是一个偏好问题:

def another_expression
  return @cached_value if defined?(@cached_value)
  @cached_value = some_expression
end
Enter fullscreen mode Exit fullscreen mode

哈希缓存

他还提到了Hash使用 es 进行缓存,fetch它还有一些有趣的行为:

cache.fetch(:key) { cache[:key] = some_expression }
Enter fullscreen mode Exit fullscreen mode

有几种方法可以fetch完成一些重要的事情,值得在这里提一下:

hash = { a: 1 }
# => {:a=>1}

hash.fetch(:a)
# => 1

hash.fetch(:b, 1)
# => 1
hash.fetch(:b) { 1 }
# => 1

hash.fetch(:b)
# KeyError (key not found: :b)
Enter fullscreen mode Exit fullscreen mode

如果您fetch对一个不存在的值没有使用默认或提供的块,它将引发一个KeyError,这对于确保事物存在非常有用。

内存优势

最后需要补充的是,由于truefalsenil是直接对象类型,它们的执行速度会比大多数其他 Ruby 对象更快。这意味着创建时无需分配内存,后续访问时也无需进行间接访问,因此它们比非直接对象更快。

满足不同需求的不同数字类型

接下来我们来看看不同的数字类型。Jeremy 开门见山地指出,在大多数情况下,你可能只需要一个Integer类型,而不是分数类型。Ruby 还提供了浮点数、有理数和 BigDecimal 等一些非十进制类型的变量。它们都属于这个Numeric类别。

注意: - 如上所述,BigDecimal默认情况下不需要:require 'big_decimal'。它还有一个特别麻烦的兼容性问题,BigDecimal.new会导致与 发生冲突BigDecimal()。我仍然不明白他们为什么不保留它而只是给它起个别名,但可惜我们就是这样。

他首先举了一个例子times

10.times do
  # executed 10 times
end
Enter fullscreen mode Exit fullscreen mode

这里可能最好也包含块变量并指出它接收每个值:

3.times do |i|
  puts i
end
# 0
# 1
# 2
Enter fullscreen mode Exit fullscreen mode

...因为示例引用了for循环等效性,这可能会导致一些混淆和引入计数器变量,其中已经内置了一个计数器变量来涵盖这种情况。

整数除法和截断

关于 s 的一个常见混淆点Integer以及他在这里提出的一个混淆点是截断时会发生什么:

5 / 10
# => 0

7 / 3
# => 2
Enter fullscreen mode Exit fullscreen mode

这可能并不是我们想要的结果,因此在将其中一个数字转换为不同的数字类型时要小心Rational(因为Float它有自己的一些乐趣,我们稍后会介绍。)

它只返回商,不返回余数或小数部分。这和 C 语言类似,而且挺有意思的,在某些公司面试时会问这个问题。

浮点数

书中提到的解决方法RationalFloat在这里:

# or Rational(5, 10) or 5 / 10.to_r
5 / 10r
# => (1/2)

# Float
7.0 / 3
# => 2.3333333333333335
Enter fullscreen mode Exit fullscreen mode

Float虽然被认为是最快的,但速度并不精确。这个网站对原因有一个很好的解释,但简而言之,就是没有足够的数字来表示所有数字,而且你对 进行的操作越多,Float这一点就越明显,例如:

f = 1.1
v = 0.0

1000.times do
  v += f
end

v
# => 1100.0000000000086
Enter fullscreen mode Exit fullscreen mode

有理数

Rational可以更精确地解决这个问题,但总体来说速度较慢。如果你处理的是金钱或其他需要精确计算的事情,那么使用这种方法Float是个坏主意。

如果我们使用相同的代码来Rational代替,书中会显示以下内容:

f = 1.1r
v = 0.0r

1000.times do
  v += f
end

v
# => (1100/1)
Enter fullscreen mode Exit fullscreen mode

现在就速度而言,Jeremy 提出了一个很好的观点,让人想起了 YAGNI(你不需要它)。它们的速度可能要慢 2-6 倍,而且微优化很少会成为代码的瓶颈。

正如他在书中提到的,理性在你需要精确答案的时候非常有用,而正如之前提到的,金钱绝对是其中之一。但如果你只是比较数字而不是进行计算呢?嗯,Float可能没问题。

大十进制

那么,这个等式中还有什么意义呢BigDecimal?让我们看一下提供的示例:

v = BigDecimal(1) / 3
v * 3
# => 0.999999999999999999e0

f = BigDecimal(1.1, 2)
v = BigDecimal(0)

1000.times do
  v += f
end

v
# => 0.11e4

v.to_s('F')
# => "1100.0"
Enter fullscreen mode Exit fullscreen mode

BigDecimal正如其名称所示,它使用科学计数法,因此可以处理非常大的数字。本书并没有对此进行过多的详细介绍,而且坦白说,我自己在 Ruby 中也很少使用它们。

我个人很喜欢HoneyBadger发表的这篇关于货币以及何时BigDecimalRational可能使用货币的文章。

了解符号与字符串的区别

如果说 Ruby 中有一个问题比其他大多数问题加起来还要让人困惑,那就是SymbolvsString以及两者同时使用的情况。我对此有一些个人看法,但我会留到以后再说。

正如书中所述,Rails 会将它们一视同仁地处理,以此来避免这种烦人的问题,Hash#with_indifferent_access从而避免关注它们之间的区别。正如书中所述,在后台,许多 Ruby 也会进行这种转换。

那么这两者是什么?

字符串

Ruby 中的字符串是一系列字符或字节,用于存储文本或二进制数据。除非字符串被冻结,否则可以向其添加内容、修改其中的现有字符或将其替换为其他字符串。

在大多数情况下,我都会主张冻结Strings,Ruby 甚至有冻结字符串文字注释来执行此操作,该注释位于文件顶部:

# frozen_string_literal: true
Enter fullscreen mode Exit fullscreen mode

事实证明,这种方法可以提升应用程序性能,而且通常更容易操作,因为变异(尤其是在接收器上)可能会带来各种意想不到的后果。我们不会就此展开函数纯度之争,但一般来说,Ruby 中的变异方法会使代码推理更加困难,因此请谨慎使用。

我稍后会提到这一点,但如果冻结字符串文字是默认设置,那么很多用例Symbol将变得更难以证明,尽管它们的实现仍然会带来一些边际性能提升。

象征

Ruby 中的符号是一个附加了标识符的数字,该标识符是一系列字符或字节。Ruby 中的符号是 Ruby 内部类型的对象包装器,该类型称为ID,是一种整数类型。在 Ruby 代码中使用符号时,Ruby 会查找与该标识符关联的数字。内部使用ID类型的原因是,对于计算机来说,处理整数比处理一系列字符或字节要快得多。Ruby 使用ID值来引用局部变量、实例变量、类变量、常量和方法名称。

解释 可能有点复杂Symbol,但确实涉及到一些重要的实现细节。更简单地说,Symbol是用于描述 Ruby 代码某一部分的标识性文本。

例如,方法可以通过Symbol表示其名称来识别,就像def add可以:add在程序的其他地方表示的那样,并传递send给检索方法代码:

method = :add
foo.send(method, bar)
Enter fullscreen mode Exit fullscreen mode

警告:我个人更喜欢method_name这里,因为method它本身是一种可以method通过名称获取的方法,这可能会造成混淆。

令人困惑的是,这也有效,正如书中提到的:

method = "add"
foo.send(method, bar)
Enter fullscreen mode Exit fullscreen mode

正如书中所述,这是因为 Ruby 试图对程序员友善,而且说实话,我觉得它有点自知之明,知道这很容易让人困惑。许多String方法都会在 上起作用Symbol,这加剧了这种情况。

书中提到了以下几个例子:

def switch(value)
  case value
  when :foo
    # foo
  when :bar
    # bar
  when :baz
    # baz
  end
end
Enter fullscreen mode Exit fullscreen mode

在本例中,我们使用s 作为标识文本,而不是文本本身。但是,Symbol如果我们想用 s 来做某事那就没有多大意义了:valueSymbol

def append2(value)
  value.gsub(/foo/, "bar")
end
Enter fullscreen mode Exit fullscreen mode

在这种情况下value,它将作为 起作用String,因此我们应该确保String将 传递给它。

个人观点

我个人认为,如果对冻结字符串进行优化,它更适合用来替代Symbol。无论它能带来多少性能提升,都不值得为用户带来困惑,因此应该避免。

例如,Javascript 具有与 Ruby 相同的 JSON 类语法,但将键视为String值:

const map = { a: 1, b: 2, c: 3 };
map['a'] // => 1
map.a // => 1
Enter fullscreen mode Exit fullscreen mode

当然,正如 RubyConf 上的上述讨论中所提到的,后期的点语法在 Ruby 中是一个非常糟糕的Hashie想法,但那是另一回事。

我主要的抱怨是,尽管 Ruby 赋予了Symbol它的使用价值,但在很多情况下它却喜欢假装它们不存在并强制执行某些操作以防止用户遇到错误。

无论如何,个人的抱怨已经结束,我实际上也不认为这种改变会在该语言的未来版本中发生,因为这将是一个太大的突破性变化,不值得社区为此付出迁移的痛苦。

学习如何最好地使用数组、哈希和集合

要讲的内容太多了,说实话,一章根本不够涵盖 Ruby 中那些有趣的部分Array,但这也不是本书的重点,所以我跑题了。我强烈建议至少阅读一下本章之后的官方文档Enumerable了解一下 Ruby 的所有可能性。

大批

[[:foo, 1], [:bar, 3], [:baz, 7]].each do |sym, i|
  # ...
end
Enter fullscreen mode Exit fullscreen mode

提供的示例是一组包含两项的元组来表示数据,除了块可以使用像symi这样的参数来解构值之外,这里没有太多可展示的内容。请注意,这里与 though 相比,有一个非常微妙的地方需要注意Hash:这里可以有多个 实例:foo,但在Hash需要唯一键的 中只能有一个。

哈希

Hash 示例非常相似:

{ foo: 1, bar: 3, baz: 7 }.each do |sym, i|
  # ...
end
Enter fullscreen mode Exit fullscreen mode

书中提到,Array从设计角度来看,该解决方案可能更正确,但更Hash容易实现。我倾向于同意这一点,但上面提到的情况可能会变得复杂。

想象一下,如果你有一组来自 AWS 的标签,以Array元组的形式存在,那么将其表示为 aHash可能不是一个好主意。在决定如何用 Ruby 表达它时,请记住你的底层数据。

实现内存数据库

这是我见过的书中关于这两个方法的一个比较独特的应用,我很喜欢他在这里尝试了一些更实质性的内容。他首先生成了一些模拟数据来演示:

album_infos = 100.times.flat_map do |i|
  10.times.map do |j|
    ["Album #{i}", j, "Track #{j}"]
  end
end
Enter fullscreen mode Exit fullscreen mode

需要注意的是,flat_map映射(转换)集合后会变平,但本书确实假设读者具备中级 Ruby 知识。

创建索引 - 数组元组

第一部分涉及索引数据,或者提供一种清晰的方法,可以从多个角度查找数据。如果我们要为其创建一个简单的索引函数,Array可能如下所示(Rails 也做了类似的事情):

class Array
  def index_by(&block)
    indexes = {}
    self.each { |v| indexes[block.call(v)] = v }
    indexes
  end
end
Enter fullscreen mode Exit fullscreen mode

不过,请记住关于唯一键的那一点,因为这确实会让事情变得复杂。如果它用一个人的名字来索引,但两个人的名字相同怎么办?总之,回到他们提供的问题解决方案:

album_artists = {}
album_track_artists = {}

album_infos.each do |album, track, artist|
  (album_artists[album] ||= []) << artist
  (album_track_artists[[album, track]] ||= []) << artist
end

album_artists.each_value(&:uniq!)
Enter fullscreen mode Exit fullscreen mode

当然,我可能会做类似这样的事情:

album_artists = Hash.new { |h, k| h[k] = Set.new }
album_track_artists = Hash.new { |h, k| h[k] = Set.new }

album_infos.each do |album, track, artist|
  album_artists[album].add artist
  album_track_artists[[album, track]].add artist
end
Enter fullscreen mode Exit fullscreen mode

...这避免了混淆默认分配和以后的唯一性约束,因为Set只能具有唯一值,但这也使得解决方案在第一章中更加复杂和难以解释,所以我可以理解为什么这样写。

查找功能很有趣:

lookup = -> (album, track = nil) do
  if track
    album_track_artists[[album, track]]
  else
    album_artists[album]
  end
end
Enter fullscreen mode Exit fullscreen mode

为什么?我们的第一个直觉可能是创建一个如下方法:

def lookup(album, track = nil)
  # ...
end
Enter fullscreen mode Exit fullscreen mode

……但它究竟在哪里得到“然后”呢album_artistsalbum_track_artists这个解决方案通过使用 lambda 函数来避免这个问题,lambda 函数通过所谓的闭包来捕获它们定义的本地上下文。

当然,我认为这在 Ruby 中有点不常见,也不太常用,但它避免了把所有这些都封装在一个类里,从而大大延长章节长度。不过我不确定在其他地方是否会推荐这样做。

(您还会注意到,在文章的篇幅内,我特意不亲自实现它)

创建索引 - 嵌套哈希

第二种解决方案是使用嵌套哈希:

albums = {}

album_infos.each do |album, track, artist|
  ((albums[album] ||= {})[track] ||= []) << artist
end
Enter fullscreen mode Exit fullscreen mode

...和前一种情况一样,通过将该代码提升到初始对象实例,可能值得将赋值和默认值分离:

albums = Hash.new do |h, k|
  h[k] = Hash.new { |h2, k2| h2[k2] = [] }
end
Enter fullscreen mode Exit fullscreen mode

它是不是不够简洁?当然,但它也明确了我们数据的形状,我认为这是一个很好的权衡。

正如书中所提到的,查找代码变得更加复杂:

lookup = -> (album, track = nil) do
  if track
    albums.dig(album, track)
  else
    a = albums[album].each_value.to_a
    a.flatten!
    a.uniq!
    a
  end
end
Enter fullscreen mode Exit fullscreen mode

我喜欢这本书的一点是,Jeremy 提到了每种方法的利弊。Array元组方法占用更多内存,但查找大量记录时速度更快。第二种方法在album查找方面效率低得多,但在嵌套查询方面表现出色。

创建索引 - 已知数据

不过,他在下一部分中所做的是对了解底层数据及其为我们带来什么的有趣见解。

albums = {}

album_infos.each do |album, track, artist|
   album_array = albums[album] ||= [[]]
   album_array[0] << artist
   (album_array[track] ||= []) << artist  
end

albums.each_value do |array|
  array[0].uniq!
end
Enter fullscreen mode Exit fullscreen mode

与前几节不同,本例假设第一项是艺术家,1第二项99是曲目。我们可以明确地对数据进行建模,但那样会变得相当混乱:

TRACK_COUNT = 99

albums = Hash.new { |h, k| h[k] = [Set.new, *([] * TRACK_COUNT)]}
Enter fullscreen mode Exit fullscreen mode

...我并不特别喜欢,但确实暴露出这种数据结构有点危险。

这里的一个技巧是,Ruby 的dig函数可以与 和 一起使用HashArray这意味着编号索引在这里可以工作,从而使查找函数更加简单:

lookup = -> (album, track = 0) do
  albums.dig(album, track)
end
Enter fullscreen mode Exit fullscreen mode

……但与其他两种方法不同,由于代码与数据结构紧密相关,因此在需求发生变化时可能会变得脆弱。虽然可以在这里获得一些额外的性能,但如果以后需要重新审视和重构,那么这样做可能就不值得了。

知名艺术家姓名 - 数组

下一部分想要开发一项功能,用于在专辑中查找知名艺术家的姓名,而不是用户提供的列表:

album_artists = album_infos.flat_map(&:last)
album_artists.uniq!

lookup = -> (artists) do
  album_artists & artists
end
Enter fullscreen mode Exit fullscreen mode

知名艺术家姓名 - Hash

……但提到,如果艺术家数量较多,这种方法可能会很慢。建议的解决方案是使用一个Hash键值来标记知名艺术家:

album_artists = {}

album_infos.each do |_, _, artist|
  album_artists[artist] ||= true
end

lookup = -> (artists) do
  artists.select do |artist|
    album_artists[artist]
  end
end
Enter fullscreen mode Exit fullscreen mode

尽管这可能更容易values_at

lookup = -> (artists) do
  album_artists.values_at(*artists)
end
Enter fullscreen mode Exit fullscreen mode

知名艺术家姓名 - 套装

...但这个练习的目的是引导我们Set,所以让我们开始吧:

require 'set'

album_artists = Set.new(album_infos.flat_map(&:last))

lookup = -> (artists) do
  album_artists & artists
end
Enter fullscreen mode Exit fullscreen mode

区别在于,Set前者比后者快得多Array,但不如后者快Hash。本书推荐前者,因为它有更好的 API;而后者,如果你需要性能提升,则推荐后者。

使用 Struct —— 未被充分重视的核心类之一

瞧,我真的很喜欢Struct,尤其是在 REPL 里。很高兴在这里看到它。Jeremy 先从这里一个普通类的例子开始:

class Artist
  attr_accessor :name, :albums

  def initialize(name, albums)
    @name = name
    @albums = albums
  end
end
Enter fullscreen mode Exit fullscreen mode

如果你曾经觉得其中很多都是多余的,你一定会喜欢Struct

Artist = Struct.new(:name, :albums)
Enter fullscreen mode Exit fullscreen mode

...尽管我个人喜欢使用 kwargs 来表示类,以便清楚地了解你传递给它的内容,并且Struct还涵盖了这种情况:

Artist = Struct.new(:name, :albums, keyword_init: true)
Artist.new(name: 'Brandon', albums: [])
Enter fullscreen mode Exit fullscreen mode

对我来说更清楚了。总之,书里提到了权衡,它Struct比 a 更轻量class,但查找属性需要更长的时间。

他确实提到了一个有趣的特性Struct,一个新的实例实际上是一个Class

Struct.new(:a, :b).class
# => Class
Enter fullscreen mode Exit fullscreen mode

子类化结构体

尽管对于所提到的子类来说情况并非如此:

Struct.new('A', :a, :b).new(1, 2).class
# => Struct::A
Enter fullscreen mode Exit fullscreen mode

...他还指出了该Struct.new方法的实现方式:

def Struct.new(name, *fields)
  unless name.is_a?(String)
    fields.unshift(name)
    name = nil
  end

  subclass = Class.new(self)

  if name
    const_set(name, subclass)
  end

  # Internal magic to setup fields/storage for subclass
  def subclass.new(*values)
    obj = allocate
    obj.initialize(*values)
    obj
  end

  # Similar for allocate, [], members, inspect
  # Internal magic to setup accessor instance methods
  subclass
end
Enter fullscreen mode Exit fullscreen mode

如果你恰好传递了类似的名称,'A'它会在当前命名空间中定义一个常量,并将该子类附加到它上面。这里需要一些底层细节的说明,这肯定需要一些时间,然后是最后一节,讲解如何实际创建一个新的实例。

就我个人而言,我宁愿避免这种情况,而倾向于后面提到的子类化:

class SubStruct < Struct
end
Enter fullscreen mode Exit fullscreen mode

Struct...对于大多数情况下您需要了解的内容来说,上面的代码可能有点多。

冻结结构

下一节将提到自动冻结结构:

A = Struct.new(:a, :b) do
  def initialize(...)
    super
    freeze
  end
end
Enter fullscreen mode Exit fullscreen mode

……这使得值不可变。Jeremy 还提到,Ruby 跟踪器上曾提交过几个问题,希望将其完善,但都没有被纳入 Ruby 3,而这是最可行的解决方法。

我个人喜欢使用不可变的小数据类型(如 Haskell 和 Scala 案例类)的想法,以便快速用作数据容器而不是域对象。

总结和问题

本章以总结和一些问题结束。让我们快速回顾一下这些问题。

1. nilfalse与所有其他对象有何不同?

nil实际上什么也不是,你在 Ruby 中经常看到的错误是由于进入了应用程序不期望的地方而导致的。

false是 的一个实例FalseClass,所以当我将它与 并列时,我不确定我是否理解了这个问题的意图nil。或许用以下表述会更好,更能说明这些数据类型的意图?

2. 使用两个 BigDecimal 对象的所有标准算术运算都是精确的吗?

就两种BigDecimal类型而言是的,但如果Float偏向一侧则不会那么多。

3. Ruby 将符号和字符串结合起来有意义吗?

从哲学角度来说?我想Symbol放弃它,因为它让 Ruby 新手的操作变得异常复杂,却几乎没有什么实际收益,甚至还让我时不时地犯错。我不喜欢它们,因为它们给语言带来了复杂性。

务实吗?不。应该保持原样,因为修改后的后果会破坏大量 Ruby 代码,并在社区引发一场大战。虽然我不喜欢,但这样做不值得。

4. 对于相同的数据,hash 和Set哪个占用的内存更少?

可能吧Hash,但差别不大。我记得好像Set是用 a 来实现的Hash,所以应该不会差太多。

5.返回Class新实例的仅有的两个核心方法是什么?

Struct.newClass.new会这么想。

包起来

优点

总的来说?实用主义。Jeremy 擅长权衡利弊,并解释为什么某些事情要以某种方式完成,这在他的很多作品中都有所体现。这是最好的解决方案吗?也许不是,但它能考虑到极端情况,而这正是他真正擅长的地方:深入挖掘这些细节。

本书以务实的立场探讨了不同数据结构及其用法对性能的影响。这样做的书籍并不多。

我花了一些时间来解决 Ruby 社区中的一个棘手问题SymbolString并得到了一个相当合理的回应。我本来想看看删除其中一个问题会带来什么影响,但我明白,这会让本章的篇幅迅速膨胀。

它在入门问题上采取了更大胆的立场album,这为探索有趣的代码提供了更多机会。太多示例感觉非常基础,并没有真正展现出很多潜在的问题,我认为这本书在这方面做得很好。

Safari Books Online 有一个抢先体验版,所有代码都换行,并采用衬线字体,没有高亮显示。我希望 Packt 能解决这个问题,因为这样几乎无法阅读。我真心希望实体书也能解决这个问题。

就这本书本身而言,我觉得第一章试图将大量内容放在一章中,如果将其分成更多部分可能会更好。

true我确实希望,false部分nil更多地讨论合理的默认值,而不是像现在这样深入研究 bang 方法,因为这些方法在许多 Ruby 程序中会更有用,可以防止出现错误。

一些示例倾向于混淆赋值和连接行为,并且可能通过在代码之上明确定义数据结构来获得更好的效果||=

该部分Struct从非常有用的概述转向了有点冗杂的内容,让我感到困惑。

概述

我打算继续阅读和编写其他章节的类似内容,并期待接下来的内容。

我对某些内容有异议吗?当然有,但我对上个月自己编写的代码有异议,我只是确保理解做出这些决定的原因,并尽可能记录相关因素。这些评论的乐趣在于提供额外的背景信息,并探讨某些主题被涵盖的原因。

第二章再见!

鏂囩珷鏉ユ簮锛�https://dev.to/baweaver/let-s-read-polish-ruby-programming-2mh5
PREV
掌握 Web 可访问性:前端开发人员指南
NEXT
这是使用 JS 中的 fetch 发送请求的完整指南