Как реализовать Enums в Ruby?


324

Какой лучший способ реализовать enum идиому в Ruby? Я ищу что-то, что я могу использовать (почти), как перечисления Java / C #.


7
@auramo, хороший вопрос и отличный выбор для лучшего ответа. Любите это или ненавидите, вы не получаете безопасность типов и (по крайней мере, в Ruby) защиту от опечаток. Я был в восторге, когда обнаружил перечисления в C # и позже в Java (выберите значение, но из них!), Ruby вообще не предоставляет реального способа сделать это в любом случае.
— Дэн Розенстарк

2
Проблема с этим вопросом в том, что перечисления Java и C # - это совершенно разные вещи. Член перечисления Java - это экземпляр объекта и одиночка. Перечисление Java может иметь конструктор. Напротив, перечисления C # основаны на примитивных значениях. Какое поведение ищет спрашивающий? Хотя вполне вероятно, что дело C # является желательным, Java явно упоминается, а не C или C ++, поэтому есть некоторые сомнения. Что касается предположения, что в Ruby нет способа быть «безопасным», то это явно неверно, но вам нужно реализовать что-то более сложное.
— user1164178

Ответы:


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
Что, если эти перечисления слишком сохранены в базе данных? Будет ли работать обозначение символа? Я сомневаюсь ...
— Phương Nguyễn

Я бы использовал подход констант, если бы я сохранял в базу данных. Конечно, тогда вы должны выполнить какой-то поиск при извлечении данных из БД. Вы также можете использовать что-то вроде :minnesota.to_sпри сохранении в базе данных, чтобы сохранить строковую версию символа. Rails, я считаю, имеет несколько вспомогательных методов для решения некоторых из этих проблем.
— mlibby

7
Разве модуль не будет лучше группировать константы - поскольку вы не собираетесь делать его экземпляры?
— Thomthom

3
Просто комментарий. Руби немного беспокоит соглашения об именах, но они не совсем очевидны, пока вы не разберетесь с ними. Имена перечислений должны быть заглавными, а первая буква имени модуля должна быть написана заглавными буквами, чтобы 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 широко использует).

Кроме того, вам не нужно беспокоиться о том, что опечатки могут вызывать сбои в режиме без вывода сообщений, как если бы вы использовали решение типа hash.


1
Это отличный способ решить эту конкретную проблему, но причина, по которой никто не предположил, что это, вероятно, связана с тем фактом, что он не очень похож на перечисления C # / Java.
— mlibby

1
Это немного неполно, но служит хорошим намеком на то, как вы можете реализовать решения с помощью динамического подхода. Он имеет некоторое сходство с перечислением C # с набором FlagsAttribute, но, как и приведенные выше решения на основе символов / констант, это один из многих ответов. Проблема в исходном вопросе, который запутан в своем намерении (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 используют тестовый дизайн, который находит не только опечатки, но и логические ошибки.
— ЭМК

10
@yar: Ну, языковой дизайн - это серия компромиссов, и языковые особенности взаимодействуют. Если вам нужен хороший, высокодинамичный язык, используйте Ruby, сначала напишите свои модульные тесты и следуйте духу языка. :-) Если это не то, что вы ищете, есть десятки других отличных языков, каждый из которых имеет свой компромисс.
— Emk

10
@emk, я согласен, но моя личная проблема в том, что я чувствую себя вполне комфортно в Ruby, но я не чувствую себя комфортно в рефакторинге в Ruby. И теперь, когда я начал писать модульные тесты (наконец-то), я понял, что они не панацея: я думаю, 1) что код Ruby не подвергается массовому рефакторингу, что часто, на практике, и 2) Ruby - не конец - с точки зрения динамических языков, именно потому, что трудно автоматически выполнить рефакторинг. См. Мой вопрос 2317579, который, как ни странно, был задан людьми из Smalltalk.
— Дэн Розенстарк

4
Да, но использование этих строк не было бы в духе языка C #, это просто плохая практика.
— Эд С.

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, я обновил имена. спасибо за предложение
— Алексей

Я все еще немного растерялся, и ссылка 410'd (нет, не 404). Не могли бы вы привести примеры того, как будет использоваться этот enum?
— Шелваку

17

Если вы используете Rails 4.2 или выше, вы можете использовать перечисления Rails.

Rails теперь имеет по умолчанию перечисления без необходимости включать какие-либо драгоценные камни.

Это очень похоже (и больше с функциями) на перечисления 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 для перечислений в вашей базе данных. Не обобщаемое решение, которое может быть применено в любом другом случае использования.
— Адам Лассек

