在 Ruby 中用 Hash 总结对象区域

发布于 2024-09-02 21:34:18 字数 956 浏览 3 评论 0原文

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'

As I'm used to using PHP and just do $array['myownassoc'] += bignumber; 但我想这在使用 Ruby 时不是正确的方法吗?

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

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

I'm trying to get the structure in the array as such:

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

By the way, I'm making a ruby script for Google Sketchup

But if I run this code I only get

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'

As I'm used to using PHP and just doing $array['myownassoc'] += bignumber;
But I guess this isn't the right approach when using Ruby?

So any help in how I need to go would be nice.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

旧伤还要旧人安 2024-09-09 21:34:18

问题是这样的:

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 = Hash 替换 summa = {} (顺便说一句,这是惯用的编写方式) .new(0)。现在,代码至少可以工作了。

下一步,我将切换两个局部变量的分配:首先分配 entities,然后分配 summa,然后对 entities< 执行某些操作/code> 并且您必须查看三行才能找出实体是什么。如果切换两者,实体的使用和分配将彼此相邻。

结果,我们看到实体被分配,然后立即使用,然后再也没有使用过。我认为这并不能提高可读性,所以我们可以完全摆脱它:

for face in Sketchup.active_model.entities

接下来是 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
}

它的一个优点是,现在 face 是循环体的本地变量,而之前,它会泄漏到周围的作用域中。 (在 Ruby 中,只有模块体、类体、方法体、块体和脚本体有自己的作用域;forwhile 循环体以及 if/unless/case 表达式没有。)

让我们进入循环体。

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

第二行是,如果 face.material 是 true-ish,它会执行某些操作,否则它不会执行任何操作,这意味着循环结束。所以,这是另一个保护条款!然而,它的编写风格与第一个保护子句完全不同,就在它上面一行!同样,一致性很重要:

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 过滤器,该过滤器使用 === 进行模式化match,而不是 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

在本例中,累加器 > 是 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 }
}

而且,就是这样!

The problem is this:

summa[face.material.display_name] += face.area

This is (roughly) equivalent to

summa[face.material.display_name] = summa[face.material.display_name] + face.area

However, you start out with summa as an empty hash:

summa = Hash.new

Which means that whenever you encounter a specific material for the first time (and obviously, this is going to be already the case in the very first iteration of the loop), summa[face.material.display_name] simply doesn't exist. So, you are trying to add a number to something that doesn't exist, which obviously cannot work.

The quick fix would be to just initialize the hash with a default value, so that it returns something useful instead of nil for a non-existing key:

summa = Hash.new(0)

There are, however, a lot of other improvements that could be made to the code. Here's how I would do it:

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

I find that much easier to read, instead of "loop over this, but skip one iteration if that thing happens, and also don't do this if that happens".

This is actually a common pattern, that pretty much every Rubyist has already written a dozen times, so I actually had a code snippet lying around that I only needed to slightly adapt. However, I am going to show you how I could have refactored your original code step-by-step if I hadn't already had the solution.

First, let's start with coding style. I know it's boring, but it is important. What the actual coding style is, is not important, the important thing is that the code is consistent, which means that one piece of code should look the same as any other piece of code. In this particular instance, you are asking the Ruby community to provide you with unpaid support, so it is polite to at least format the code in a style that members of that community are used to. This means standard Ruby coding style: 2 spaces for indentation, snake_case for method and variable names, CamelCase for constants which refer to modules or classes, ALL_CAPS for constants, and so on. Don't use parentheses unless they clear up the precedence.

In your code, for example, you use sometimes 3 spaces, sometimes 4 spaces, sometimes 5 spaces and sometimes 6 spaces for indentation, and all of that in just 9 non-empty lines of code! Your coding style is not only inconsistent with the rest of the community, it isn't even consistent with its own next line!

Let's fix that first:

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

Ah, much better.

As I already mentioned, the first thing we need to do, is fix the obvious problem: replace summa = {} (which BTW would be the idiomatic way to write it) with summa = Hash.new(0). Now, the code at least works.

