与红宝石

问题描述:

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 } 

顺便说一句,我正在做一个Ruby脚本谷歌Sketchup的

但是,如果我运行此代码,我只得到

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

所以我需要怎么去任何帮助将是很好。

问题是这样的:

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.new(0)替换summa = {}(这将是写作它的惯用方式)。现在,代码至少工程

作为下一个步骤,我会切换两个局部变量的赋值:第一分配entities,然后分配summa,那么你做的东西跟entities,你要看看三行高达弄清楚什么entities是。如果您切换这两个,entities的使用和分配紧挨着。

因此,我们看到entities被分配,然后立即使用,然后再也没有使用过。我不认为这多少增加了可读性,所以我们可以得到完全摆脱它:

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? 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语言,计划,Clojure中,哈斯克尔,二郎,F#,Scala中,&hellip一些经验;基本上所有任何现代语言,将立即认识到这种模式的catamorphism,reducefoldinject:into:inject或任何您所选择的语言中一样调用它。

一个reduce做什么,基本上是“减少”几件事情到了一两件事。最明显的例子是数字列表的总和:它减少了几个号码到一个号码:

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

[注:在惯用的红宝石,这将被写入就像[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-组合子)来代替:

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

而且,就是这样!

+0

Omg,这是完全值得的阅读,并thx所有提示和更正我的代码。如果它没有渗透,我是Ruby的初学者,也是Sketchup的插件编程人员。 – 2010-05-25 19:06:54

+1

优秀! +1是不够的! :P我希望我有耐心去做你刚刚做的事情。 – 2010-05-25 19:45:49

+0

如果我要更改代码来计算双方的材料,那么最好的方法是怎么做的呢? back_material和材料是。 – 2010-05-26 06:04:33

summa[face.material.display_name]返回nil时,默认情况face.material.display_name不是现有的密钥。创建散列时,您可以指定一个不同的默认值来返回。喜欢的东西:

summa = Hash.new(0) 
+0

Thx很多家伙,这真的帮助了我很多工作;) – 2010-05-24 11:20:40

你的脸区域的总结只是注意 - 你还必须考虑到组/部件可能被缩放,所以你需要使用包含您检查面组/部件的整个层次的变革。请记住,组/组件也可能会发生倾斜 - 因此也必须考虑到这一点。