Como implementar Enums em Ruby?


324

Qual é a melhor maneira de implementar o idioma enum em Ruby? Estou procurando algo que eu possa usar (quase) como as enums do Java / C #.


7
@uramo, boa pergunta e ótima opção para a melhor resposta. Ame ou odeie, você não tem segurança de tipo e (pelo menos em Ruby) não tem erro de digitação. Fiquei emocionado quando descobri enums em C # e mais tarde em Java (escolha um valor, mas dentre esses!), Ruby não fornece uma maneira real de fazer isso, em nenhum caso.
Dan Rosenstark 11/03/10

2
O problema com esta pergunta é que Java e C # enums são coisas dramaticamente diferentes. Um membro enum Java é uma instância de objeto e um singleton. Um enum Java pode ter um construtor. Por outro lado, enums de C # são baseadas em valores primitivos. Qual comportamento o pesquisador está procurando? Embora seja provável que o caso C # seja desejado, o Java é mencionado explicitamente, em vez de C ou C ++, então há alguma dúvida. Quanto a sugerir que não há como ser 'seguro' em Ruby, isso é claramente falso, mas você precisa implementar algo mais sofisticado.
user1164178

Respostas:


319

Dois caminhos. Símbolos ( :foonotação) ou constantes ( FOOnotação).

Os símbolos são apropriados quando você deseja aprimorar a legibilidade sem desarrumar o código com cadeias literais.

postal_code[:minnesota] = "MN"
postal_code[:new_york] = "NY"

As constantes são apropriadas quando você tem um valor subjacente importante. Apenas declare um módulo para manter suas constantes e, em seguida, declare as constantes dentro dele.

module Foo
  BAR = 1
  BAZ = 2
  BIZ = 4
end

flags = Foo::BAR | Foo::BAZ # flags = 3

2
E se esses enum também forem armazenados no banco de dados? A notação de símbolo funciona? Duvido ...
Phương Nguyễn

Eu usaria a abordagem de constantes se estivesse salvando em um banco de dados. Obviamente, você precisa fazer algum tipo de pesquisa ao extrair os dados do banco de dados. Você também pode usar algo como :minnesota.to_sao salvar em um banco de dados para salvar a versão em cadeia do símbolo. O Rails, acredito, tem alguns métodos auxiliares para lidar com isso.
mlibby

7
Um módulo não seria melhor para agrupar constantes - como você não fará nenhuma instância dele?
21812 thomthom

3
Apenas um comentário. Ruby está um pouco preocupada com as convenções de nomenclatura, mas não é realmente óbvia sobre elas até você tropeçar nelas. Os nomes das enumerações devem estar em maiúsculas e a primeira letra do nome do módulo deve ser maiúscula para que o ruby ​​saiba que o módulo é um módulo de constantes.
Rokujolady # 8/13

3
Não é inteiramente verdade. A primeira letra da constante deve ser maiúscula, mas nem todas as letras precisam ser. Essa é uma questão de preferência da convenção. Por exemplo, todos os nomes de módulos e nomes de classes também são constantes.
Michael Brown

59

Estou surpreso que ninguém tenha oferecido algo como o seguinte (extraído da gema RAPI ):

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

Que pode ser usado assim:

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

Exemplo:

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

Isso funciona bem em cenários de banco de dados ou ao lidar com constantes / enumerações de estilo C (como é o caso do FFI , do qual o RAPI faz uso extensivo).

Além disso, você não precisa se preocupar com erros de digitação que causam falhas silenciosas, como faria com o uso de uma solução do tipo hash.


1
Essa é uma ótima maneira de resolver esse problema específico, mas a razão pela qual ninguém sugeriu que provavelmente tenha a ver com o fato de não ser muito parecido com enumerações C # / Java.
mlibby

1
Isso é um pouco incompleto, mas serve como uma boa dica de como você pode implementar soluções com uma abordagem dinâmica. Ele tem alguma semelhança com uma enumeração C # com o conjunto FlagsAttribute, mas, como as soluções baseadas em símbolos / constantes acima, é uma resposta de muitas. O problema é a pergunta original, que é confusa em sua intenção (C # e Java não são intercambiáveis). Existem várias maneiras de especificar objetos no Ruby; selecionar o caminho certo depende do problema que está sendo resolvido. Replicar servilmente os recursos dos quais você não precisa é equivocado. A resposta correta precisa depender do contexto.
user1164178

