RubyでEnumを実装する方法は?


324

Rubyで列挙型イディオムを実装する最良の方法は何ですか?私はJava / C#列挙型のように(ほとんど)使用できるものを探しています。


7
@auramo、良い質問、そして最良の答えのための素晴らしい選択。好きでも嫌いでも、タイプセーフは得られず(少なくともRubyでは)タイプセーフは得られません。C#と後でJavaで列挙型を発見したときは感激しました(値を選択しますが、これらから!)。Rubyは、どのような場合でもそれを行う実際の方法を提供していません。
Dan Rosenstark、2010年

2
この質問の問題は、Java列挙型とC#列挙型が劇的に異なることです。Java列挙型メンバーは、オブジェクトインスタンスとシングルトンです。Java enumはコンストラクタを持つことができます。対照的に、C#列挙型はプリミティブ値に基づいています。質問者が探している行動はどれですか?C#のケースが必要なケースのようですが、CまたはC ++ではなくJavaが明示的に言及されているため、疑問がいくつかあります。Rubyで「安全」にする方法がないことを示唆することについては、それは透過的に誤りですが、より洗練されたものを実装する必要があります。
user1164178 2015

回答:


319

二つの方法。記号(: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
これらの列挙型がデータベースに格納されすぎるとどうなりますか?記号表記は機能しますか?私は...疑う
フオングエン

データベースに保存する場合は、定数アプローチを使用します。もちろん、DBからデータを取り出すときに、なんらかのルックアップを行う必要があります。また:minnesota.to_s、データベースに保存するときに、シンボルの文字列バージョンを保存するようなものを使用することもできます。Railsには、これを処理するためのヘルパーメソッドがいくつかあると思います。
mlibby

7
モジュールは定数をグループ化する方が良いでしょう-あなたはそれのインスタンスを作るつもりはないので?
thomthom 2012年

3
ただのコメント。Rubyは命名規則については少し苦労しますが、慣れるまで実際にはわかりません。enumの名前はすべて大文字でなければならず、モジュールが定数のモジュールであることをrubyが認識できるように、モジュール名の最初の文字を大文字にする必要があります。
Rokujolady 2013

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スタイルの定数/列挙型(RAPIが広範囲に使用するFFIを使用する場合と同様)を処理するときにうまく機能します。

また、ハッシュタイプのソリューションを使用する場合のように、タイプミスがサイレント障害を引き起こすことを心配する必要はありません。


1
これはその特定の問題を解決する優れた方法ですが、だれもそれをC#/ Java列挙型とあまり似ていないという事実に関係があると誰もが示唆していない理由です。
mlibby 2012

1
これは少し不完全ですが、動的なアプローチでソリューションを実装する方法に関するヒントとして役立ちます。FlagsAttributeが設定されたC#列挙型に似ていますが、上記のシンボル/定数ベースのソリューションと同様に、多くの回答の1つです。問題は元の質問であり、その意図は混乱しています(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の精神によく適合します。

シンボルも非常によく機能します。たとえば、2つのシンボルが等しいかどうかを比較すると、2つの文字列を比較するよりもはるかに高速です。


107
したがって、Rubyの精神は次のとおりです。「
Typos

82
人気のあるRubyフレームワークはランタイムメタプログラミングに大きく依存しており、あまりにも多くのロード時間チェックを実行すると、Rubyの表現力のほとんどが失われます。問題を回避するために、ほとんどのRubyプログラマーはタイプミスだけでなく論理エラーも見つけるテスト駆動設計を実践しています。
2009

10
@yar:まあ、言語設計は一連のトレードオフであり、言語機能は相互に作用します。優れた非常に動的な言語が必要な場合は、Rubyを使用して、最初にユニットテストを記述し、言語の精神を使用してください。:-)それがあなたが探しているものではない場合、他にも数十の優れた言語があり、それぞれが異なるトレードオフになります。
EMK

10
@emk、私は同意しますが、私の個人的な問題は、Rubyでかなり快適であるということですが、Rubyでのリファクタリングは快適ではありません。そして、私は(ついに)単体テストを書き始めたので、それらが万能薬ではないことに気づきました。私の推測は、1)Rubyコードが実際にはそれほど頻繁にリファクタリングされないこと、2)Rubyが終わりではないことです動的言語の観点から言えば、自動的にリファクタリングするのが難しいからです。Smalltalkの人々に奇妙に引き継がれた私の質問2317579を参照してください。
Dan Rosenstark、2010年

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. シンボルだけでなく、あらゆるタイプの値を持つことができます

