¿Cómo implementar Enums en Ruby?


324

¿Cuál es la mejor manera de implementar el lenguaje de enumeración en Ruby? Estoy buscando algo que pueda usar (casi) como las enumeraciones Java / C #.


77
@auramo, buena pregunta y excelente opción para la mejor respuesta. Lo ames o lo odies, no obtienes seguridad de escritura y (al menos en Ruby) no hay seguridad de escritura. Me emocioné cuando descubrí enumeraciones en C # y más tarde en Java (¡elija un valor, pero de estos!), Ruby no proporciona una forma real de hacerlo en ningún caso.
Dan Rosenstark

2
El problema con esta pregunta es que las enumeraciones Java y C # son cosas dramáticamente diferentes. Un miembro de enumeración de Java es una instancia de objeto y un singleton. Una enumeración de Java puede tener un constructor. Por el contrario, las enumeraciones de C # se basan en valores primitivos. ¿Qué comportamiento está buscando el interrogador? Si bien es probable que se desee el caso C #, Java se menciona explícitamente, en lugar de C o C ++, por lo que hay algunas dudas. En cuanto a sugerir que no hay forma de estar 'seguro' en Ruby, eso es transparentemente falso, pero hay que implementar algo más sofisticado.
user1164178

Respuestas:


319

Dos caminos. Símbolos ( :foonotación) o constantes ( FOOnotación).

Los símbolos son apropiados cuando desea mejorar la legibilidad sin ensuciar el código con cadenas literales.

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

Las constantes son apropiadas cuando tiene un valor subyacente que es importante. Simplemente declare un módulo para contener sus constantes y luego declare las constantes dentro de eso.

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

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

2
¿Qué pasa si estas enumeraciones también se almacenan en la base de datos? ¿Funcionará la notación de símbolos? Dudo ...
Phương Nguyễn

Usaría el enfoque de constantes si estuviera guardando en una base de datos. Por supuesto, entonces debe hacer algún tipo de búsqueda al extraer los datos de la base de datos. También puede usar algo como :minnesota.to_sguardar en una base de datos para guardar la versión de cadena del símbolo. Rails, creo, tiene algunos métodos auxiliares para lidiar con algo de esto.
mlibby

77
¿No sería mejor un módulo para agrupar constantes, ya que no va a hacer ninguna instancia de él?
thomthom

3
Solo un comentario. Ruby es un poco molesto por nombrar convenciones pero no es realmente obvio sobre ellas hasta que te tropiezas con ellas. Los nombres de las enumeraciones deben estar en mayúsculas y la primera letra del nombre del módulo debe estar en mayúscula para que Ruby sepa que el módulo es un módulo de constantes.
Rokujolady

3
No del todo cierto. La primera letra de la constante debe estar en mayúscula, pero no todas las letras deben ser. Esta es una cuestión de preferencia de convención. Por ejemplo, todos los nombres de módulos y nombres de clases son en realidad constantes también.
Michael Brown

59

Me sorprende que nadie haya ofrecido algo como lo siguiente (cosechado de la 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 se puede usar así:

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

Ejemplo:

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

Esto funciona bien en escenarios de bases de datos, o cuando se trata de constantes / enumeraciones de estilo C (como es el caso cuando se usa FFI , que RAPI hace un uso extensivo).

Además, no tiene que preocuparse por los errores tipográficos que causan fallas silenciosas, como lo haría con el uso de una solución de tipo hash.


1
Esa es una excelente manera de resolver ese problema en particular, pero la razón por la que nadie sugirió que probablemente tenga que ver con el hecho de que no se parece mucho a las enumeraciones de C # / Java.
mlibby

1
Esto es un poco incompleto, pero sirve como una buena pista sobre cómo implementar soluciones con un enfoque dinámico. Tiene cierta semejanza con una enumeración de C # con el conjunto FlagsAttribute, pero como el símbolo / las soluciones basadas en constantes anteriores, es una respuesta de muchas. El problema es la pregunta original, que está confusa en su intención (C # y Java no son intercambiables). Hay muchas formas de detallar objetos en Ruby; seleccionar el correcto depende del problema que se resuelva. Las funciones de replicación servil que no necesita están mal orientadas. La respuesta correcta tiene que depender del contexto.
user1164178

52

La forma más idiomática de hacer esto es usar símbolos. Por ejemplo, en lugar de:

enum {
  FOO,
  BAR,
  BAZ
}

myFunc(FOO);

... solo puedes 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)

