Ruby - konvertiert Variablen elegant in ein Array, wenn nicht bereits ein Array

119

Bei einem gegebenen Array erhält ein einzelnes Element oder Null ein Array - die beiden letzteren sind ein einzelnes Elementarray bzw. ein leeres Array.

Ich dachte fälschlicherweise, Ruby würde so funktionieren:

[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

Aber was Sie wirklich bekommen, ist:

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

Um dies zu lösen, muss ich entweder eine andere Methode verwenden oder ich kann ein Meta-Programm erstellen, indem ich die to_a-Methode aller Klassen ändere, die ich verwenden möchte - was für mich keine Option ist.

Also eine Methode ist es:

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

Das Problem ist, dass es ein bisschen chaotisch ist. Gibt es eine elegante Möglichkeit, dies zu tun? (Ich würde mich wundern, wenn dies der rubinrote Weg ist, um dieses Problem zu lösen.)


Welche Anwendungen hat das? Warum überhaupt in ein Array konvertieren?

In Rails 'ActiveRecord gibt der Aufruf von say user.postsentweder ein Array von Posts, einen einzelnen Post oder null zurück. Wenn Sie Methoden schreiben, die an den Ergebnissen arbeiten, ist es am einfachsten anzunehmen, dass die Methode ein Array verwendet, das null, eins oder viele Elemente enthalten kann. Beispielmethode:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}
xxjjnn
quelle
2
user.postssollte niemals einen einzelnen Beitrag zurückgeben. Zumindest habe ich es nie gesehen.
Sergio Tulentsev
1
Ich denke, in deinen ersten beiden Codeblöcken meinst du ==statt =, oder?
Patrick Oscity
3
Übrigens, [1,2,3].to_akommt nicht zurück [[1,2,3]]! Es kehrt zurück [1,2,3].
Patrick Oscity
Danke Paddel, wird Frage aktualisieren ... facepalms at self
xxjjnn

Antworten:

151

[*foo]oder Array(foo)wird die meiste Zeit funktionieren, aber in einigen Fällen wie einem Hash bringt es es durcheinander.

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

Ich kann mir nur vorstellen, dass dies auch für einen Hash funktioniert, indem ich eine Methode definiere.

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}]
sawa
quelle
2
statt ensure_arrayverlängernto_a
Dan Grahn
9
@screenmutt Dies würde sich auf Methoden auswirken, die auf der ursprünglichen Verwendung von basieren to_a. Zum Beispiel {a: 1, b: 2}.each ...würde anders funktionieren.
Sawa
1
Können Sie diese Syntax erklären? In vielen Jahren von Ruby war ich nie auf diese Art von Anrufung gestoßen. Was machen Klammern auf einem Klassennamen? Ich kann das nicht in Dokumenten finden.
mastaBlasta
1
@mastaBlasta Array (arg) versucht, ein neues Array zu erstellen, indem es to_ary und dann to_a für das Argument aufruft. Dies ist in offiziellen Ruby-Dokumenten dokumentiert. Ich habe es aus Avdis "Confident Ruby" -Buch erfahren.
Mambo
2
@mambo Irgendwann, nachdem ich meine Frage gestellt hatte, fand ich die Antwort. Der schwierige Teil war, dass es nichts mit der Array-Klasse zu tun hat, sondern eine Methode im Kernel-Modul. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta
119

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

Wenn Sie Rails nicht verwenden, können Sie eine eigene Methode definieren, die der Rails-Quelle ähnelt .

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
quelle
12
class Array; singleton_class.send(:alias_method, :hug, :wrap); endfür zusätzliche Niedlichkeit.
Rthbound
21

Die einfachste Lösung ist die Verwendung [foo].flatten(1). Im Gegensatz zu anderen vorgeschlagenen Lösungen funktioniert es gut für (verschachtelte) Arrays, Hashes und 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'}]
oli
quelle
Leider hat dieser ein ernstes Leistungsproblem im Vergleich zu anderen Ansätzen. Kernel#Arraydh Array()ist der schnellste von allen. Ruby 2.5.1-Vergleich: Array (): 7936825.7 i / s. Array.wrap: 4199036.2 i / s - 1,89x langsamer. Wrap: 644030,4 i / s - 12,32x langsamer
Wasif Hossain
19