As a next step, I would switch the assignment of the two local variables: first you assign entities, then you assign summa, then you do something with entities and you have to look three lines up to figure out what entities was. If you switch the two, the usage and the assignment of entities are right next to each other.

As a result, we see that entities is assigned, then immediately used and then never used again. I don't think this improves readability much, so we can get rid of it altogether:

for face in Sketchup.active_model.entities

Next comes the for loop. Those are highly un-idiomatic in Ruby; Rubyists strongly prefer internal iterators. So, let's switch to one:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face
  if face.material
    summa[face.material.display_name] += face.area
  end
}

One advantage this has, is that now face is local to the body of the loop, whereas before, it was leaking out into the surrounding scope. (In Ruby, only module bodies, class bodies, method bodies, block bodies and script bodies have their own scope; for and while loop bodies as well as if/unless/case expressions don't.)

Let's get on to the body of the loop.

The first line is a guard clause. That's good, I like guard clauses :-)

The second line is, well, if face.material is true-ish, it does something otherwise it does nothing, which means the loop is over. So, it's another guard clause! However, it is written in a totally different style than the first guard clause, directly one line above it! Again, consistency is important:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face
  next unless face.material
  summa[face.material.display_name] += face.area
}

Now we have two guard clauses right next to each other. Let's simplify the logic:

Sketchup.active_model.entities.each {|face|
  next unless face.kind_of? Sketchup::Face && face.material
  summa[face.material.display_name] += face.area
}

But now there is only one single guard clause guarding only one single expression. So, we can just make the whole expression itself conditional:

Sketchup.active_model.entities.each {|face|
  summa[face.material.display_name] += face.area if
    face.kind_of? Sketchup::Face && face.material
}

However, that's still kind of ugly: we are looping over some collection, and then inside the loop we skip over all the items we don't want to loop over. So, if we don't want to loop over them, we do we loop over them in the first place? We don't we just select the "interesting" items first and then loop over just them?

Sketchup.active_model.entities.select {|e|
  e.kind_of? Sketchup::Face && e.material
}.each {|face|
  summa[face.material.display_name] += face.area
}

We can do some simplification on this. If we realize that o.kind_of? C is the same as C === o, then we can use the grep filter which uses === to pattern match, instead of select:

Sketchup.active_model.entities.grep(Sketchup::Face).select {|e| e.material
}.each { … }

Our select filter can further be simplified by using Symbol#to_proc:

Sketchup.active_model.entities.grep(Sketchup::Face).select(&:material).each { … }

Now let's get back to the loop. Anybody who has some experience in a higher-order language such as Ruby, JavaScript, Python, C++ STL, C#, Visual Basic.NET, Smalltalk, Lisp, Scheme, Clojure, Haskell, Erlang, F#, Scala, … basically any modern language at all, will immediately recognize this pattern as a catamorphism, reduce, fold, inject:into:, inject or whatever your language of choice happens to call it.

What a reduce does, is basically it "reduces" several things into just one thing. The most obvious example is the sum of a list of numbers: it reduces several numbers into just one number:

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

[Note: in idiomatic Ruby, this would be written just as [4, 8, 15, 16, 23, 42].reduce(:+).]

One way to spot a reduce lurking behind a loop is to look for the following pattern:

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

In this case, the accumulator is the summa hash.

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

Last but not least, I don't like this explicit returning of h at the end of block. We could obviously write it on the same line:

h[face.material.display_name] += face.area; h

But I prefer the use of Object#tap (aka the K-combinator) instead:

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

And, that's it!

背叛残局 2024-09-09 21:34:18

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

summa = Hash.new(0)

summa[face.material.display_name] returns nil by default when face.material.display_name isn't an existing key. When creating a hash, you can specify a different default value to return. Something like:

summa = Hash.new(0)
水水月牙 2024-09-09 21:34:18

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

Just a note on your summary of face areas - you must also take into account of group/components might be scaled so you need make use of the transformations of the whole hierarchy of the groups/components containing the face you inspect. Remember that groups/components can also be skewed - so that has to be taken into account as well.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文