【问题标题】:Summarize object area with a Hash in Ruby用 Ruby 中的 Hash 总结对象区域
【发布时间】:2011-02-23 04:17:46
【问题描述】:
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​​ 脚本

但如果我运行这段代码,我只会得到 ​​p>

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 时的正确方法?

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

【问题讨论】:

    标签: ruby hash area associative sketchup


    【解决方案1】:

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

    summa = Hash.new(0)
    

    【讨论】:

      【解决方案2】:

      问题是这样的:

      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 都已经写了十几次了,所以我实际上有一个代码 sn-p,我只需要稍微适应一下。但是,如果我还没有解决方案,我将向您展示如何逐步重构您的原始代码。

      首先,让我们从编码风格开始。我知道这很无聊,但它很重要。 什么实际的编码风格是什么,并不重要,重要的是代码是一致的,这意味着一段代码应该与其他任何一段代码看起来相同的代码。在这个特定的例子中,您要求 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 = Hash.new(0) 替换summa = {}(顺便说一句,这是惯用的写法)。现在,代码至少有效

      下一步,我将切换两个局部变量的分配:首先分配entities,然后分配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 中,只有模块体、类体、方法体、块体和脚本体有自己的范围;forwhile 循环体以及 if/unless/case 表达式没有t.)

      让我们进入循环的主体。

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

      第二行是,好吧,如果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? CC === 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 等高阶语言方面有一定经验的人……基本上任何现代语言会立即将此模式识别为变质,reducefoldinject:into:inject 或任何您选择的语言碰巧称呼它。

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

      而且,就是这样!

      【讨论】:

      • 天哪,这完全值得一读,感谢所有对我的代码的提示和更正。如果它没有渗透,我是 Ruby 的初学者,也是 Sketchup 编程插件的初学者。
      • 太棒了! +1 还不够! :P 我希望我有耐心做你刚才所做的事情。
      • 如果我要更改代码以计算两边的材料,最好的方法是什么? back_material 和材质就是。
      【解决方案3】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-10-31
        • 1970-01-01
        • 1970-01-01
        • 2012-03-09
        • 1970-01-01
        • 2011-05-09
        • 2022-11-17
        • 1970-01-01
        相关资源
        最近更新 更多