2010-05-24 258 views
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 } 

順便說一句,我正在做一個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的正確方法?

所以我需要怎麼去任何幫助將是很好。

回答

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

2

summa[face.material.display_name]返回nil時,默認情況face.material.display_name不是現有的密鑰。創建散列時,您可以指定一個不同的默認值來返回。喜歡的東西:

summa = Hash.new(0) 
+0

Thx很多傢伙,這真的幫助了我很多工作;) – 2010-05-24 11:20:40

0

你的臉區域的總結只是注意 - 你還必須考慮到組/部件可能被縮放,所以你需要使用包含您檢查面組/部件的整個層次的變革。請記住,組/組件也可能會發生傾斜 - 因此也必須考慮到這一點。