Ruby - converte elegantemente una variabile in un array se non già un array


120

Dato un array, un singolo elemento o nil, ottieni un array: gli ultimi due sono rispettivamente un array a singolo elemento e un array vuoto.

Ho erroneamente pensato che Ruby avrebbe funzionato in questo modo:

[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

Ma quello che ottieni davvero è:

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

Quindi, per risolvere questo problema, ho bisogno di utilizzare un altro metodo, oppure posso meta-programmare modificando il metodo to_a di tutte le classi che intendo utilizzare, il che non è un'opzione per me.

Quindi un metodo è:

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

Il problema è che è un po 'un casino. C'è un modo elegante per farlo? (Sarei stupito se questo fosse il modo rubino per risolvere questo problema)


Quali applicazioni ha questo? Perché anche convertire in un array?

Nell'ActiveRecord di Rails, la chiamata, ad esempio, user.postsrestituirà un array di post, un singolo post o nil. Quando si scrivono metodi che funzionano sui risultati di ciò, è più facile presumere che il metodo richieda un array, che può avere zero, uno o molti elementi. Metodo di esempio:

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

2
user.postsnon dovrebbe mai restituire un singolo messaggio. Almeno, non l'ho mai visto.
Sergio Tulentsev

1
Penso che nei tuoi primi due blocchi di codice intendi ==invece di =, giusto?
Patrick Oscity


3
Btw, [1,2,3].to_anon non tornare [[1,2,3]]! Ritorna [1,2,3].
Patrick Oscity

Grazie paddle, aggiornerò la domanda ... facepalms su self
xxjjnn

Risposte:


153

[*foo]o Array(foo)funzionerà per la maggior parte del tempo, ma in alcuni casi, come un hash, lo rovina.

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

L'unico modo in cui posso pensare che funzioni anche per un hash è definire un metodo.

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
invece di ensure_arrayestendereto_a
Dan Grahn

9
@screenmutt Ciò influenzerebbe i metodi che si basano sull'uso originale di to_a. Ad esempio, {a: 1, b: 2}.each ...funzionerebbe in modo diverso.
sawa

1
Puoi spiegare questa sintassi? In molti anni di Ruby non mi ero mai imbattuto in questo tipo di invocazione. Cosa fanno le parentesi sul nome di una classe? Non riesco a trovarlo nei documenti.
mastaBlasta

1
@mastaBlasta Array (arg) cerca di creare un nuovo array chiamando to_ary, quindi to_a sull'argomento. Ciò è documentato nei documenti ufficiali di Ruby. L'ho imparato dal libro "Confident Ruby" di Avdi.
mambo

2
@mambo Ad un certo punto dopo aver pubblicato la mia domanda ho trovato la risposta. La parte difficile è che non ha nulla a che fare con la classe Array ma è un metodo sul modulo Kernel. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta

119

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

Se non stai usando Rails, puoi definire il tuo metodo simile al sorgente di 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); endper una dolcezza extra.
andata

21

La soluzione più semplice è usare [foo].flatten(1). A differenza di altre soluzioni proposte, funzionerà bene per array (annidati), hash e 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'}]

purtroppo questo ha un serio problema di prestazioni rispetto ad altri approcci. Kernel#Arraycioè Array()è il più veloce di tutti. Confronto Ruby 2.5.1: Array (): 7936825.7 i / s. Array.wrap: 4199036,2 i / s - 1,89 volte più lento. wrap: 644030,4 i / s - 12,32 volte più lento
Wasif Hossain

19

Array(whatever) dovrebbe fare il trucco

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]

14
non funzionerà per Hash. La matrice ({a: 1, b: 2}) sarà [[: a, 1], [: b, 2]]
davispuh

13

ActiveSupport (Rails)

ActiveSupport ha un metodo molto carino per questo. È caricato con Rails, quindi con aria di sfida il modo più carino per farlo:

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

Splat (Ruby 1.9+)

L'operatore splat ( *) rimuove un array, se può:

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

Ovviamente, senza un array, fa cose strane e gli oggetti che "schizzi" devono essere messi in array. È un po 'strano, ma significa:

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

Se non hai ActiveSupport, puoi definire il metodo:

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

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

Sebbene, se prevedi di avere array di grandi dimensioni e meno cose non di array, potresti volerlo cambiare: il metodo sopra è lento con array di grandi dimensioni e può persino causare l'overflow del tuo Stack (omg so meta). Ad ogni modo, potresti voler fare questo invece:

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]

Ho anche alcuni benchmark con e senza l'operatore teneray.


Non funzionerà per grandi array. SystemStackError: stack level too deepper elementi 1M (rubino 2.2.3).
denis.peplin

@ denis.peplin sembra che tu abbia ricevuto un errore StackOverflow: D - onestamente, non sono sicuro di cosa sia successo. Scusate.
Ben Aubin

Recentemente ho provato Hash#values_atcon 1M di argomenti (usando splat) e genera lo stesso errore.
denis.peplin

@ denis.peplin Funziona con object.is_a? Array ? object : [*object]?
Ben Aubin

1
Array.wrap(nil)[]non ritorna nil: /
Aeramor

7

Che ne dite di

[].push(anything).flatten

2
Sì, penso di aver finito per usare [qualsiasi] .flatten nel mio caso ... ma per il caso generale questo appiattirà anche qualsiasi struttura di array annidata
xxjjnn

1
[].push(anything).flatten(1)funzionerebbe! Non appiattisce gli array annidati!
xxjjnn

2

Con il rischio di affermare l'ovvio e sapendo che questo non è lo zucchero sintattico più gustoso mai visto sul pianeta e nelle aree circostanti, questo codice sembra fare esattamente quello che descrivi:

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

1

è possibile sovrascrivere il metodo array di Object

class Object
    def to_a
        [self]
    end
end

tutto eredita Object, quindi to_a sarà ora definito per tutto ciò che è sotto il sole


3
rattoppo blasfemo di scimmia! Pentitevi!
xxjjnn

1

Ho esaminato tutte le risposte e per lo più non funziona in Ruby 2+

Ma elado ha la soluzione più elegante, ovvero

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

Purtroppo, ma anche questo non funziona per Ruby 2+ in quanto riceverai un errore

undefined method `wrap' for Array:Class

Quindi, per risolvere questo problema, devi richiedere.

richiedono "supporto_attivo / deprecazione"

richiede "supporto_attivo / testo_protezione / array / wrap"


0

Poiché il metodo #to_aesiste già per le due principali classi problematiche ( Nile Hash), è sufficiente definire un metodo per il resto estendendo Object:

class Object
    def to_a
        [self]
    end
end

e quindi puoi facilmente chiamare quel metodo su qualsiasi oggetto:

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

5
Penso davvero che l'applicazione di patch di scimmia a una classe principale di Ruby, in particolare un oggetto, sia da evitare. Darò un passaggio ad ActiveSupport, quindi considerami un ipocrita. Le soluzioni sopra di @sawa sono molto più praticabili di questa.
pho3nixf1re
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.