Ruby - елегантно перетворити змінну в масив, якщо вже не масив


120

Давши масив, один елемент або нуль, отримайте масив - останні два являють собою масив одного елемента та порожній масив відповідно.

Я помилково вважав, що Рубі працює так:

[1,2,3].to_a  #= [1,2,3]     # Already an array, so no change
1.to_a        #= [1]         # Creates an array and adds element
nil.to_a      #= []          # Creates empty array

Але те, що ви насправді отримуєте:

[1,2,3].to_a  #= [1,2,3]         # Hooray
1.to_a        #= NoMethodError   # Do not want
nil.to_a      #= []              # Hooray

Тому для вирішення цього питання мені або потрібно використовувати інший метод, або я міг би програму мета, змінивши метод to_a всіх класів, які я маю намір використовувати - що для мене не є варіантом.

Отже, це метод:

result = nums.class == "Array".constantize ? nums : (nums.class == "NilClass".constantize ? [] : ([]<<nums))

Проблема в тому, що це трохи безлад. Чи є елегантний спосіб зробити це? (Я був би вражений, якщо це спосіб вирішити цю проблему)


Які програми це має? Чому навіть перетворити на масив?

У Rails 'ActiveRecord, зателефонувавши скажімо, user.postsповерне масив повідомлень, одиночну публікацію, або нульову. При написанні методів, які працюють на результатах цього, найпростіше припустити, що метод візьме масив, який може мати нуль, один або багато елементів. Приклад способу:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}

2
user.postsніколи не повинні повертати жодної посади. Принаймні, я цього ніколи не бачив.
Серхіо Туленцев

1
я думаю, що у перших двох блоках коду ви маєте на увазі ==замість цього =, чи не так?
Патрік Осіті


3
До речі, [1,2,3].to_aзовсім НЕ повернеться [[1,2,3]]! Це повертається [1,2,3].
Патрік Осіті

Дякую, весло, оновимо питання ... facepalms at self
xxjjnn

Відповіді:


153

[*foo]або Array(foo)працюватиме більшу частину часу, але для деяких випадків, як хеш, це заплутує його.

Array([1, 2, 3])    # => [1, 2, 3]
Array(1)            # => [1]
Array(nil)          # => []
Array({a: 1, b: 2}) # => [[:a, 1], [:b, 2]]

[*[1, 2, 3]]    # => [1, 2, 3]
[*1]            # => [1]
[*nil]          # => []
[*{a: 1, b: 2}] # => [[:a, 1], [:b, 2]]

Єдиний спосіб, що я можу думати, що це працює навіть для хешу, - це визначити метод.

class Object; def ensure_array; [self] end end
class Array; def ensure_array; to_a end end
class NilClass; def ensure_array; to_a end end

[1, 2, 3].ensure_array    # => [1, 2, 3]
1.ensure_array            # => [1]
nil.ensure_array          # => []
{a: 1, b: 2}.ensure_array # => [{a: 1, b: 2}]

2
замість того ensure_array, щоб продовжитиto_a
Дан Гран

9
@screenmutt Це вплине на методи, які покладаються на оригінальне використання to_a. Наприклад, {a: 1, b: 2}.each ...працювали б інакше.
sawa

1
Чи можете ви пояснити цей синтаксис? За багато років Рубі я ніколи не стикався з таким видом виклику. Що роблять дужки в імені класу? Я не можу знайти це в документах.
mastaBlasta

1
@mastaBlasta Array (arg) намагається створити новий масив, викликавши to_ary, а потім to_a в аргументі. Це зафіксовано в офіційних рубінових документах. Я дізнався про це з книги Авді "Впевнено Рубі".
мамбо

2
@mambo У якийсь момент після того, як я опублікував своє запитання, я знайшов відповідь. Важкою частиною було те, що він не має нічого спільного з класом Array, але це метод на модулі Kernel. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta

119

За допомогою ActiveSupport (Rails): Array.wrap

Array.wrap([1, 2, 3])     # => [1, 2, 3]
Array.wrap(1)             # => [1]
Array.wrap(nil)           # => []
Array.wrap({a: 1, b: 2})  # => [{:a=>1, :b=>2}]

Якщо ви не використовуєте Rails, ви можете визначити свій власний метод, подібний до джерела рейки .

class Array
  def self.wrap(object)
    if object.nil?
      []
    elsif object.respond_to?(:to_ary)
      object.to_ary || [object]
    else
      [object]
    end
  end
end

12
class Array; singleton_class.send(:alias_method, :hug, :wrap); endдля додаткової милості.
rthbound

21

Найпростішим рішенням є використання [foo].flatten(1). На відміну від інших запропонованих рішень, він буде добре працювати (вкладені) масиви, хеші та nil:

def wrap(foo)
  [foo].flatten(1)
end

