【问题标题】:How to implement Enums in Ruby?如何在 Ruby 中实现枚举?
【发布时间】:2010-09-09 16:46:19
【问题描述】:

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

【问题讨论】:

  • @auramo,好问题,也是最佳答案的绝佳选择。爱它或恨它,你没有类型安全,并且(至少在 Ruby 中)没有错字安全。当我在 C# 和后来的 Java 中发现枚举时,我很激动(选择一个值,但是从这些中选择一个值!),无论如何,Ruby 根本没有提供真正的方法来做到这一点。
  • 这个问题的问题在于 Java 和 C# 枚举是截然不同的东西。 Java 枚举成员是一个对象实例和一个单例。 Java 枚举可以有一个构造函数。相比之下,C# 枚举基于 Primitive 值。提问者在寻找哪种行为?虽然很可能需要 C# 案例,但明确提到了 Java,而不是 C 或 C++,因此存在一些疑问。至于建议在 Ruby 中没有“安全”的方法,这显然是错误的,但你必须实现一些更复杂的东西。

标签: ruby enums


【解决方案1】:

两种方式。符号(: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

于 2021 年 1 月 17 日添加

如果您要传递枚举值(例如,将其存储在数据库中)并且您需要能够将值转换回符号,则可以将这两种方法混搭

COMMODITY_TYPE = {
  currency: 1,
  investment: 2,
}

def commodity_type_string(value)
  COMMODITY_TYPE.key(value)
end

COMMODITY_TYPE[:currency]

这种方法的灵感来自 andrew-grimm 的回答 https://stackoverflow.com/a/5332950/13468

我还建议您阅读此处的其余答案,因为有很多方法可以解决这个问题,并且归结为您关心的其他语言的枚举是什么

【讨论】:

  • 如果这些枚举也被存储到数据库中怎么办?符号表示法会起作用吗?我怀疑...
  • 如果我要保存到数据库,我会使用常量方法。当然,在将数据从数据库中拉出时,您必须进行某种查找。您还可以在保存到数据库时使用 :minnesota.to_s 之类的东西来保存符号的字符串版本。我相信 Rails 有一些辅助方法来处理其中的一些问题。
  • 模块对常量进行分组难道不是更好吗?因为您不会创建它的任何实例?
  • 只是一个评论。 Ruby 对命名约定有点苦恼,但在你绊倒它们之前,它们并不是很明显。枚举的名称必须全部大写,并且模块名称的第一个字母必须大写,以便 ruby​​ 知道该模块是常量模块。
  • 不完全正确。常量的第一个字母必须大写,但并非所有字母都必须大写。这是约定俗成的问题。例如,所有的模块名和类名实际上也是常量。
【解决方案2】:

我很惊讶没有人提供类似以下的东西(摘自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 风格的常量/枚举时表现良好(例如使用 RAPI 广泛使用的 FFI 时的情况)。

此外,您不必担心拼写错误会导致静默失败,就像使用哈希类型解决方案一样。

【讨论】:

  • 这是解决该特定问题的好方法,但没有人建议它的原因可能与它不像 C#/Java 枚举的事实有关。
  • 这有点不完整,但可以很好地提示您如何使用动态方法实现解决方案。它与带有 FlagsAttribute 集的 C# 枚举有一些相似之处,但就像上面基于符号/常量的解决方案一样,它是众多解决方案之一。问题是最初的问题,其意图混乱(C# 和 Java 不可互换)。在 Ruby 中有很多方法可以逐项列出对象。选择正确的取决于要解决的问题。盲目地复制你不需要的功能是被误导的。正确答案必须取决于上下文。
【解决方案3】:

最惯用的方法是使用符号。例如,而不是:

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 精神。

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

【讨论】:

  • 所以Ruby的精神是:“错字会编译”
  • 流行的 Ruby 框架严重依赖运行时元编程,执行过多的加载时检查会削弱 Ruby 的大部分表达能力。为避免出现问题,大多数 Ruby 程序员都会练习测试驱动设计,这不仅会发现拼写错误,还会发现逻辑错误。
  • @yar:嗯,语言设计是一系列权衡,语言特性相互作用。如果您想要一种好的、高度动态的语言,请使用 Ruby,首先编写您的单元测试,并遵循该语言的精神。 :-) 如果这不是您想要的,那么还有许多其他优秀的语言,每种语言都有不同的权衡。
  • @emk,我同意,但我个人的问题是我在 Ruby 中感觉很舒服,但在 Ruby 中重构我感觉不舒服。现在我已经开始编写单元测试(终于),我意识到它们并不是万能药:我的猜测是 1)Ruby 代码在实践中不会经常被大规模重构 2)Ruby 不是终点就动态语言而言,这是一流的,正是因为它很难自动重构。请参阅我的问题 2317579,奇怪的是,它被 Smalltalk 人接管了。
  • 是的,但是使用这些字符串不符合 C# 语言的精神,这是一种不好的做法。
【解决方案4】:

我使用以下方法:

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

我喜欢它有以下优点:

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

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

【讨论】:

  • 我认为这是最好的答案。功能、语法和最小的代码开销最接近 Java/C#。您还可以将定义嵌套得比一层更深,并且仍然可以使用 MyClass::MY_ENUM.flatten 恢复所有值。作为旁注,我将在这里使用大写名称,因为这是 Ruby 中常量的标准。 MyClass::MyEnum 可能会被误认为是对子类的引用。
  • @Janosch,我已经更新了名称。谢谢建议
  • 我还是有点困惑,链接是 410'd(不,不是 404)。您能否举例说明如何使用此枚举?
【解决方案5】:

如果您使用的是 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

