как реализовать enums в ruby?

<p>какой лучший способ реализовать enum идиому в ruby? я ищу что-то, что я могу использовать (почти), как перечисления java/c #.</p>

<p>два пути. символы (нотация <code>:foo</code>) или константы (нотация <code>foo</code>).</p><p>символы подходят, когда вы хотите улучшить удобочитаемость, не засоряя код буквальными строками.</p><pre><code>postal\_code[:minnesota] = "mn" postal\_code[:new\_york] = "ny" </code></pre><p>константы подходят, когда у вас есть базовое значение, которое важно. просто объявите модуль для хранения ваших констант, а затем объявите константы внутри этого.</p><pre><code>module foo bar = 1 baz = 2 biz = 4 end flags = foo::bar | foo::baz # flags = 3 </code></pre>

<p>самый идиоматический способ сделать это - использовать символы. например, вместо:</p><pre><code>enum { foo, bar, baz } myfunc(foo); </code></pre><p>... вы можете просто использовать символы:</p><pre><code># you don't actually need to declare these, of course--this is # just to show you what symbols look like. :foo :bar :baz my\_func(:foo) </code></pre><p>это немного более открытый, чем перечисления, но он хорошо сочетается с духом ruby.</p><p>символы также работают очень хорошо. например, сравнение двух символов на равенство намного быстрее, чем сравнение двух строк.</p>

<p>я удивлен, что никто не предложил что-то вроде следующего (получено из <a href="https://github.com/cstrahan/rapi/blob/master/lib/rapi.rb">rapi</a> gem):</p><pre><code>class enum private def self.enum\_attr(name, num) name = name.to\_s define\_method(name + '?') do @attrs & num != 0 end define\_method(name + '=') do |set| if set @attrs |= num else @attrs &= ~num end end end public def initialize(attrs = 0) @attrs = attrs end def to\_i @attrs end end </code></pre><p>который можно использовать так:</p><pre><code>class fileattributes < enum enum\_attr :readonly, 0x0001 enum\_attr :hidden, 0x0002 enum\_attr :system, 0x0004 enum\_attr :directory, 0x0010 enum\_attr :archive, 0x0020 enum\_attr :in\_rom, 0x0040 enum\_attr :normal, 0x0080 enum\_attr :temporary, 0x0100 enum\_attr :sparse, 0x0200 enum\_attr :reparse\_point, 0x0400 enum\_attr :compressed, 0x0800 enum\_attr :rom\_module, 0x2000 end </code></pre><p>пример:</p><pre><code>>> example = fileattributes.new(3) => #<fileattributes:0x629d90 @attrs=3> >> example.readonly? => true >> example.hidden? => true >> example.system? => false >> example.system = true => true >> example.system? => true >> example.to\_i => 7 </code></pre><p>это хорошо работает в сценариях базы данных или при работе с константами/перечислениями стиля c (как в случае использования <a href="https://github.com/ffi/ffi/">ffi</a> , который rapi широко использует).</p><p>кроме того, вам не нужно беспокоиться о том, что опечатки могут вызывать сбои без вывода сообщений, как если бы вы использовали решение с хеш-типами.</p>

<p>я использую следующий подход:</p><pre><code>class myclass my\_enum = [my\_value\_1 = 'value1', my\_value\_2 = 'value2'] end </code></pre><p>мне нравятся следующие преимущества:</p><ol> <li>он группирует значения визуально как одно целое</li> <li>это делает некоторую проверку времени компиляции (в отличие от просто использования символов)</li> <li>я могу легко получить доступ к списку всех возможных значений: просто <code>my\_enum</code></li> <li>я могу легко получить доступ к различным значениям: <code>my\_value\_1</code></li> <li>может иметь значения любого типа, не только symbol</li> </ol><p>символы могут быть лучше, потому что вам не нужно писать имя внешнего класса, если вы используете его в другом классе (<code>myclass::my\_value\_1</code>)</p>

