问题是这样的:
summa[face.material.display_name] += face.area
这(大致)相当于
summa[face.material.display_name] = summa[face.material.display_name] + face.area
但是,您从summa
一个空哈希开始:
summa = Hash.new
这意味着,每当您第一次遇到特定材料时(显然,在循环的第一次迭代中已经是这种情况),summa[face.material.display_name]
根本不存在。因此,您正在尝试向不存在的东西添加一个数字,这显然无法正常工作。
快速解决方法是使用默认值初始化散列,以便它返回有用的东西而不是nil
不存在的键:
summa = Hash.new(0)
但是,可以对代码进行许多其他改进。这是我的做法:
require 'sketchup'
Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).
reduce(Hash.new(0)) {|h, face|
h.tap {|h| h[face.material.display_name] += face.area }
}
我发现这更容易阅读,而不是“循环这个,但如果发生这种情况则跳过一次迭代,如果发生这种情况也不要这样做”。
这实际上是一种常见的模式,几乎每个 Rubyist 都已经写过十几次了,所以我实际上有一个代码片段,我只需要稍微适应一下。但是,如果我还没有解决方案,我将向您展示如何逐步重构您的原始代码。
首先,让我们从编码风格开始。我知道这很无聊,但这很重要。实际的编码风格是什么,并不重要,重要的是代码是一致的,这意味着一段代码应该与其他任何一段代码看起来相同。在这个特定的例子中,您要求 Ruby 社区为您提供无偿支持,因此至少以该社区成员习惯的样式格式化代码是有礼貌的。这意味着标准的 Ruby 编码风格:2 个空格用于缩进,snake_case 用于方法和变量名称,CamelCase 用于引用模块或类的常量,ALL_CAPS 用于常量,等等。除非它们清除优先级,否则不要使用括号。
例如,在您的代码中,您有时使用 3 个空格、有时 4 个空格、有时 5 个空格、有时 6 个空格来进行缩进,而所有这些都只需要 9 行非空代码!您的编码风格不仅与社区的其他人不一致,甚至与自己的下一行不一致!
让我们先解决这个问题:
require 'sketchup'
entities = Sketchup.active_model.entities
summa = {}
for face in entities
next unless face.kind_of? Sketchup::Face
if face.material
summa[face.material.display_name] += face.area
end
end
啊,好多了。
正如我已经提到的,我们需要做的第一件事是解决明显的问题:summa = {}
用summa = Hash.new(0)
. 现在,代码至少可以工作了。
作为下一步,我将切换两个局部变量的赋值:首先你 assign entities
,然后你 assign summa
,然后你做一些事情,entities
你必须查看三行以确定是什么entities
。如果您切换两者,则用法和分配entities
彼此相邻。
结果,我们看到它entities
被分配,然后立即使用,然后再也没有使用过。我不认为这会大大提高可读性,所以我们可以完全摆脱它:
for face in Sketchup.active_model.entities
接下来是for
循环。这些在 Ruby 中是非常不习惯的。Ruby 主义者非常喜欢内部迭代器。所以,让我们切换到一个:
Sketchup.active_model.entities.each {|face|
next unless face.kind_of? Sketchup::Face
if face.material
summa[face.material.display_name] += face.area
end
}
这样做的一个优点是,现在face
是循环体的本地,而之前,它泄漏到周围的范围内。(在 Ruby 中,只有模块体、类体、方法体、块体和脚本体有自己的作用域;循环体和for
//表达式没有。)while
if
unless
case
让我们进入循环的主体。
第一行是一个保护子句。很好,我喜欢保护条款 :-)
第二行是,好吧,如果face.material
是真的,它会做一些事情,否则它什么也不做,这意味着循环结束。所以,这是另一个保护条款!但是,它的写法与第一个保护子句完全不同,直接在它上面一行!同样,一致性很重要:
Sketchup.active_model.entities.each {|face|
next unless face.kind_of? Sketchup::Face
next unless face.material
summa[face.material.display_name] += face.area
}
现在我们有两个紧挨着的保护子句。让我们简化逻辑:
Sketchup.active_model.entities.each {|face|
next unless face.kind_of? Sketchup::Face && face.material
summa[face.material.display_name] += face.area
}
但是现在只有一个保护子句只保护一个表达式。所以,我们可以让整个表达式本身有条件:
Sketchup.active_model.entities.each {|face|
summa[face.material.display_name] += face.area if
face.kind_of? Sketchup::Face && face.material
}
然而,这仍然有点难看:我们正在循环一些集合,然后在循环中我们跳过所有我们不想循环的项目。所以,如果我们不想循环它们,我们是否首先循环它们?我们不只是先选择“有趣”的项目,然后循环遍历它们吗?
Sketchup.active_model.entities.select {|e|
e.kind_of? Sketchup::Face && e.material
}.each {|face|
summa[face.material.display_name] += face.area
}
我们可以对此做一些简化。如果我们意识到这o.kind_of? C
与 相同C === o
,那么我们可以使用用于模式匹配的grep
过滤器,而不是:===
select
Sketchup.active_model.entities.grep(Sketchup::Face).select {|e| e.material
}.each { … }
我们的select
过滤器可以通过使用进一步简化Symbol#to_proc
:
Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).each { … }
现在让我们回到循环。任何在 Ruby、JavaScript、Python、C++ STL、C#、Visual Basic.NET、Smalltalk、Lisp、Scheme、Clojure、Haskell、Erlang、F#、Scala 等高阶语言方面有一定经验的人……基本上任何现代语言总之,会立即将此模式识别为 catamorphism、、 、reduce
或fold
任何您选择的语言碰巧称呼它。inject:into:
inject
a 的reduce
作用基本上是将几件事“减少”为一件事。最明显的例子是数字列表的总和:它将几个数字简化为一个数字:
[4, 8, 15, 16, 23, 42].reduce(0) {|accumulator, number| accumulator += number }
[注意:在惯用的 Ruby 中,这将被写成[4, 8, 15, 16, 23, 42].reduce(:+)
.]
发现reduce
潜伏在循环后面的一种方法是寻找以下模式:
accumulator = something # create an accumulator before the loop
collection.each {|element|
# do something with the accumulator
}
# now, accumulator contains the result of what we were looking for
在这种情况下,accumulator
是summa
哈希。
Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).
reduce(Hash.new(0)) {|h, face|
h[face.material.display_name] += face.area
h
}
最后但并非最不重要的一点是,我不喜欢h
在块末尾显式返回。我们显然可以把它写在同一行:
h[face.material.display_name] += face.area; h
但我更喜欢使用Object#tap
(又名 K-combinator):
Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).
reduce(Hash.new(0)) {|h, face|
h.tap {|h| h[face.material.display_name] += face.area }
}
而且,就是这样!