Я уже упоминал об этом в своем ответе.
— Vedant

Это лучший ответ IFF с использованием Rails.
— Второе

Мне это не нравится, потому что он должен храниться в базе данных Rails (для работы) и потому что он позволяет создавать много экземпляров Conversationкласса - я считаю, что он должен разрешать только 1 экземпляр.
— программа

8

Это мой подход к перечислениям в Ruby. Я собирался кратко и сладко, не обязательно самый C-like. Есть предположения?

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, ...}Это определяет ключевые символы. missing, somethingИ т.д., а также делает их сопоставимыми с помощью соответствующих значений.)
— Теему Лейсти

Я имею в виду, не говоря об этом в самом начале ответа.
— Теему Лейсти,

4

Кто-то пошел дальше и написал рубиновый камень под названием Renum . Он утверждает, что получил наиболее близкое поведение Java / C #. Лично я все еще изучаю Ruby, и я был немного шокирован, когда мне хотелось, чтобы конкретный класс содержал статическое перечисление, возможно, хеш, который не совсем легко найти через Google.


Я никогда не нуждался в перечислении в Ruby. Символы и константы идиоматичны и решают те же проблемы, не так ли?
— Чак

Вероятно, Чак; но поиск глагола в ruby ​​не поможет вам так далеко. Он покажет вам результаты для лучшей попытки людей на прямой эквивалент. Что заставляет меня задуматься, может быть, есть что-то хорошее в объединении концепции.
— Дламблин

@ Чак Символы и константы не обеспечивают, например, что значение должно быть одним из небольшого набора значений.
— Дэвид Моулз

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
Намного лучше использовать здесь символы, ИМО.
— Коллин Грейвс

3

Недавно мы выпустили гем, который реализует Enums в Ruby . В моем посте вы найдете ответы на свои вопросы. Также я описал, почему наша реализация лучше существующих (на самом деле в Ruby есть много реализаций этой возможности, но в виде гемов).


Это позволяет самостоятельно увеличивать значения, не указывая их явно. +1
— менее

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) по определенной причине? Если нет особой причины, то идиоматично для переменных быть snake_case_with_all_lower_case, а для символов быть :lower_case.
— Эндрю Гримм

1
@Андрей; этот пример был взят из реальной жизни, а в документации сетевого протокола использовался xxx_Yyy, поэтому код на нескольких языках использовал одну и ту же концепцию, чтобы можно было следовать изменениям спецификации.
— Jonke

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 # enum, и это связано с моделью.


Я бы не советовал этот подход, потому что он основан на ручной настройке значений и обеспечении правильного оформления заказа :VAL. Было бы лучше начать с массива и построить хеш с помощью.map.with_index
— DaveMongoose

1
Точный смысл - привязать себя к значению, которое диктуют третьи стороны. Речь идет не о расширяемости как таковой, а о необходимости иметь дело с посторонними ограничениями, которые влияют на вычислимость в рамках вашего процесса.
— JJK

Честная оценка! В этом случае определенно имеет смысл указать значения, но я бы предпочел выполнить обратный поиск с ключом .keyили .invertвместо него :VAL( stackoverflow.com/a/10989394/2208016 )
— DaveMongoose,

Да, это (назад на тебя) справедливая точка зрения. Мой рубин был не элегантен и громоздок. Будет использовать def keyилиinvert
— JJK

1

Большинство людей используют символы (это :foo_barсинтаксис). Это своего рода уникальные непрозрачные значения. Символы не принадлежат ни к какому типу enum-стиля, так что они на самом деле не являются точным представлением enum-типа 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

Иногда все, что мне нужно, - это получить значение 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 и делает его очень расширяемым. Вы можете добавить больше методов в класс Enum, и альты получат их бесплатно во всех определенных перечислениях. например. get_all_names и тому подобное.


0

Другой подход заключается в использовании класса Ruby с хешем, содержащим имена и значения, как описано в следующем сообщении в блоге RubyFleebie . Это позволяет вам легко преобразовывать значения и константы (особенно если вы добавляете метод класса для поиска имени для данного значения).


0

Я думаю, что лучший способ реализовать перечисления, подобные типам, - это использовать символы, так как они ведут себя как целые числа (когда дело доходит до производительности, для сравнения используется object_id); вам не нужно беспокоиться об индексации, и они выглядят очень аккуратно в вашем коде xD


0

Другой способ подражать перечислению с последовательной обработкой равенства (бесстыдно принятый от Дейва Томаса). Позволяет открывать перечисления (как символы) и закрытые (предопределенные) перечисления.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.