Esto es un poco más abierto que las enumeraciones, pero encaja bien con el espíritu de Ruby.

Los símbolos también funcionan muy bien. Comparar dos símbolos para la igualdad, por ejemplo, es mucho más rápido que comparar dos cadenas.


107
Así que el espíritu de Ruby es: "Los
errores tipográficos

82
Los marcos populares de Ruby dependen en gran medida de la metaprogramación en tiempo de ejecución, y realizar demasiadas verificaciones de tiempo de carga eliminaría la mayor parte del poder expresivo de Ruby. Para evitar problemas, la mayoría de los programadores de Ruby practican el diseño basado en pruebas, que encontrará no solo errores tipográficos sino también errores lógicos.
emk

10
@yar: Bueno, el diseño del lenguaje es una serie de compensaciones, y las características del lenguaje interactúan. Si desea un lenguaje bueno y altamente dinámico, vaya con Ruby, escriba primero sus pruebas unitarias y siga el espíritu del idioma. :-) Si eso no es lo que estás buscando, hay docenas de otros idiomas excelentes, cada uno de los cuales hace diferentes compensaciones.
emk

10
@emk, estoy de acuerdo, pero mi problema personal es que me siento bastante cómodo en Ruby, pero no me siento cómodo refactorizando en Ruby. Y ahora que comencé a escribir pruebas unitarias (finalmente), me doy cuenta de que no son una panacea: mi suposición es 1) que el código Ruby no se refactoriza masivamente tan a menudo, en la práctica y 2) Ruby no es el final -de-la-línea en términos de lenguajes dinámicos, precisamente porque es difícil refactorizar automáticamente. Vea mi pregunta 2317579 que fue tomada, extrañamente, por la gente de Smalltalk.
Dan Rosenstark

44
Sí, pero usar esas cadenas no estaría en el espíritu del lenguaje C #, es simplemente una mala práctica.
Ed S.

38

Yo uso el siguiente enfoque:

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

Me gusta por las siguientes ventajas:

  1. Agrupa los valores visualmente como un todo.
  2. Realiza algunas comprobaciones en tiempo de compilación (en contraste con solo usar símbolos)
  3. Puedo acceder fácilmente a la lista de todos los valores posibles: solo MY_ENUM
  4. Puedo acceder fácilmente a valores distintos: MY_VALUE_1
  5. Puede tener valores de cualquier tipo, no solo Symbol

Los símbolos pueden ser mejores porque no tiene que escribir el nombre de la clase externa, si lo está usando en otra clase ( MyClass::MY_VALUE_1)


44
Creo que esta es la mejor respuesta. La funcionalidad, la sintaxis y la sobrecarga mínima del código se acercan más a Java / C #. También puede anidar las definiciones incluso más profundo que un nivel y aún recuperar todos los valores con MyClass :: MY_ENUM.flatten. Como nota al margen, usaría nombres en mayúsculas aquí, como es el estándar para las constantes en Ruby. MyClass :: MyEnum puede confundirse con una referencia a una subclase.
Janosch

@Janosch, he actualizado los nombres. gracias por su sugerencia
Alexey

Todavía estoy un poco confundido, y el enlace 410'd (no, no 404). ¿Podría dar ejemplos de cómo se usaría esta enumeración?
Shelvacu

17

Si está utilizando Rails 4.2 o superior, puede usar las enumeraciones de Rails.

Rails ahora tiene enumeraciones por defecto sin la necesidad de incluir gemas.

Esto es muy similar (y más con características) a Java, enumeraciones de C ++.

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

77
Como dijiste, no es útil si el OP no está usando Rails (o más exactamente, el objeto no es del tipo ActiveRecord). Solo explicar mi voto negativo es todo.
Ger

2
Estas no son enumeraciones en Ruby, es una interfaz ActiveRecord para enumeraciones en su base de datos. No es una solución generalizable que pueda aplicarse en cualquier otro caso de uso.
Adam Lassek