別のクラスで使用している場合は、外部クラスの名前を記述する必要がないため、シンボルの方がよい場合があります(MyClass::MY_VALUE_1


4
これが最良の答えだと思います。機能、構文、最小限のコードオーバーヘッドは、Java / C#に最も近くなります。また、定義を1レベルよりも深くネストし、MyClass :: MY_ENUM.flattenですべての値を回復することもできます。補足として、Rubyの定数の標準と同じように、ここでは大文字の名前を使用します。MyClass :: MyEnumは、サブクラスへの参照と間違われる可能性があります。
Janosch、2015

@Janosch、名前を更新しました。提案をありがとう
アレクセイ

私はまだ少し混乱しており、リンクは410です(いいえ、404ではありません)。この列挙型がどのように使用されるかについて例を挙げていただけますか?
シェルバク

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でない場合)は役に立ちません。私の反対票を説明するだけです。
Ger

2
これらはRubyの列挙型ではなく、データベースの列挙型へのActiveRecordインターフェースです。他のユースケースに適用できる一般化可能なソリューションではありません。
Adam Lassek、2016年

私は私の答えでそれを述べました。
ヴェダント2016年

これは、Railsを使用した最良の回答IFFです。
theUtherSide 2016年

Railsデータベースに(動作するように)保存する必要があり、Conversationクラスのインスタンスを多数作成できるので、私はそれが好きではありません。許可するインスタンスは1つだけにする必要があると思います。
prograils 2017年

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 2013

4

誰かが先に進んで、レナムと呼ばれるルビーの宝石を書いた。最も近いJava / C#のような動作を取得すると主張しています。個人的にはまだRubyを学習していますが、特定のクラスに静的列挙型(おそらくハッシュ)を含めたいと思ったときに少しショックを受けました。


Rubyにenumが必要なことは一度もありません。シンボルと定数は慣用的であり、同じ問題を解決しませんか?
チャック

おそらくチャック。しかし、ルビーの列挙型をググリングしても、それほど遠くまでは行きません。これは、直接同等のものでの人々の最善の試みの結果を示します。それで、コンセプトをまとめることに何かいいところがあるのではないかと思います。
dlamblin 2009年

@Chuckシンボルと定数は、たとえば、値が小さな値のセットの1つでなければならないことを強制しません。
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]ですか?
Dan Rosenstark、2010年

2
ここで、IMOのシンボルを使用する方がはるかに良い方法です。
Collin Graves、

3

最近、RubyでEnumsを実装するgemをリリースしました。私の投稿では、あなたの質問に対する答えを見つけるでしょう。また、実装が既存の実装よりも優れている理由についても説明しました(実際、Rubyにはまだこの機能の実装がgemとして数多くあります)。


明示的に指定しなくても、自己増分値を使用できます。+1
2016

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と通信する必要があります。


#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特定の理由で、変数(たとえば)の2番目の単語を大文字にしていますか?特別な理由がない限り、変数がsnake_case_with_all_lower_caseであり、記号がであるのは慣用的です:lower_case
Andrew Grimm

1
@Andrew; この例は実際のものから取られたものであり、ネットワークプロトコルのドキュメントではxxx_Yyyを使用しているため、いくつかの言語のコードは同じ概念を使用しているため、仕様の変更に従うことができます。
Jonke、2011年

1
コードゴルフ:server_Symb.each_with_index { |e,i| server_Enum[e] = i}。必要はありませんi = 0
Andrew Grimm

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#enumの厳密さが得られ、モデルに関連付けられます。


このアプローチは、手動で値を設定し、で正しい順序になっていることを確認する必要があるため、お勧めしません:VAL。配列から始めて、次を使用してハッシュを構築する方がよいでしょう.map.with_index
DaveMongoose

1
正確なポイントは、第三者によって指示された値に自分を結びつけることです。それ自体は拡張性ではなく、プロセスの境界内での計算可能性に影響を与える無関係な制約に対処する必要があります。
jjk

フェアポイント!その場合、それは間違いなく値を指定することは理にかなっていますが、私は逆引き参照を実行するために傾斜させることだろう.keyか、.invertというよりも、:VALキー(stackoverflow.com/a/10989394/2208016
DaveMongoose

ええ、それは(あなたに戻って)公正なポイントです。私のルビーは上品で扱いにくいものでした。def use 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を一方、enumC#で/ 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

enumの値をフェッチしてその名前を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など。


0

別のアプローチは、次のRubyFleebieブログ投稿で説明されているように、名前と値を含むハッシュを持つRubyクラスを使用することです。これにより、値と定数の間で簡単に変換できます(特に、クラスメソッドを追加して、指定された値の名前を検索する場合)。


0

型のような列挙を実装する最善の方法は、シンボルを使用することだと思います。インデックス作成について心配する必要はありません。コード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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.