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 #.
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 #.
Respostas:
Dois caminhos. Símbolos ( :foo
notação) ou constantes ( FOO
notaçã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
:minnesota.to_s
ao 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.
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.
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.
Eu uso a seguinte abordagem:
class MyClass
MY_ENUM = [MY_VALUE_1 = 'value1', MY_VALUE_2 = 'value2']
end
Gosto pelas seguintes vantagens:
MY_ENUM
MY_VALUE_1
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
)
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
Conversation
classe - acredito que deve permitir apenas 1 instância.
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
Confira a gema ruby-enum, https://github.com/dblock/ruby-enum .
class Gender
include Enum
Gender.define :MALE, "male"
Gender.define :FEMALE, "female"
end
Gender.all
Gender::MALE
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
class ABC; end
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
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 fetch
vez 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.
FOO_VALUES = {missing: 0, something: 1, something_else: 2, ...}
Isto define os símbolos-chave. missing
, something
Etc., e também os torna comparáveis via os valores associados.)
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.
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 Set
tipo 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"}>
Set[:a, :b, :c]
?
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).
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'
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
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_case
e que os símbolos existam :lower_case
.
server_Symb.each_with_index { |e,i| server_Enum[e] = i}
. Não há necessidade i = 0
.
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
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.
:VAL
. Seria melhor começar com uma matriz e construir o hash utilizando.map.with_index
.key
ou .invert
em vez de uma :VAL
chave ( stackoverflow.com/a/10989394/2208016 )
key
ouinvert
A maioria das pessoas usa símbolos (essa é a :foo_bar
sintaxe). 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.
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_enum
dá-lhe uma enumera tor , enquanto que enum
no C # / sentido Java é uma enumera ção
À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.
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).
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
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
Experimente o inum. https://github.com/alfa-jpn/inum
class Color < Inum::Base
define :RED
define :GREEN
define :BLUE
end
Color::RED
Color.parse('blue') # => Color::BLUE
Color.parse(2) # => Color::GREEN
veja mais https://github.com/alfa-jpn/inum#usage