如何在Ruby中实现枚举?


323

在Ruby中实现枚举用法的最佳方法是什么?我正在寻找(几乎)可以使用的东西,例如Java / C#枚举。


7
@auramo,好问题,是最佳答案的最佳选择。爱它或恨它,您将没有类型安全性(至少在Ruby中),也没有拼写安全性。当我在C#和后来的Java中发现枚举时,我感到很兴奋(从中选择一个值,但是从中!),Ruby根本没有提供一种在任何情况下都能做到这一点的真正方法。
丹·罗森斯塔克

2
这个问题的问题在于Java和C#枚举是截然不同的东西。Java枚举成员是对象实例和单例。Java枚举可以具有构造函数。相反,C#枚举基于原始值。发问者在寻找哪种行为?虽然很可能需要C#的情况,但明确提到了Java,而不是C或C ++,因此存在一些疑问。至于暗示没有办法在Ruby中做到“安全”,这显然是错误的,但是您必须实现更复杂的东西。
user1164178'2

Answers:


317

两种方式。符号(:foo符号)或常量(FOO符号)。

当您想提高可读性而不用文字字符串乱码时,使用符号是合适的。

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

当您具有重要的基础值时,常数是适当的。只需声明一个模块来保存您的常量,然后在其中声明常量。

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

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

2
如果这些枚举也存储在数据库中怎么办?符号符号会起作用吗?我怀疑...
PHUONG阮

如果要保存到数据库,我将使用常量方法。当然,在将数据从数据库中拉回时,您必须进行某种查找。:minnesota.to_s当保存到数据库中时,还可以使用类似的方式来保存符号的字符串版本。我相信,Rails有一些辅助方法可以解决其中的一些问题。
mlibby 2010年

7
用模块对常量进行分组是否会更好-因为您将不对其进行任何实例化?
thomthom 2012年

3
只是一个评论。Ruby在命名约定方面有些痛苦,但是在您不习惯它们之前,对它们的了解并不十分明显。枚举的名称必须全部大写,并且模块名称的首字母必须大写,以使ruby知道该模块是常量模块。
Rokujolady

3
并非完全正确。常量的第一个字母必须大写,但并非所有字母都必须大写。这是惯例优先的问题。例如,所有模块名称和类名称实际上也是常量。
迈克尔·布朗

59

我很惊讶没有人提供以下内容(从RAPI gem中获得):

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

可以这样使用:

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

例:

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

这在数据库方案中或在处理C样式常量/枚举(如使用FFI的情况)时非常有效 RAPI广泛使用的很好。

而且,您不必担心像输入错误一样会导致无提示失败,就像使用哈希类型的解决方案一样。


1
这是解决该特定问题的好方法,但是没有人提出它的原因可能与它不像C#/ Java枚举这一事实有关。
mlibby 2012年

1
这有点不完整,但是可以很好地提示您如何使用动态方法来实现解决方案。它与设置了FlagsAttribute的C#枚举有些相似,但是像上面基于符号/常量的解决方案一样,它是许多答案的一个。问题是原始问题,它的意图是混乱的(C#和Java不可互换)。在Ruby中有很多方法可以逐项列出对象。选择合适的解决方案取决于要解决的问题。错误地复制不需要的功能。正确答案必须取决于上下文。
user1164178

52

最惯用的方法是使用符号。例如,代替:

enum {
  FOO,
  BAR,
  BAZ
}

myFunc(FOO);

...您可以只使用符号:

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

它比枚举更开放,但是与Ruby精神非常契合。

符号的表现也很好。例如,比较两个符号是否相等要比比较两个字符串快得多。


107
因此,Ruby的精神是:“错别字会编译”
mxcl

82
流行的Ruby框架在很大程度上依赖于运行时元编程,而执行过多的加载时检查将失去Ruby的大多数表达能力。为了避免出现问题,大多数Ruby程序员都会实践测试驱动的设计,这种设计不仅会发现拼写错误,还会发现逻辑错误。
emk

10
@yar:好吧,语言设计需要权衡一系列因素,并且语言功能会相互作用。如果您想使用一种良好的,高度动态的语言,请使用Ruby,首先编写单元测试,然后遵循该语言的精神。:-)如果这不是您想要的,那么这里还有数十种其他出色的语言,每种语言都有不同的权衡。
emk 2010年

