2
require 'sketchup'

entities = Sketchup.active_model.entities
summa = Hash.new

for face in entities
  next unless face.kind_of? Sketchup::Face
  if (face.material)
    summa[face.material.display_name] += face.area
  end
end

我正在尝试这样获取数组中的结构:

summa { "Bricks" => 500, "Planks" => 4000 }

顺便说一句,我正在为 Google Sketchup 制作一个 ruby​​ 脚本

但是如果我运行这段代码,我只会得到

Error: #<NoMethodError: undefined method `+' for nil:NilClass>
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:17
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:14:in `each'
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:14
C:\Program Files (x86)\Google\Google SketchUp 7\Plugins\test.rb:8:in `call'

因为我习惯使用 PHP 并且只是在做$array['myownassoc'] += bignumber; 但是我想在使用 Ruby 时这不是正确的方法?

所以任何关于我需要如何去的帮助都会很好。

4

3 回答 3

7

问题是这样的:

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//表达式没有。)whileifunlesscase

让我们进入循环的主体。

第一行是一个保护子句。很好,我喜欢保护条款 :-)

第二行是,好吧,如果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、、 、reducefold任何您选择的语言碰巧称呼它。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

在这种情况下,accumulatorsumma哈希。

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 }
}

而且,就是这样!

于 2010-05-24T13:33:45.627 回答
2

summa[face.material.display_name]当 face.material.display_name 不是现有键时,默认返回 nil。创建哈希时,您可以指定要返回的不同默认值。就像是:

summa = Hash.new(0)
于 2010-05-24T10:16:33.687 回答
0

只需注意您的面部区域摘要 - 您还必须考虑到组/组件可能会被缩放,因此您需要利用包含您检查的面部的组/组件的整个层次结构的转换。请记住,组/组件也可以倾斜 - 因此也必须考虑到这一点。

于 2010-12-26T14:38:57.777 回答