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

324

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

Auramo
источник
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
mlibby
источник
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
Vedant
источник
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
johnnypez
источник
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.

dlamblin
источник
Я никогда не нуждался в перечислении в 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"}>
mislav
источник
9
Почему бы не использовать символы Set[:a, :b, :c]?
Дэн Розенстарк
2
Намного лучше использовать здесь символы, ИМО.
Коллин Грейвс
3

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

ka8725
источник
Это позволяет самостоятельно увеличивать значения, не указывая их явно. +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

Jonke
источник
Вы прописываете второе слово в переменных (например 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
Masuschi
источник
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, и это связано с моделью.

JJK
источник
Я бы не советовал этот подход, потому что он основан на ручной настройке значений и обеспечении правильного оформления заказа :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 и тому подобное.

dark_src
источник
0

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

Филипп Монне
источник
0

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

goreorto
источник
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
Даниэль Даблдей
источник