使用Ruby中的Hash汇总对象区域

Art*_*gas 2 ruby hash associative area sketchup

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
Run Code Online (Sandbox Code Playgroud)

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

summa { "Bricks" => 500, "Planks" => 4000 }
Run Code Online (Sandbox Code Playgroud)

顺便说一句,我正在为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'
Run Code Online (Sandbox Code Playgroud)

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

所以我需要的任何帮助都会很好.

Jör*_*tag 7

问题是这样的:

summa[face.material.display_name] += face.area
Run Code Online (Sandbox Code Playgroud)

这(大致)相当于

summa[face.material.display_name] = summa[face.material.display_name] + face.area
Run Code Online (Sandbox Code Playgroud)

但是,您从summa一个空哈希开始:

summa = Hash.new
Run Code Online (Sandbox Code Playgroud)

这意味着无论何时第一次遇到特定材料(显然,在循环的第一次迭代中已经是这种情况),summa[face.material.display_name]根本就不存在.所以,你试图在一个不存在的东西上添加一个数字,这显然是行不通的.

快速解决方法是使用默认值初始化哈希值,以便返回有用的内容而不nil是非现有键:

summa = Hash.new(0)
Run Code Online (Sandbox Code Playgroud)

但是,可以对代码进行许多其他改进.我是这样做的:

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 }
}
Run Code Online (Sandbox Code Playgroud)

我发现容易阅读,而不是"循环这个,但如果发生这种事情就跳过一次迭代,如果发生这种情况也不要这样做".

这实际上是一种常见的模式,几乎每个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
Run Code Online (Sandbox Code Playgroud)

啊,好多了.

正如我已经提到的,我们需要做的第一件事就是解决明显的问题:替换summa = {}(用BTW编写它的惯用方法)summa = Hash.new(0).现在,代码至少有效.

作为下一步,我将切换两个局部变量的赋值:首先分配entities,然后分配summa,然后你做一些事情,entities你必须看三行才能弄清楚是什么entities.如果切换两者,则使用和分配entities彼此相邻.

结果,我们看到entities已分配,然后立即使用,然后再也不再使用.我不认为这会提高可读性,所以我们可以完全摆脱它:

for face in Sketchup.active_model.entities
Run Code Online (Sandbox Code Playgroud)

接下来是for循环.这些在Ruby 中非常不恰当; Rubyists非常喜欢内部迭代器.那么,让我们切换到一个:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face
  if face.material
    summa[face.material.display_name] += face.area
  end
}
Run Code Online (Sandbox Code Playgroud)

它具有的一个优点是,现在face是循环体的局部,而在此之前,它正在泄漏到周围的范围内.(在Ruby中,只有模块体,类体,方体,块体和脚本体具有自己的范围; forwhile循环体和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
}
Run Code Online (Sandbox Code Playgroud)

现在我们彼此相邻有两个保护条款.让我们简化逻辑:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face && face.material
  summa[face.material.display_name] += face.area
}
Run Code Online (Sandbox Code Playgroud)

但现在只有一个单一的守卫条款只守护一个表达式.所以,我们可以让整个表达式本身有条件:

Sketchup.active_model.entities.each {|face|
  summa[face.material.display_name] += face.area if
    face.kind_of? Sketchup::Face && face.material
}
Run Code Online (Sandbox Code Playgroud)

然而,这仍然有点难看:我们循环一些集合,然后在循环内部我们跳过我们不想循环的所有项目.那么,如果我们不想遍历它们,我们首先会在它们上面循环它们吗?我们不是先选择"有趣"的项目然后再循环它们吗?

Sketchup.active_model.entities.select {|e|
  e.kind_of? Sketchup::Face && e.material
}.each {|face|
  summa[face.material.display_name] += face.area
}
Run Code Online (Sandbox Code Playgroud)

我们可以对此做一些简化.如果我们意识到它o.kind_of? C是相同的C === o,那么我们可以使用用于模式匹配的grep过滤器===,而不是select:

Sketchup.active_model.entities.grep(Sketchup::Face).select {|e| e.material
}.each { … }
Run Code Online (Sandbox Code Playgroud)

我们的select过滤器可以进一步简化使用Symbol#to_proc:

Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).each { … }
Run Code Online (Sandbox Code Playgroud)

现在让我们回到循环中.任何有高级语言经验的人,如Ruby,JavaScript,Python,C++ STL,C#,Visual Basic.NET,Smalltalk,Lisp,Scheme,Clojure,Haskell,Erlang,F#,Scala ......基本上任何现代语言在所有的,会立即认识到这种模式的catamorphism, ,reduce,,fold 或任何您所选择的语言中一样调用它.inject:into:inject

这样reduce做,基本上它将几件事"简化"为一件事.最明显的例子是数字列表的总和:它将几个数字简化为一个数字:

[4, 8, 15, 16, 23, 42].reduce(0) {|accumulator, number| accumulator += number }
Run Code Online (Sandbox Code Playgroud)

[注意:在惯用的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
Run Code Online (Sandbox Code Playgroud)

在这种情况下,accumulatorsumma哈希.

Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).
reduce(Hash.new(0)) {|h, face|
  h[face.material.display_name] += face.area
  h
}
Run Code Online (Sandbox Code Playgroud)

最后但并非最不重要的是,我不喜欢h在块结束时显式返回.我们显然可以在同一行上写它:

h[face.material.display_name] += face.area; h
Run Code Online (Sandbox Code Playgroud)

但我更喜欢使用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 }
}
Run Code Online (Sandbox Code Playgroud)

而且,就是这样!