Ya lo he mencionado en mi respuesta.
Vedant

Esta es la mejor respuesta IFF usando Rails.
theUtherSide

No me gusta porque debe almacenarse en una base de datos Rails (para funcionar) y porque permite crear muchas instancias de la Conversationclase, creo que debe permitir solo 1 instancia.
programa

8

Este es mi enfoque para las enumeraciones en Ruby. Iba por algo corto y dulce, no necesariamente el más parecido a C. ¿Alguna idea?

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

Quizás el mejor enfoque ligero sería

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

De esta manera, los valores tienen nombres asociados, como en Java / C #:

MyConstants::ABC
=> MyConstants::ABC

Para obtener todos los valores, puedes hacer

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

Si desea el valor ordinal de una enumeración, puede hacer

MyConstants.constants.index :GHI
=> 2

1
En mi humilde opinión, esto replica muy de cerca el uso y el propósito (seguridad de tipo) de Java, también, como una cuestión de preferencia, las constantes se pueden definir de esta manera:class ABC; end
wik

8

Sé que ha pasado mucho tiempo desde que el chico publicó esta pregunta, pero tenía la misma pregunta y esta publicación no me dio la respuesta. Quería una manera fácil de ver qué representa el número, una comparación fácil y, sobre todo, el soporte de ActiveRecord para la búsqueda usando la columna que representa la enumeración.

No encontré nada, así que hice una implementación impresionante llamada yinum que permitió todo lo que estaba buscando. Hice toneladas de especificaciones, así que estoy bastante seguro de que es seguro.

Algunas características de ejemplo:

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

Si le preocupan los errores tipográficos con símbolos, asegúrese de que su código genere una excepción cuando acceda a un valor con una clave inexistente. Puede hacerlo utilizando en fetchlugar de []:

my_value = my_hash.fetch(:key)

o haciendo que el hash genere una excepción por defecto si proporciona una clave 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

Si el hash ya existe, puede agregar un comportamiento de aumento de excepciones:

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, no tiene que preocuparse por la seguridad de los errores tipográficos con constantes. Si escribe mal un nombre constante, generalmente generará una excepción.


Parece que estás abogando por emular enumeraciones con hashes , sin decirlo explícitamente. Puede ser una buena idea editar su respuesta para decirlo. (También tienen actualmente una necesidad de algo así como las enumeraciones en Ruby, y mi primer acercamiento a la solución que es mediante el uso de hashes: FOO_VALUES = {missing: 0, something: 1, something_else: 2, ...}esto define los símbolos de las teclas. missing, something, Etc., y también los hace comparables a través de los valores asociados.)
Teemu Leisti

Quiero decir, sin decirlo al comienzo de la respuesta.
Teemu Leisti

4

Alguien siguió adelante y escribió una gema de rubí llamada Renum . Afirma obtener el comportamiento similar a Java / C # más cercano. Personalmente, todavía estoy aprendiendo Ruby, y me sorprendió un poco cuando quise hacer que una clase específica contuviera una enumeración estática, posiblemente un hash, que no se pudo encontrar fácilmente a través de Google.


Nunca he necesitado una enumeración en Ruby. Los símbolos y las constantes son idiomáticos y resuelven los mismos problemas, ¿no?
Chuck

Probablemente Chuck; pero buscar en Google una enumeración en rubí no te llevará tan lejos. Le mostrará resultados para el mejor intento de las personas de un equivalente directo. Lo que me hace preguntarme, tal vez hay algo bueno en tener el concepto envuelto.
dlamblin 05 de

Los símbolos y constantes de @Chuck no imponen, por ejemplo, que un valor tenga que ser uno de un pequeño conjunto de valores.
David Moles

3

Todo depende de cómo use las enumeraciones Java o C #. La forma en que lo use determinará la solución que elegirá en Ruby.

Pruebe el Settipo nativo , por ejemplo:

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

99
¿Por qué no usar símbolos Set[:a, :b, :c]?
Dan Rosenstark

2
Una práctica mucho mejor para usar símbolos aquí, OMI.
Collin Graves

3

Recientemente lanzamos una gema que implementa Enums en Ruby . En mi publicación encontrarás las respuestas a tus preguntas. También describí allí por qué nuestra implementación es mejor que las existentes (en realidad, hay muchas implementaciones de esta característica en Ruby aún como gemas).