52

A maneira mais idiomática de fazer isso é usar símbolos. Por exemplo, em vez de:

enum {
  FOO,
  BAR,
  BAZ
}

myFunc(FOO);

... você pode simplesmente usar símbolos:

# 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)

Isso é um pouco mais aberto do que enums, mas se encaixa bem com o espírito Ruby.

Os símbolos também funcionam muito bem. Comparar dois símbolos para igualdade, por exemplo, é muito mais rápido do que comparar duas cadeias.


107
Então o espírito Ruby é: "Typos irá compilar"
mxcl

82
As estruturas populares do Ruby dependem muito da metaprogramação em tempo de execução, e executar muita verificação no tempo de carregamento tiraria a maior parte do poder expressivo do Ruby. Para evitar problemas, a maioria dos programadores Ruby pratica o design orientado a testes, que encontrará não apenas erros de digitação, mas também erros de lógica.
emk

10
@ yar: Bem, o design de linguagem é uma série de vantagens e recursos de linguagem interagem. Se você deseja uma linguagem boa e altamente dinâmica, vá com Ruby, escreva seus testes de unidade primeiro e siga o espírito da linguagem. :-) Se não é isso que você procura, existem dezenas de outros idiomas excelentes por aí, cada um dos quais faz trocas diferentes.
emk

10
@emk, eu concordo, mas meu problema pessoal é que me sinto bastante à vontade em Ruby, mas não me refiro à refatoração em Ruby. E agora que comecei a escrever testes de unidade (finalmente), percebo que eles não são uma panacéia: meu palpite é 1) que o código Ruby não é massivamente refatorado com frequência, na prática, e 2) Ruby não é o fim em termos de linguagens dinâmicas, precisamente porque é difícil refatorar automaticamente. Veja minha pergunta 2317579, que foi assumida, estranhamente, pelo pessoal da Smalltalk.
Dan Rosenstark 10/03/10

4
Sim, mas o uso dessas strings não estaria no espírito da linguagem C #, é simplesmente uma prática ruim.
Ed S.

38

Eu uso a seguinte abordagem:

class MyClass
  MY_ENUM = [MY_VALUE_1 = 'value1', MY_VALUE_2 = 'value2']
end

Gosto pelas seguintes vantagens:

  1. Agrupa valores visualmente como um todo
  2. Ele faz uma verificação do tempo de compilação (ao contrário do uso de símbolos)
  3. Eu posso acessar facilmente a lista de todos os valores possíveis: apenas MY_ENUM
  4. Eu posso acessar facilmente valores distintos: MY_VALUE_1
  5. Pode ter valores de qualquer tipo, não apenas Symbol

Os símbolos podem ser melhores porque você não precisa escrever o nome da classe externa, se estiver usando-o em outra classe ( MyClass::MY_VALUE_1)


4
Eu acho que essa é a melhor resposta. A funcionalidade, sintaxe e sobrecarga mínima de código se aproximam do Java / C #. Além disso, você pode aninhar as definições ainda mais profundamente que um nível e ainda recuperar todos os valores com MyClass :: MY_ENUM.flatten. Como observação, eu usaria nomes em maiúsculas aqui, como é o padrão para constantes no Ruby. MyClass :: MyEnum pode ser confundido com uma referência a uma subclasse.
Janosch

@ Janosch, eu atualizei os nomes. obrigado pela sugestão
Alexey

Eu ainda estou um pouco confuso, e o link 410'd (não, não 404). Você poderia dar exemplos de como esse enum seria usado?
21415 Shelvacu

17

Se você estiver usando o Rails 4.2 ou superior, poderá usar as enumerações do Rails.

O Rails agora tem enums por padrão, sem a necessidade de incluir gemas.

Isso é muito semelhante (e mais com os recursos) ao Java, enums C ++.

Citado em http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html :

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

7
Como você disse - não é útil se o OP não estiver usando o Rails (ou mais precisamente, o objeto não é do tipo ActiveRecord). Apenas explicar o meu voto negativo é tudo.
Ger

2
Essas não são enums no Ruby, é uma interface do ActiveRecord para as enums no seu banco de dados. Não é uma solução generalizável que pode ser aplicada em qualquer outro caso de uso.
Adam Lassek

Eu já mencionei isso na minha resposta.
vedant 13/05/16

Esta é a melhor resposta do IFF usando o Rails.
theUtherSide