Array(whatever) sollte den Trick machen

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]
Benjamin Gruenbaum
quelle
14
funktioniert nicht für Hash. Array ({a: 1, b: 2}) wird [[: a, 1], [: b, 2]] sein
davispuh
13

ActiveSupport (Rails)

ActiveSupport hat dafür eine ziemlich gute Methode. Es ist voller Rails, also trotzig der schönste Weg, dies zu tun:

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

Splat (Ruby 1.9+)

Der splat-Operator ( *) hebt die Anordnung eines Arrays auf, wenn dies möglich ist:

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

Ohne Array macht es natürlich seltsame Dinge, und die Objekte, die Sie "splat", müssen in Arrays abgelegt werden. Es ist etwas seltsam, aber es bedeutet:

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

Wenn Sie nicht über ActiveSupport verfügen, können Sie die Methode definieren:

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

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

Wenn Sie jedoch große Arrays und weniger Nicht-Array-Elemente planen, möchten Sie diese möglicherweise ändern. Die obige Methode ist bei großen Arrays langsam und kann sogar zu einem Überlauf Ihres Stacks führen (omg so meta). Auf jeden Fall möchten Sie dies stattdessen tun:

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]

Ich habe auch einige Benchmarks mit und ohne Teneray-Operator.

Ben Aubin
quelle
Funktioniert nicht für große Arrays. SystemStackError: stack level too deepfür 1M Elemente (Rubin 2.2.3).
denis.peplin
@ denis.peplin scheint einen StackOverflow-Fehler zu haben: D - ehrlich gesagt bin ich mir nicht sicher, was passiert ist. Es tut uns leid.
Ben Aubin
Ich habe es kürzlich Hash#values_atmit 1M-Argumenten (mit splat) versucht , und es wird der gleiche Fehler ausgegeben.
denis.peplin
@ denis.peplin Funktioniert es mit object.is_a? Array ? object : [*object]?
Ben Aubin
1
Array.wrap(nil)kehrt []nicht zurück nil: /
Aeramor
7

Wie wäre es mit

[].push(anything).flatten
Bruno Meira
quelle
2
Ja, ich glaube, ich habe in meinem Fall [irgendetwas] .flatten verwendet ... aber für den allgemeinen Fall werden dadurch auch alle verschachtelten Array-Strukturen abgeflacht
xxjjnn
1
[].push(anything).flatten(1)würde funktionieren! Verschachtelte Arrays werden nicht abgeflacht!
xxjjnn
2

Mit dem Risiko, das Offensichtliche zu sagen und zu wissen, dass dies nicht der leckerste syntaktische Zucker ist, der jemals auf dem Planeten und in den umliegenden Gebieten gesehen wurde, scheint dieser Code genau das zu tun, was Sie beschreiben:

foo = foo.is_a?(Array) ? foo : foo.nil? ? [] : [foo]
Der Pellmeister
quelle
1

Sie können die Array-Methode von Object überschreiben

class Object
    def to_a
        [self]
    end
end

alles erbt Objekt, daher wird to_a nun für alles unter der Sonne definiert

runub
quelle
3
blasphemisches Affenflicken! Tut Buße!
xxjjnn
1

Ich habe alle Antworten durchgesehen und arbeite meistens nicht in Ruby 2+

Aber elado hat die eleganteste Lösung, dh

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

Leider funktioniert dies aber auch nicht für Ruby 2+, da Sie eine Fehlermeldung erhalten

undefined method `wrap' for Array:Class

Also, um das zu beheben, müssen Sie benötigen.

erfordern 'active_support / deprecation'

erfordern 'active_support / core_ext / array / wrap'

Malware Skiddie
quelle
0

Da die Methode #to_afür die beiden Hauptproblemklassen ( Nilund Hash) bereits vorhanden ist , definieren Sie einfach eine Methode für den Rest, indem Sie Folgendes erweitern Object:

class Object
    def to_a
        [self]
    end
end

und dann können Sie diese Methode einfach für jedes Objekt aufrufen:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []
Schuh
quelle
5
Ich denke wirklich, dass Affen, die eine Ruby-Kernklasse, insbesondere ein Objekt, patchen, vermieden werden sollten. Ich werde ActiveSupport einen Pass geben, also betrachte mich als Heuchler. Die obigen Lösungen von @sawa sind viel praktikabler.
Pho3nixf1re