<p>если вы используете rails 4.2 или выше, вы можете использовать перечисления rails.</p><p>rails теперь имеет по умолчанию перечисления без необходимости включать какие-либо драгоценные камни.</p><p>это очень похоже (и больше с функциями) на перечисления java, c++.</p><p>цитируется с <a href="http://edgeapi.rubyonrails.org/classes/activerecord/enum.html">http://edgeapi.rubyonrails.org/classes/activerecord/enum.html</a> :</p><pre><code>class conversation < activerecord::base enum status: [ :active, :archived ] end # conversation.update! status: 0 conversation.active! conversation.active? # => true conversation.status # => "active" # conversation.update! status: 1 conversation.archived! conversation.archived? # => true conversation.status # => "archived" # conversation.update! status: 1 conversation.status = "archived" # conversation.update! status: nil conversation.status = nil conversation.status.nil? # => true conversation.status # => nil </code></pre>

<p>это мой подход к перечислениям в ruby. я собирался кратко и сладко, не обязательно самый c-like. какие-нибудь мысли?</p><pre><code>module kernel def enum(values) module.new do |mod| values.each\_with\_index{ |v,i| mod.const\_set(v.to\_s.capitalize, 2**i) } def mod.inspect "#{self.name} {#{self.constants.join(', ')}}" end end end end states = enum %w(draft published trashed) => states {draft, published, trashed} states::draft => 1 states::published => 2 states::trashed => 4 states::draft | states::trashed => 3 </code></pre>

<p>я знаю, что прошло уже много времени с тех пор, как парень отправил этот вопрос, но у меня был тот же вопрос, и этот пост не дал мне ответа. мне нужен был простой способ увидеть, что представляет число, простое сравнение и, прежде всего, поддержка activerecord для поиска с использованием столбца, представляющего перечисление.</p><p>я ничего не нашел, поэтому сделал потрясающую реализацию под названием <a href="https://github.com/toplex/enum">yinum</a> , которая позволила все, что я искал. сделал тонну спецификаций, так что я уверен, что это безопасно.</p><p>некоторые примеры функций:</p><pre><code>colors = enum.new(:colors, :red => 1, :green => 2, :blue => 3) => colors(:red => 1, :green => 2, :blue => 3) colors.red == 1 && colors.red == :red => true class car < activerecord::base attr\_enum :color, :colors, :red => 1, :black => 2 end car = car.new car.color = :red / "red" / 1 / "1" car.color => car::colors.red car.color.black? => false car.red.to\_sql => "select `cars`.* from `cars` where `cars`.`color` = 1" car.last.red? => true </code></pre>

<p>проверьте драгоценный камень ruby-enum, <a href="https://github.com/dblock/ruby-enum">https://github.com/dblock/ruby-enum</a> .</p><pre><code>class gender include enum gender.define :male, "male" gender.define :female, "female" end gender.all gender::male </code></pre>

<p>если вы беспокоитесь об опечатках с символами, убедитесь, что ваш код вызывает исключение при доступе к значению с несуществующим ключом. вы можете сделать это, используя <code>fetch</code> вместо <code>[]</code>:</p><pre><code>my\_value = my\_hash.fetch(:key) </code></pre><p>или заставляя хеш вызывать исключение по умолчанию, если вы предоставляете несуществующий ключ:</p><pre><code>my\_hash = hash.new do |hash, key| raise "you tried to access using #{key.inspect} when the only keys we have are #{hash.keys.inspect}" end </code></pre><p>если хеш уже существует, вы можете добавить поведение, вызывающее исключения:</p><pre><code>my\_hash = hash[[[1,2]]] my\_hash.default\_proc = proc do |hash, key| raise "you tried to access using #{key.inspect} when the only keys we have are #{hash.keys.inspect}" end </code></pre><p>как правило, вам не нужно беспокоиться о безопасности опечаток с константами. если вы неправильно напишите имя константы, обычно возникает исключение.</p>

<p>кто-то пошел дальше и написал рубиновый камень под названием <a href="http://renum.rubyforge.org/">renum</a> . он утверждает, что получил наиболее близкое поведение java/c #. лично я все еще изучаю ruby, и я был немного шокирован, когда мне хотелось, чтобы конкретный класс содержал статическое перечисление, возможно, хеш, который не совсем легко найти через google.</p>