Permite valores de auto incremento, sin indicarlos explícitamente. +1
tenue

3

Otra solución es usar OpenStruct. Es bastante sencillo y limpio.

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

Ejemplo:

# 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 es el camino de rubí. Sin embargo, a veces uno necesita hablar con algún código C o algo o Java que exponga alguna enumeración para varias cosas.


#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

Esto se puede usar así


require 'server_roles'

sSymb, sEnum =EnumLike.server_role()

foreignvec[sEnum[:SERVER_WORKSTATION]]=8

Esto, por supuesto, puede hacerse abstracto y puede rodar nuestra propia clase Enum


¿Está capitalizando la segunda palabra en variables (por ejemplo server_Symb) por una razón particular? A menos que haya una razón particular, es idiomático que las variables sean snake_case_with_all_lower_casey que los símbolos sean :lower_case.
Andrew Grimm

1
@Andrés; este ejemplo fue tomado de una cosa del mundo real y la documentación del protocolo de red usó xxx_Yyy, por lo que el código en varios idiomas usó el mismo concepto para poder seguir los cambios de especificación.
Jonke

1
Código de golf: server_Symb.each_with_index { |e,i| server_Enum[e] = i}. No hay necesidad de i = 0.
Andrew Grimm

2

He implementado enumeraciones como esa

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

entonces es fácil de hacer operaciones

EnumType.ENUM_1.label

...

enum = EnumType.find_by_id 1

...

valueArray = EnumType.values

2

Esto parece un poco superfluo, pero esta es una metodología que he usado varias veces, especialmente cuando me estoy integrando con xml o algo así.

#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

Esto me da el rigor de ac # enum y está vinculado al modelo.


No recomendaría este enfoque porque se basa en que usted establezca manualmente los valores y se asegure de obtener el pedido correctamente :VAL. Sería mejor comenzar con una matriz y construir el hash usando.map.with_index
DaveMongoose

1
El punto exacto es vincularse a un valor dictado por terceros. No se trata de la extensibilidad per se, sino de tener que lidiar con restricciones extrañas que afectan la computabilidad dentro de los límites de su proceso.
jjk

¡Punto justo! En ese caso, definitivamente tiene sentido especificar los valores, pero me inclinaría a hacer la búsqueda inversa con .keyo en .invertlugar de una :VALclave ( stackoverflow.com/a/10989394/2208016 )
DaveMongoose el

Sí, ese es (de vuelta a ti) un punto justo. Mi rubí era poco elegante y poco manejable. keyinvert
Definitivamente

1

La mayoría de las personas usan símbolos (esa es la :foo_barsintaxis). Son una especie de valores opacos únicos. Los símbolos no pertenecen a ningún tipo de estilo de enumeración, por lo que no son realmente una representación fiel del tipo de enumeración de C, pero esto es bastante bueno.


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

Salida:

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


to_enumle da una enumera tor , mientras que enumen el C # / Java es un sentido enumera ción
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)

Salida:

GOOD

1

A veces, todo lo que necesito es poder obtener el valor de enum e identificar su nombre de manera similar a java world.

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'

Esto para mí tiene el propósito de enum y también lo mantiene muy extensible. Puede agregar más métodos a la clase Enum y viola obtenerlos de forma gratuita en todas las enumeraciones definidas. por ejemplo. get_all_names y cosas así.


0

Otro enfoque es usar una clase Ruby con un hash que contenga nombres y valores como se describe en la siguiente publicación de blog de RubyFleebie . Esto le permite convertir fácilmente entre valores y constantes (especialmente si agrega un método de clase para buscar el nombre de un valor dado).


0

Creo que la mejor manera de implementar la enumeración como los tipos es con símbolos, ya que se comportan como enteros (cuando se trata de rendimiento, object_id se usa para hacer comparaciones); no necesita preocuparse por la indexación y se ven muy bien en su código xD


0

Otra forma de imitar una enumeración con un manejo de igualdad consistente (adoptado descaradamente de Dave Thomas). Permite enumeraciones abiertas (muy parecidas a los símbolos) y enumeraciones cerradas (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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.