wrap([1,2,3])         #= [1,2,3]
wrap([[1,2],[3,4]])   #= [[1,2],[3,4]]
wrap(1)               #= [1]
wrap(nil)             #= [nil]
wrap({key: 'value'})  #= [{key: 'value'}]

на жаль, ця проблема має серйозні результати в порівнянні з іншими підходами. Kernel#Arrayтобто Array()найшвидший з них. Порівняння Ruby 2.5.1: масив (): 7936825,7 i / s. Array.wrap: 4199036.2 i / s - 1,89x повільніше. обернути: 644030,4 I / S - 12.32x повільніше
Wasif Хоссаін


13

ActiveSupport (рейки)

ActiveSupport має досить приємний метод для цього. Він завантажений Rails, тому викликаючи найкращий спосіб зробити це:

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> nil

Splat (Ruby 1.9+)

Оператор splat ( *) знімає масив, якщо він може:

*[1,2,3] #=> 1, 2, 3 (notice how this DOES not have braces)

Звичайно, без масиву це робить дивні речі, а об’єкти, які ви "плескаєте", потрібно помістити в масиви. Це дещо дивно, але це означає:

[*[1,2,3]] #=> [1, 2, 3]
[*5] #=> [5]
[*nil] #=> []
[*{meh: "meh"}] #=> [[:meh, "meh"], [:meh2, "lol"]]

Якщо у вас немає ActiveSupport, ви можете визначити метод:

class Array
    def self.wrap(object)
        [*object]
    end
end

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> nil

Хоча, якщо ви плануєте мати великі масиви та менше речі, що не мають масиву, можливо, ви захочете змінити - вищевказаний метод повільний з великими масивами, і навіть може спричинити переповнення вашого стека (omg so meta). У будь-якому випадку ви можете зробити це замість цього:

class Array
    def self.wrap(object)
        object.is_a? Array ? object : [*object]
    end
end

Array.wrap([1, 2, 3]) #=> [1, 2, 3]
Array.wrap(nil) #=> [nil]

У мене також є деякі орієнтири з оператором teneray і без нього.


Не працюватимуть для великих масивів. SystemStackError: stack level too deepдля елементів 1М (рубін 2.2.3).
denis.peplin

@ denis.peplin, здається, у вас виникла помилка StackOverflow: D - чесно кажучи, я не впевнений, що сталося. Вибачте.
Бен Оббін

Нещодавно я намагався Hash#values_atз аргументами 1M (використовуючи splat), і він видає ту ж помилку.
denis.peplin

@ denis.peplin Це працює з object.is_a? Array ? object : [*object]?
Бен Оббін

1
Array.wrap(nil)повернення []не nil: /
Aeramor

7

Як щодо

[].push(anything).flatten

2
Так, я думаю, що в моєму випадку я використовував [що-небудь] .flatten ... але для загального випадку це також згладить будь-які вкладені структури масиву
xxjjnn

1
[].push(anything).flatten(1)працювали б! Це не згладжує вкладені масиви!
xxjjnn

2

Ризикуючи сказати очевидним, і знаючи, що це не найсмачніший синтаксичний цукор, який коли-небудь бачили на планеті та навколишніх територіях, цей код, здається, робить саме те, що ви описуєте:

foo = foo.is_a?(Array) ? foo : foo.nil? ? [] : [foo]

1

ви можете перезаписати метод масиву Object

class Object
    def to_a
        [self]
    end
end

все успадковує Об'єкт, тому тепер до_а буде визначено все під сонцем


3
блюзнірська лапка мавп! Покаяйтесь!
xxjjnn

1

Я переглянув усі відповіді і в основному не працює в рубіні 2+

Але еладо має найелегантніше рішення, тобто

За допомогою ActiveSupport (Rails): Array.wrap

Array.wrap ([1, 2, 3]) # => [1, 2, 3]

Array.wrap (1) # => [1]

Array.wrap (нуль) # => []

Array.wrap ({a: 1, b: 2}) # => [{: a => 1,: b => 2}]

На жаль, але це також не працює для рубіну 2+, оскільки ви отримаєте помилку

undefined method `wrap' for Array:Class

Отже, щоб виправити те, що вам потрібно вимагати.

вимагати "active_support / deprecation"

вимагати "active_support / core_ext / array / wrap"


0

Оскільки метод #to_aвже існує для двох основних проблемних класів ( Nilі Hash), просто визначте метод для решти, розширивши Object:

class Object
    def to_a
        [self]
    end
end

і тоді ви можете легко викликати цей метод на будь-якому об'єкті:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []

5
Я дійсно думаю, що мавпи, що латкує основний клас Ruby, особливо об'єкта, слід уникати. Я дам ActiveSupport пропуск, хоча так вважаю мене лицеміром. Вищевикладені рішення @sawa набагато життєздатніші за це.
pho3nixf1re
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.