Não gosto porque deve ser armazenado em um banco de dados Rails (para funcionar) e porque permite criar muitas instâncias da Conversationclasse - acredito que deve permitir apenas 1 instância.
prograils

8

Esta é a minha abordagem para enums em Ruby. Eu estava indo para curto e doce, não necessariamente o mais parecido com C. Alguma ideia?

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


8

Talvez a melhor abordagem leve seja

module MyConstants
  ABC = Class.new
  DEF = Class.new
  GHI = Class.new
end

Dessa maneira, os valores têm nomes associados, como em Java / C #:

MyConstants::ABC
=> MyConstants::ABC

Para obter todos os valores, você pode fazer

MyConstants.constants
=> [:ABC, :DEF, :GHI] 

Se você deseja o valor ordinal de uma enumeração, pode fazer

MyConstants.constants.index :GHI
=> 2

1
IMHO isso replica muito de perto o uso e a finalidade (segurança de tipo) do Java, também, por uma questão de preferência, as constantes podem ser definidas assim:class ABC; end
wik

8

Eu sei que já faz muito tempo desde que o cara postou esta pergunta, mas eu tinha a mesma pergunta e essa postagem não me deu a resposta. Eu queria uma maneira fácil de ver o que o número representa, a comparação fácil e, acima de tudo, o suporte ao ActiveRecord para pesquisa usando a coluna que representa a enumeração.

Não encontrei nada, então fiz uma implementação incrível chamada yinum que permitiu tudo o que estava procurando. Feito toneladas de especificações, então eu tenho certeza que é seguro.

Alguns exemplos de recursos:

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

5

Se você estiver preocupado com erros de digitação com símbolos, verifique se o seu código gera uma exceção ao acessar um valor com uma chave inexistente. Você pode fazer isso usando, em fetchvez de []:

my_value = my_hash.fetch(:key)

ou fazendo o hash gerar uma exceção por padrão, se você fornecer uma chave inexistente:

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

Se o hash já existir, você poderá adicionar um comportamento de criação de exceção:

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

Normalmente, você não precisa se preocupar com a segurança de erros de digitação com constantes. Se você escreve incorretamente um nome constante, isso geralmente gera uma exceção.


Parece que você está defendendo emular enums com hashes , sem dizer isso explicitamente. Pode ser uma boa ideia editar sua resposta para dizer isso. (Eu também tenho atualmente uma necessidade de algo como enums em Ruby, e minha primeira abordagem para resolvê-lo é usando hashes: FOO_VALUES = {missing: 0, something: 1, something_else: 2, ...}Isto define os símbolos-chave. missing, somethingEtc., e também os torna comparáveis via os valores associados.)
Teemu Leisti

Quero dizer, sem dizer isso no início da resposta.
Teemu Leisti

4

Alguém foi em frente e escreveu uma gema de rubi chamada Renum . Alega obter o comportamento mais próximo de Java / C #. Pessoalmente, ainda estou aprendendo Ruby, e fiquei um pouco chocado quando quis fazer com que uma classe específica contivesse um enum estático, possivelmente um hash, que não foi encontrado com facilidade pelo google.


Eu nunca precisei de um enum em Ruby. Símbolos e constantes são idiomáticos e resolvem os mesmos problemas, não são?
Chuck #

Provavelmente Chuck; mas pesquisar por um enum em rubi não o levará tão longe. Ele mostrará os resultados da melhor tentativa das pessoas em um equivalente direto. O que me faz pensar, talvez haja algo de bom em envolver o conceito.
Dlamblin 5/03/09

@Chuck Símbolos e constantes não impõem, por exemplo, que um valor deva ser um de um pequeno conjunto de valores.
David Moles

3

Tudo depende de como você usa Java ou C # enums. A forma como você o usa determinará a solução que você escolherá no Ruby.

Experimente o Settipo nativo , por exemplo:

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

9
Por que não usar símbolos Set[:a, :b, :c]?
Dan Rosenstark 11/03/10

2
Prática muito melhor para usar símbolos aqui, IMO.
Collin Graves

3

Recentemente, lançamos uma gema que implementa Enums em Ruby . No meu post, você encontrará as respostas para suas perguntas. Também descrevi lá por que nossa implementação é melhor do que a existente (na verdade, existem muitas implementações desse recurso no Ruby ainda como gemas).