10
@emk,我同意,但是我个人的问题是我对Ruby感到很自在,但是我对Ruby的重构感到不自在。现在,我已经开始(最终)编写单元测试了,我意识到它们不是万能药:我的猜测是:1)在实践中,Ruby代码不会经常被大量重构; 2)Ruby并非最终目标就动态语言而言,正是因为很难自动重构。看到我的问题2317579,奇怪地,它被Smalltalk的人们接管了。
丹·罗森斯塔克

4
是的,但是使用这些字符串并不符合C#语言的精神,这只是一种不好的做法。
Ed S.

38

我使用以下方法:

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

我喜欢它具有以下优点:

  1. 它在视觉上将价值分组为一个整体
  2. 它会进行一些编译时检查(与仅使用符号相反)
  3. 我可以轻松访问所有可能值的列表: MY_ENUM
  4. 我可以轻松访问不同的值: MY_VALUE_1
  5. 它可以具有任何类型的值,而不仅仅是Symbol

符号可能更好,因为如果您在另一个类(MyClass::MY_VALUE_1)中使用它,则不必写外部类的名称。


4
我认为这是最好的答案。功能,语法和最少的代码开销最接近Java / C#。另外,您甚至可以嵌套比一个级别更深的定义,并仍然使用MyClass :: MY_ENUM.flatten恢复所有值。附带说明一下,我将在这里使用大写名称,这是Ruby中常量的标准。MyClass :: MyEnum可能被误认为是对子类的引用。
Janosch

@Janosch,我更新了名字。感谢您的建议
Alexey 2015年

我还是有点困惑,链接410'(不,不是404)。您能否举例说明如何使用此枚举?
谢尔瓦库2015年

17

如果您使用的是Rails 4.2或更高版本,则可以使用Rails枚举。

Rails现在默认情况下具有枚举,无需包含任何gem。

这与Java,C ++枚举非常相似(并且在功能上更多)。

引用自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
如您所说-如果OP没有使用Rails(或更准确地说,该对象不是ActiveRecord类型),则没有用。仅解释我的反对意见即可。
2015年

2
这些不是Ruby中的枚举,它是数据库中Enums的ActiveRecord接口。不是可以在任何其他用例中应用的通用解决方案。
亚当·拉瑟克

我有一个领导在答复中提到了这一点。
吠丹

这是IFF使用Rails的最佳答案。
theUtherSide

我不喜欢它,因为它必须存储在Rails数据库中(才能工作),并且因为它允许创建Conversation该类的许多实例-我认为它只能允许一个实例。
祝贺

8

这是我在Ruby中枚举的方法。我追求短暂而甜蜜,不一定是最喜欢C的人。有什么想法吗?

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

也许最好的轻量级方法是

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

这样,值就具有关联的名称,如Java / C#中那样:

MyConstants::ABC
=> MyConstants::ABC

要获取所有值,您可以执行

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

如果您想要枚举的序数值,则可以执行

MyConstants.constants.index :GHI
=> 2

1
恕我直言,这非常接近地复制了Java的用途和目的(类型安全性),并且,作为优先事项,常量可以这样定义:class ABC; end
wik

8

我知道这个人发布这个问题已经有很长时间了,但是我也有同样的问题,而这个发布并没有给我答案。我想要一种简单的方法来查看数字表示的内容,进行简单的比较,以及使用表示枚举的列对查询进行的所有ActiveRecord支持。

我什么都没找到,所以我做了一个叫yinum的出色实现,它允许我寻找的所有内容。做了很多规格,所以我很确定它是安全的。

一些示例功能:

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

如果您担心带符号的错字,请确保在使用不存在的键访问值时代码会引发异常。您可以使用fetch而不是[]

my_value = my_hash.fetch(:key)

或者如果您提供不存在的密钥,则默认情况下通过使哈希引发异常来实现:

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

如果哈希已经存在,则可以添加异常引发行为:

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

通常,您不必担心常量的错字安全性。如果您拼写错误的常量名称,通常会引发异常。


似乎您在倡导用哈希值模仿枚举,而不必这么明确地说。编辑您的答案以这样说可能是个好主意。(我也现在有需要类似Ruby的枚举,我的第一个方法来解决它是通过使用散列:FOO_VALUES = {missing: 0, something: 1, something_else: 2, ...}此界定重点符号。missingsomething等,也使他们通过关联的值相媲美。)
Teemu Leisti

我的意思是,在回答之初就不这样说。
Teemu Leisti

4

