Ruby - элегантно преобразовать переменную в массив, если это еще не массив

120

Учитывая массив, единственный элемент или nil, получить массив - последние два являются массивом с одним элементом и пустым массивом соответственно.

Я ошибочно подумал, что Ruby будет работать следующим образом:

[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))

Проблема в том, что это немного беспорядок. Есть ли элегантный способ сделать это? (Я был бы удивлен, если бы это был способ решения этой проблемы в стиле Ruby)


Какие приложения у этого есть? Зачем вообще преобразовывать в массив?

В ActiveRecord Rails вызов, скажем, user.postsбудет либо возвращать массив сообщений, либо одну запись, либо ноль. При написании методов, которые работают с результатами этого, проще всего предположить, что метод будет принимать массив, который может иметь ноль, один или несколько элементов. Пример метода:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}
xxjjnn
источник
2
user.postsникогда не должен возвращать ни одного сообщения. По крайней мере, я этого не видел.
Серджио Тюленцев
1
я думаю , что в ваших первых двух блоков кода вы имеете в виду , ==а не =, не так ли?
Патрик Оссити
2
Возможное повторение: stackoverflow.com/questions/385912/ruby-object-to-a-replacement
Дэн Гран
3
Кстати, [1,2,3].to_aвовсе не вернется [[1,2,3]]! Он возвращается [1,2,3].
Патрик Оссити,
Спасибо, весло,
обновлю

Ответы:

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
Вы можете объяснить этот синтаксис? За многие годы использования Ruby я ни разу не встречал такого типа вызова. Что делают скобки в имени класса? Я не могу найти это в документации.
mastaBlasta
1
@mastaBlasta Array (arg) пытается создать новый массив, вызывая to_ary, а затем to_a для аргумента. Это задокументировано в официальных документах по Ruby. Я узнал об этом из книги Авди «Уверенный Рубин».
mambo
2
@mambo В какой-то момент после того, как я разместил свой вопрос, я нашел ответ. Сложность заключалась в том, что он не имеет ничего общего с классом Array, а является методом модуля ядра. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta 02
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, вы можете определить свой собственный метод, аналогичный исходному тексту 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
elado
источник
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: Array (): 7936825.7 i / s. Array.wrap: 4199036,2 i / s - в 1,89 раза медленнее. wrap: 644030,4 i / s - в 12,32 раза медленнее
Wasif Hossain
19

Array(whatever) должен сделать трюк

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]
Бенджамин Грюнбаум
источник
14
не будет работать для Hash. Массив ({a: 1, b: 2}) будет [[: a, 1], [: b, 2]]
davispuh
13

ActiveSupport (Rails)

У ActiveSupport есть для этого довольно хороший метод. Он загружен с Rails, так что лучший способ сделать это:

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

Сплат (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для элементов 1M (ruby 2.2.3).
denis.peplin
@ denis.peplin похоже, что у вас ошибка StackOverflow: D - честно говоря, я не уверен, что произошло. Сожалею.
Бен Обин
Недавно я пробовал использовать Hash#values_at1M аргументов (используя 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

все наследует Object, поэтому to_a теперь будет определено для всего, что находится под солнцем

runub
источник
3
кощунственная обезьяна залатывала! Покайтесь!
xxjjnn
1

Я просмотрел все ответы и в основном не работает в ruby ​​2+

Но у elado есть самое элегантное решение, т.е.

С 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}]

К сожалению, это также не работает для Ruby 2+, так как вы получите сообщение об ошибке

undefined method `wrap' for Array:Class

Итак, чтобы исправить это, вам необходимо потребовать.

требовать active_support / deprecation

требуется 'active_support / core_ext / array / wrap'

Вредоносное ПО Skiddie
источник
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 07