<p>возможно, лучший легкий подход будет</p><pre><code>module myconstants abc = class.new def = class.new ghi = class.new end </code></pre><p>таким образом, значения имеют связанные имена, как в java/c #:</p><pre><code>myconstants::abc => myconstants::abc </code></pre><p>чтобы получить все значения, вы можете сделать</p><pre><code>myconstants.constants => [:abc, :def, :ghi] </code></pre><p>если вы хотите порядковый номер перечисления, вы можете сделать</p><pre><code>myconstants.constants.index :ghi => 2 </code></pre>

<p>недавно мы выпустили <a href="https://github.com/mezuka/enum">gem</a> , который реализует <strong>enums в ruby</strong>. в моем <a href="http://railsguides.net/safe-enums-in-ruby/">посте</a> вы найдете ответы на свои вопросы. также я описал, почему наша реализация лучше существующих (на самом деле в ruby есть еще много реализаций этой функции, хотя и в виде гемов). </p>

<p>это кажется немного излишним, но эту методологию я использовал несколько раз, особенно там, где я интегрируюсь с xml или чем-то подобным.</p><pre><code>#model class profession def self.pro\_enum {:baker => 0, :manager => 1, :fireman => 2, :dev => 3, :val => ["baker", "manager", "fireman", "dev"] } end end profession.pro\_enum[:dev] #=>3 profession.pro\_enum[:val][1] #=>manager </code></pre><p>это дает мне строгость перечисления c #, и это связано с моделью.</p>

<p>символы это рубиновый путь. тем не менее, иногда нужно поговорить с каким-нибудь кодом на c или чем-то или с java, которые предоставляют различные перечисления для разных вещей.</p><hr><pre><code>#server\_roles.rb module enumlike def enumlike.server\_role server\_symb=[ :server\_cloud, :server\_desktop, :server\_workstation] server\_enum=hash.new i=0 server\_symb.each{ |e| server\_enum[e]=i; i +=1} return server\_symb,server\_enum end end </code></pre><hr><p>это может быть использовано следующим образом</p><hr><pre><code>require 'server\_roles' ssymb, senum =enumlike.server\_role() foreignvec[senum[:server\_workstation]]=8 </code></pre><hr><p>это, конечно, можно сделать абстрактным, и вы можете использовать наш собственный класс enum. </p>

<p>я реализовал перечисления, как это </p><pre><code>module enumtype def self.find\_by\_id id if id.instance\_of? string id = id.to\_i end values.each do |type| if id == type.id return type end end nil end def self.values [@enum\_1, @enum\_2] end class enum attr\_reader :id, :label def initialize id, label @id = id @label = label end end @enum\_1 = enum.new(1, "first") @enum\_2 = enum.new(2, "second") end </code></pre><p>тогда его легко делать операции </p><pre><code>enumtype.enum\_1.label </code></pre><p>...</p><pre><code>enum = enumtype.find\_by\_id 1 </code></pre><p>...</p><pre><code>valuearray = enumtype.values </code></pre>

<p>все зависит от того, как вы используете java или c # перечисления. то, как вы его используете, будет определять решение, которое вы выберете в ruby.</p><p>попробуйте родной тип <code>set</code>, например:</p><pre><code>>> enum = set['a', 'b', 'c'] => #<set: {"a", "b", "c"}> >> enum.member? "b" => true >> enum.member? "d" => false >> enum.add? "b" => nil >> enum.add? "d" => #<set: {"a", "b", "c", "d"}> </code></pre>

<p>другое решение использует openstruct. это довольно прямо и чисто.</p><p> <a href="https://ruby-doc.org/stdlib-2.3.1/libdoc/ostruct/rdoc/openstruct.html">https://ruby-doc.org/stdlib-2.3.1/libdoc/ostruct/rdoc/openstruct.html</a> </p><p>пример:</p><pre><code># bar.rb require 'ostruct' # not needed when using rails # by patching array you have a simple way of creating a enum-style class array def to\_enum(base=0) openstruct.new(map.with\_index(base).to\_h) end end class bar my\_enum = openstruct.new(one: 1, two: 2, three: 3) my\_enum2 = %w[one two three].to\_enum def use\_enum (value) case value when my\_enum.one puts "hello, this is enum 1" when my\_enum.two puts "hello, this is enum 2" when my\_enum.three puts "hello, this is enum 3" else puts "#{value} not found in enum" end end end # usage foo = bar.new foo.use\_enum 1 foo.use\_enum 2 foo.use\_enum 9 # put this code in a file 'bar.rb', start irb and type: load 'bar.rb' </code></pre>