【讨论】:

  • 如您所说-如果 OP 未使用 Rails(或更准确地说,对象不是 ActiveRecord 类型),则无用。只是解释我的反对意见而已。
  • 这些不是 Ruby 中的枚举,它是数据库中枚举的 ActiveRecord 接口。不是可以应用于任何其他用例的通用解决方案。
  • 我已经在回答中提到了这一点。
  • 这是使用 Rails 的最佳答案 IFF。
  • 我不喜欢它,因为它必须存储在 Rails 数据库中(才能工作),而且它允许创建 Conversation 类的许多实例 - 我相信它必须只允许 1 个实例.
【解决方案6】:

我知道这个人已经很久没有发布这个问题了,但我也有同样的问题,而这篇帖子并没有给我答案。我想要一种简单的方法来查看数字代表什么,轻松比较,最重要的是 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

【讨论】:

    【解决方案7】:

    查看 ruby​​-enum gem,https://github.com/dblock/ruby-enum

    class Gender
      include Enum
    
      Gender.define :MALE, "male"
      Gender.define :FEMALE, "female"
    end
    
    Gender.all
    Gender::MALE
    

    【讨论】:

      【解决方案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
      => 5
      

      【讨论】:

        【解决方案9】:

        也许最好的轻量级方法是

        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
        

        【讨论】:

        • 恕我直言,这非常接近地复制了 Java 的使用和目的(类型安全),此外,根据偏好,可以像这样定义常量:class ABC; end
        【解决方案10】:

        如果您担心符号拼写错误,请确保您的代码在您使用不存在的键访问值时引发异常。您可以使用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 等,并且还使它们通过相关值进行比较。)
        • 我的意思是,在答案的开头没有这么说。
        【解决方案11】:

        这完全取决于您如何使用 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"}>
        

        【讨论】:

        • 为什么不用符号Set[:a, :b, :c]
        • 在这里使用符号要好得多,IMO。
        【解决方案12】:

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

        【讨论】:

        • 我在 Ruby 中从来不需要枚举。符号和常量是惯用的,可以解决同样的问题,不是吗?
        • 可能是查克;但是在 ruby​​ 中搜索枚举不会让你走得那么远。它将向您显示人们对直接等效项的最佳尝试的结果。这让我想知道,也许将这个概念包装在一起有什么好处。
        • @Chuck 符号和常量并不强制,例如,一个值必须是一小组值中的一个。
        【解决方案13】:

        最近我们发布了一个gem,它实现了Ruby 中的枚举。在我的post 中,您将找到问题的答案。我还在那里描述了为什么我们的实现比现有的更好(实际上,在 Ruby 中这个特性有很多实现,但作为 gems)。

        【讨论】:

        • 它允许自增值,无需明确说明。 +1
        【解决方案14】:

        另一个解决方案是使用 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'
        

        【讨论】:

          【解决方案15】:

          符号是红宝石的方式。但是,有时需要与一些 C 代码或其他东西或 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;这个例子取自现实世界的东西,网络协议文档使用了 xxx_Yyy,所以几种语言的代码使用了相同的概念,因此可以遵循规范的变化。
          • 代码打高尔夫球:server_Symb.each_with_index { |e,i| server_Enum[e] = i}。不需要i = 0
          【解决方案16】:

          我已经实现了这样的枚举

          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
          

          【讨论】:

            【解决方案17】:
            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
            

            【讨论】:

              【解决方案18】:

              这似乎有点多余,但这是我用过几次的方法,尤其是在我与 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
              

              这给了我 c# 枚举的严谨性,并且它与模型相关联。

              【讨论】:

              • 我不建议使用这种方法,因为它依赖于您手动设置值并确保您在:VAL 中获得正确的订单。最好从数组开始,使用.map.with_index 构造散列
              • 确切的一点是将自己与第三方规定的价值联系起来。这不是关于可扩展性本身,而是关于必须处理影响流程边界内可计算性的无关约束。
              • 公平点!在这种情况下,指定值绝对有意义,但我倾向于使用 .key.invert 而不是 :VAL 键(stackoverflow.com/a/10989394/2208016)进行反向查找
              • 是的,这是(回到你)一个公平的观点。我的红宝石不雅且笨重。将 def 使用 keyinvert
              【解决方案19】:

              大多数人使用符号(即:foo_bar 语法)。它们是一种独特的不透明值。符号不属于任何枚举风格的类型,因此它们并不是 C 的枚举类型的真正忠实表示,但这已经非常好。

              【讨论】:

                【解决方案20】:
                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 - 一个
                2 - b
                3 - c
                4 - d

                【讨论】:

                • to_enum 给你一个枚举 tor ,而 C#/Java 意义上的 enum 是一个枚举 tion
                【解决方案21】:

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

                这对我来说服务于枚举的目的,并保持它非常可扩展。您可以向 Enum 类添加更多方法,viola 在所有已定义的枚举中免费获取它们。例如。 get_all_names 之类的。

                【讨论】:

                  【解决方案22】:

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

                  查看更多https://github.com/alfa-jpn/inum#usage

                  【讨论】:

                    【解决方案23】:

                    另一种方法是使用带有包含名称和值的哈希的 Ruby 类,如以下RubyFleebie blog post 中所述。这使您可以轻松地在值和常量之间进行转换(尤其是当您添加一个类方法来查找给定值的名称时)。

                    【讨论】:

                      【解决方案24】:

                      我认为实现类型枚举的最佳方法是使用符号,因为它几乎表现为整数(在性能方面,object_id 用于进行比较);您无需担心索引,它们在您的代码中看起来非常整洁 xD

                      【讨论】:

                        【解决方案25】:

                        另一种模仿具有一致平等处理的枚举的方法(从 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
                        

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 1970-01-01
                          • 2023-04-03
                          • 1970-01-01
                          • 2013-03-23
                          • 2015-05-30
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多