Як реалізувати 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.