<p>большинство людей используют символы (это синтаксис <code>:foo\_bar</code>). это своего рода уникальные непрозрачные значения. символы не принадлежат ни к какому типу enum-стиля, так что они на самом деле не являются точным представлением типа enum в c, но это довольно хорошо.</p>

<p>иногда все, что мне нужно, - это получить значение enum и идентифицировать его имя, похожее на мир java.</p><pre><code>module enum def get\_value(str) const\_get(str) end def get\_name(sym) sym.to\_s.upcase end end class fruits include enum apple = "delicious" mango = "sweet" end fruits.get\_value('apple') #'delicious' fruits.get\_value('mango') # 'sweet' fruits.get\_name(:apple) # 'apple' fruits.get\_name(:mango) # 'mango' </code></pre><p>это для меня служит цели enum и делает его очень расширяемым. вы можете добавить больше методов в класс enum, и альты получат их бесплатно во всех определенных перечислениях. например. get\_all\_names и тому подобное.</p>

<pre><code>irb(main):016:0> num=[1,2,3,4] irb(main):017:0> alph=['a','b','c','d'] irb(main):018:0> l\_enum=alph.to\_enum irb(main):019:0> s\_enum=num.to\_enum irb(main):020:0> loop do irb(main):021:1* puts "#{s\_enum.next} - #{l\_enum.next}" irb(main):022:1> end </code></pre><p>результат:</p><p>1 - а<br> 2 - б<br> 3 - с<br> 4 - д</p>

<pre><code>module status bad = 13 good = 24 def self.to\_str(status) for sym in self.constants if self.const\_get(sym) == status return sym.to\_s end end end end mystatus = status::good puts status::to\_str(mystatus) </code></pre><p>результат:</p><pre><code>good </code></pre>

<p>быстро и грязно, похоже на c #:</p><pre><code>class feelslikeanenum def self.option\_1() :option\_1 end def self.option\_2() :option\_2 end def self.option\_3() :option\_3 end end </code></pre><p>используйте его, как если бы вы использовали enum:</p><pre><code>method\_that\_needs\_options(feelslikeanenum.option\_1) </code></pre>

<p>я думаю, что лучший способ реализовать перечислимые типы как с помощью символов, так как они ведут себя как целые числа (когда дело касается производительности, для сравнения используется object\_id); вам не нужно беспокоиться об индексации, и они выглядят очень аккуратно в вашем коде xd</p>

<p>другой подход заключается в использовании класса ruby с хешем, содержащим имена и значения, как описано в следующем <a href="http://www.rubyfleebie.com/enumerations-and-ruby/">сообщении в блоге rubyfleebie</a> . это позволяет вам легко преобразовывать значения и константы (особенно если вы добавляете метод класса для поиска имени для данного значения).</p>

<p>другой способ подражать перечислению с последовательной обработкой равенства (бесстыдно принятый от дейва томаса). позволяет открывать перечисления (как символы) и закрытые (предопределенные) перечисления.</p><pre><code>class enum def self.new(values = nil) enum = class.new do unless values def self.const\_missing(name) const\_set(name, new(name)) end end def initialize(name) @enum\_name = name end def to\_s "#{self.class}::#@enum\_name" end end if values enum.instance\_eval do values.each { |e| const\_set(e, enum.new(e)) } end end enum end end genre = enum.new %w(gothic metal) # creates closed enum architecture = enum.new # creates open enum genre::gothic == genre::gothic # => true genre::gothic != architecture::gothic # => true </code></pre>

<p>попробуйте inum . <a href="https://github.com/alfa-jpn/inum">https://github.com/alfa-jpn/inum</a> </p><pre class="lang-js prettyprint-override"><code>class color < inum::base define :red define :green define :blue end </code></pre><pre class="lang-js prettyprint-override"><code>color::red color.parse('blue') # => color::blue color.parse(2) # => color::green </code></pre><p>увидеть больше <a href="https://github.com/alfa-jpn/inum#usage">https://github.com/alfa-jpn/inum#usage</a> </p>