有人继续写了一个名为Renum的红宝石。它声称获得最接近的Java / C#行为。就我个人而言,我仍在学习Ruby,当我想让特定的类包含静态枚举(可能是哈希)时,我感到有些震惊,因为它不是通过Google完全找到的。


我从不需要Ruby中的枚举。符号和常量是惯用的,可以解决相同的问题,不是吗?
Chuck

大概查克 但是谷歌搜索红宝石枚举不会使您走得那么远。它会向您显示人们在直接等效条件下的最佳尝试结果。这让我感到奇怪,将概念打包在一起也许有些不错。
dlamblin

@Chuck符号和常量不强制执行,例如,值必须是一小部分值中的一个。
David Moles

3

这完全取决于您如何使用Java或C#枚举。如何使用它将决定您将在Ruby中选择的解决方案。

尝试使用本机Set类型,例如:

>> 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
为什么不使用符号Set[:a, :b, :c]
丹·罗森斯塔克

2
IMO在这里使用符号的更好的做法。
Collin Graves

3

最近,我们发布了一个在Ruby中实现Enumsgem。在我的文章中,您会找到有关问题的答案。我还在那里描述了为什么我们的实现比现有的更好(实际上,Ruby中有很多此功能的实现,但它们都是宝石)。


它允许自增值,而无需明确说明。+1
dimid '16

3

另一个解决方案是使用OpenStruct。它非常简单和干净。

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

例:

# 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

符号是红宝石的方式。但是,有时需要与一些C代码或某些Java或某些暴露某些事物的Java进行交谈。


#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

然后可以这样使用


require 'server_roles'

sSymb, sEnum =EnumLike.server_role()

foreignvec[sEnum[:SERVER_WORKSTATION]]=8

当然可以使它抽象,您可以滚动我们自己的Enum类


您是否server_Symb出于特定原因将变量中的第二个单词大写(例如)?除非有特殊原因,否则变量为snake_case_with_all_lower_case,符号为是惯用法:lower_case
Andrew Grimm'3

1
@安德鲁; 该示例取自现实世界,网络协议文档使用xxx_Yyy,因此几种语言的代码使用相同的概念,因此可以遵循规范的更改。
Jonke 2011年

1
高尔夫代码:server_Symb.each_with_index { |e,i| server_Enum[e] = i}。不需要i = 0
安德鲁·格林

2

我已经实现了这样的枚举

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

那么它很容易操作

EnumType.ENUM_1.label

...

enum = EnumType.find_by_id 1

...

valueArray = EnumType.values

2

这似乎有点多余,但这是我使用过几次的方法,尤其是在我与xml或类似的东西集成的地方。

#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

这给我带来了ac#枚举的严谨性,并且与模型紧密相关。


我不建议这种方法,因为它依赖于您手动设置值并确保正确获得订单:VAL。最好从数组开始,然后使用.map.with_index
DaveMongoose

1
确切的意义是将自己绑定到第三方指定的值。它本身并不是关于可扩展性,而是关于必须处理影响流程边界内可计算性的无关紧要的约束。
jjk

有道理!在这种情况下,它肯定是有道理的,指定的值,但我倾向于做反向查询,.key.invert而不是一个:VAL键(stackoverflow.com/a/10989394/2208016
DaveMongoose

是的,那是(很对不起你)。我的红宝石笨拙而笨拙。将使用def key还是invert
–jjk

1

大多数人使用符号(这是:foo_bar语法)。它们是唯一的不透明值。符号不属于任何枚举样式类型,因此它们实际上并不是C枚举类型的忠实表示,但是它的含义几乎一样好。


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

输出:

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


to_enum给你一个enumera TOR,而enum在C#/ Java的感觉是一个enumera 重刑
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)

输出:

GOOD

1

有时,我所需要的只是能够获取枚举的值并识别其名称,类似于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'

对我来说,这符合枚举的目的,并且使其也非常可扩展。您可以向Enum类添加更多方法,中提琴可以在所有定义的枚举中免费获取它们。例如。get_all_names之类的东西。



0

我认为实现像类型这样的枚举的最佳方法是使用符号,因为符号的行为几乎都是整数(当涉及到performance时,使用object_id进行比较);您无需担心索引编制,它们在您的代码xD中看起来非常整洁


0

模仿具有一致的相等性处理的枚举的另一种方法(从Dave Thomas无耻地采用)。允许打开的枚举(很像符号)和关闭的(预定义)枚举。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.