Суммируйте область объекта с помощью хеша в 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
Я пытаюсь получить структуру в массиве как таковую:
summa { "Bricks" => 500, "Planks" => 4000 }
Кстати, я делаю сценарий ruby для Google 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?
Так что любая помощь в том, как мне нужно идти, будет хорошей.
3 ответа
Проблема заключается в следующем:
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 = {}
(Который, кстати, был бы идиоматическим способом написать это) с summa = Hash.new(0)
, Теперь код как минимум работает.
В качестве следующего шага я бы переключил назначение двух локальных переменных: сначала вы назначаете entities
затем вы назначаете summa
тогда вы делаете что-то с entities
и вы должны посмотреть на три строки, чтобы выяснить, что entities
было. Если вы переключите два, использование и назначение entities
прямо рядом друг с другом.
В результате мы видим, что entities
назначается, затем сразу используется, а затем никогда не используется снова. Я не думаю, что это значительно улучшает читабельность, поэтому мы можем полностью избавиться от него:
for face in Sketchup.active_model.entities
Далее идет for
петля. Это совершенно не идиоматично в 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 только собственные тела модулей, тела классов, тела методов, тела блоков и тела сценариев имеют собственную область видимости; for
а также while
тела петли, а также 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, Scheme, Clojure, Haskell, Erlang, F#, Scala, … практически любой современный язык вообще, сразу распознает эту закономерность как катаморфизм, reduce
, fold
, inject: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
В этом случае accumulator
это 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-комбинатор) вместо этого:
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 }
}
И это все!
summa[face.material.display_name]
по умолчанию возвращает nil, когда face.material.display_name не является существующим ключом. При создании хэша вы можете указать другое значение по умолчанию для возврата. Что-то вроде:
summa = Hash.new(0)
Просто обратите внимание на ваше резюме областей лица - вы также должны принять во внимание, что группы / компоненты могут быть масштабированы, поэтому вам необходимо использовать преобразования всей иерархии групп / компонентов, содержащих лицо, которое вы проверяете. Помните, что группы / компоненты также могут быть перекошены - так что это также необходимо учитывать.