Permite valores autoincrementáveis, sem especificá-los explicitamente. +1
dimid

3

Outra solução está usando o OpenStruct. É bem simples e limpo.

https://ruby-doc.org/stdlib-2.3.1/libdoc/ostruct/rdoc/OpenStruct.html

Exemplo:

# 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'

2

Símbolos é o caminho do rubi. No entanto, às vezes é preciso conversar com algum código C ou algo ou Java que exponha algum enum para várias coisas.


#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

Isso pode ser usado assim


require 'server_roles'

sSymb, sEnum =EnumLike.server_role()

foreignvec[sEnum[:SERVER_WORKSTATION]]=8

É claro que isso pode ser feito abstrato e você pode criar nossa própria classe Enum


Você está capitalizando a segunda palavra em variáveis ​​(por exemplo server_Symb) por um motivo específico? A menos que haja uma razão específica, é idiomático que as variáveis ​​sejam snake_case_with_all_lower_casee que os símbolos existam :lower_case.
Andrew Grimm

1
@Andrew; esse exemplo foi extraído de uma coisa do mundo real e a documentação do protocolo de rede usou xxx_Yyy; portanto, o código em vários idiomas usou o mesmo conceito para que se pudesse acompanhar as alterações de especificação.
21711 Jonke

1
Golfe código: server_Symb.each_with_index { |e,i| server_Enum[e] = i}. Não há necessidade i = 0.
Andrew Grimm

2

Eu implementei enums como esse

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

então é fácil fazer operações

EnumType.ENUM_1.label

...

enum = EnumType.find_by_id 1

...

valueArray = EnumType.values

2

Isso parece um pouco supérfluo, mas é uma metodologia que já usei algumas vezes, especialmente onde estou me integrando ao xml ou algo assim.

#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

Isso me dá o rigor do ac # enum e está vinculado ao modelo.


Eu não recomendaria essa abordagem porque ela depende de você definir manualmente os valores e garantir que você receba o pedido corretamente :VAL. Seria melhor começar com uma matriz e construir o hash utilizando.map.with_index
DaveMongoose

1
O ponto exato é amarrar-se a um valor que é ditado por terceiros. Não se trata de extensibilidade propriamente dita, mas de ter que lidar com restrições externas que afetam a computabilidade dentro dos limites do processo.
JJK

Ponto justo! Nesse caso, ele definitivamente faz sentido para especificar os valores, mas eu estaria inclinado a fazer o lookup reverso com .keyou .invertem vez de uma :VALchave ( stackoverflow.com/a/10989394/2208016 )
DaveMongoose

Sim, isso é (de volta para você) um ponto justo. Meu rubi era deselegante e pesado. Def seria usar keyouinvert
jjk 14/11/19

1

A maioria das pessoas usa símbolos (essa é a :foo_barsintaxe). Eles são uma espécie de valores opacos únicos. Os símbolos não pertencem a nenhum tipo de enumeração, por isso não são uma representação fiel do tipo de enumeração de C, mas isso é praticamente o melhor possível.


1
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

Resultado:

1 - a
2 - b
3 - c
4 - d


to_enumdá-lhe uma enumera tor , enquanto que enumno C # / sentido Java é uma enumera ção
DaveMongoose

1
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)

Resultado:

GOOD

1

Às vezes, tudo o que preciso é ser capaz de buscar o valor de enum e identificar seu nome semelhante ao mundo java.

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'

Isso para mim serve ao propósito de enum e o mantém extensível também. Você pode adicionar mais métodos à classe Enum e o viola os obtém gratuitamente em todas as enumerações definidas. por exemplo. get_all_names e coisas assim.


0

Outra abordagem é usar uma classe Ruby com um hash contendo nomes e valores, conforme descrito na seguinte postagem no blog do RubyFleebie . Isso permite converter facilmente entre valores e constantes (especialmente se você adicionar um método de classe para pesquisar o nome de um determinado valor).


0

Eu acho que a melhor maneira de implementar enumeração como tipos é com símbolos, já que eles se comportam como números inteiros (quando se trata de desempenho, object_id é usado para fazer comparações); você não precisa se preocupar com a indexação e eles parecem muito legais no seu código xD


0

Outra maneira de imitar uma enumeração com o tratamento consistente da igualdade (adotado descaradamente por Dave Thomas). Permite enumerações abertas (como símbolos) e enumerações fechadas (predefinidas).

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.