Як реалізувати Enums в Ruby?


323

Який найкращий спосіб реалізувати ідіому ендуму в Рубі? Я шукаю те, що можу (майже) використовувати, як переписи Java / C #.


7
@auramo, гарне запитання та чудовий вибір для найкращої відповіді. Любіть це чи ненавидите його, ви не отримуєте жодної безпеки та (принаймні, у Ruby), а не друкарської безпеки. Мене було в захваті, коли я виявив переписки в C # і пізніше в Java (виберіть значення, але з цього!), Ruby не дає реального способу зробити це в будь-якому випадку взагалі.
Dan Rosenstark

2
Проблема цього питання полягає в тому, що перерахунки Java та C # - це різко різні речі. Член Java enum - це екземпляр об'єкта та сингтон. Перелік Java може мати конструктор. На відміну від цього, C # перерахунки базуються на примітивних значеннях. Яку поведінку шукає запитуючий? Хоча, ймовірно, випадок, коли потрібен випадок C #, Java чітко згадується, а не C або C ++, тому є певні сумніви. Що стосується того, щоб припустити, що в Ruby немає можливості бути "безпечним", це очевидно помилково, але вам доведеться реалізувати щось більш складне.
користувач1164178

Відповіді:


318

Два способи. Символи ( :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під час збереження в базі даних для збереження рядкової версії символу. В Рейлі, я вважаю, є кілька допоміжних методів, щоб вирішити щось із цього.
mlibby

7
Чи не буде модуль краще групувати константи - так як ви не збираєтеся робити це жодними екземплярами?
thomthom

3
Просто коментар. Рубі трохи болить з приводу іменування конвенцій, але насправді не очевидна щодо них, поки не подолаєш їх. Імена перерахунків повинні бути усіма великими літерами, а перша буква літери імені модуля має бути написана з великої літери для рубіну, щоб знати, що модуль є модулем констант.
Rokujolady

3
Не зовсім правда. Перша літера константи повинна бути написана з великої літери, але не всі букви повинні бути. Це питання переваги конвенції. Наприклад, всі імена модулів та назви класів теж є константами.
Майкл Браун

59

Я здивований, що ніхто не запропонував щось подібне (зібране з дорогоцінного каміння RAPI ):

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

1
Це трохи неповно, але служить гарним підказом щодо того, як можна реалізовувати рішення з динамічним підходом. Він схожий на перерахунок на C # з набором FlagsAttribute, але як і рішення, засновані на символах / постійних рішеннях, це одна відповідь багатьох. Проблема полягає в оригінальному питанні, яке заплутане у своєму намірі (C # і Java не взаємозамінні). Існує багато способів деталізувати об’єкти в Ruby; вибір правильного залежить від проблеми, яка вирішується. По-слов'янськи, що не потрібні функції, що вам не потрібні, неправильно керуються. Правильна відповідь повинна залежати від контексту.
користувач1164178

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)

Це трохи більш відкрито, ніж переліки, але добре поєднується з духом Рубі.

Символи також дуже добре працюють. Порівняння двох символів для рівності, наприклад, набагато швидше, ніж порівняння двох рядків.


107
Отже, дух Рубі: "Typos will will компілюється"
mxcl

82
Популярні рамки Ruby значною мірою покладаються на метапрограмування часу виконання, а виконання занадто великої перевірки часу завантаження забирає більшу частину виразної сили Ruby. Щоб уникнути проблем, більшість програмістів Ruby практикують тестово-розроблений дизайн, який знайде не лише помилки друку, але й логічні помилки.
emk

10
@yar: Ну, мовний дизайн - це низка компромісів, і мовні функції взаємодіють. Якщо ви хочете гарної, дуже динамічної мови, спробуйте Рубі, спочатку напишіть свої тести, і перейдіть з духом мови. :-) Якщо це не те, що ви шукаєте, є десятки інших чудових мов там, кожна з яких робить різні компроміси.
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. Він може мати значення будь-якого типу, а не лише символ

Символи можуть бути краще, тому що вам не доведеться писати ім'я зовнішнього класу, якщо ви використовуєте його в іншому класі ( MyClass::MY_VALUE_1)


4
Я думаю, що це найкраща відповідь. Функціональність, синтаксис та мінімальний накладний код наближаються до Java / C #. Також ви можете вкласти визначення ще глибше, ніж один рівень, і все одно відновити всі значення за допомогою MyClass :: MY_ENUM.flatten. В якості бічної примітки я б тут використовував великі імена, як це стандарт для констант у Ruby. MyClass :: MyEnum може помилитися за посилання на підклас.
Янош

@Janosch, я оновив імена. дякую за пропозицію
Олексій

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

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
Як ви вже сказали - не корисно, якщо ОП не використовує Rails (або точніше, об'єкт не типу ActiveRecord). Тільки пояснення мого рахунку є все.
Гер

2
Це не перерахунки в Ruby, це інтерфейс ActiveRecord для Enums у вашій базі даних. Не є узагальнюючим рішенням, яке можна застосувати в будь-якому іншому випадку використання.
Адам Лассек

Я це алеаді згадував у своїй відповіді.
торгова

Це найкраща відповідь IFF за допомогою Rails.
другий

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

8

Це мій підхід до перерахунків у Рубі. Я збирався коротким і солодким, не обов’язково самим 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, ...}Це визначає ключові символи. missing, somethingІ т.д., а також робить їх порівнянними з допомогою відповідних значень.)
Теему Leisti

Я маю на увазі, не кажучи цього на самому початку відповіді.
Teemu Leisti

4

Хтось пішов вперед і написав рубінову дорогоцінну каменю під назвою Renum . Він стверджує, що наближається до Java / C # подібної поведінки. Особисто я все ще вивчаю Рубі, і я був трохи шокований, коли хотів, щоб конкретний клас містив статичний перелік, можливо, хеш, що його не було легко знайти через Google.


Мені ніколи не потрібні перерахунки в Рубі. Символи та константи ідіоматичні і вирішують ті самі проблеми, чи не так?
Чак

Напевно, Чак; але гуглінг до перерахунку в рубіні не дасть вам так далеко. Він покаже вам результати найкращої спроби людей в прямому еквіваленті. Що мене здивує, можливо, є щось приємне в тому, щоб концепція була загорнута разом.
dlamblin

@Chuck Символи та константи не застосовують, наприклад, що значення повинно бути одним з невеликого набору значень.
Девід Молес

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
Набагато краща практика використовувати тут символи, ІМО.
Collin Graves

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

Символи - рубіновий шлях. Однак іноді потрібно поговорити з якимось кодом С або чимось таким чином, або з 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
@Andrew; цей приклад взятий з реального світу, а документація мережевого протоколу використовувала 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

Так, справедливий момент. Мій рубін був неелегантний і незграбний. keyinvert
Відмовився

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.

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

Іншим підходом є використання класу 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.