Ruby - elegancko konwertuje zmienną na tablicę, jeśli nie jest już tablicą

120

Biorąc pod uwagę tablicę, pojedynczy element lub nil, uzyskaj tablicę - dwie ostatnie są odpowiednio tablicą jednoelementową i pustą.

Pomyłkowo pomyślałem, że Ruby będzie działać w ten sposób:

[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

Ale tak naprawdę otrzymujesz:

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

Aby rozwiązać ten problem, muszę albo użyć innej metody, albo metaprogram, modyfikując metodę to_a wszystkich klas, których zamierzam użyć - co nie jest dla mnie opcją.

Oto metoda:

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

Problem w tym, że jest trochę bałaganu. Czy jest na to elegancki sposób? (Byłbym zdziwiony, gdyby to był Rubinowy sposób rozwiązania tego problemu)


Jakie to ma aplikacje? Po co w ogóle konwertować na tablicę?

W ActiveRecord Railsów, wywołanie say, user.postszwróci albo tablicę postów, pojedynczy post, albo zero. Pisząc metody, które działają na wynikach tego, najłatwiej jest założyć, że metoda przyjmie tablicę, która może mieć zero, jeden lub wiele elementów. Przykładowa metoda:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}
xxjjnn
źródło
2
user.postsnigdy nie powinien zwracać ani jednej wiadomości. Przynajmniej nigdy tego nie widziałem.
Sergio Tulentsev
1
myślę, że w twoich pierwszych dwóch blokach kodu masz na myśli ==zamiast =, prawda?
Patrick Oscity
3
Btw, [1,2,3].to_aczy nie wrócić [[1,2,3]]! Wraca [1,2,3].
Patrick Oscity
Dzięki wiosło, zaktualizuję pytanie ... facepalms przy sobie
xxjjnn

Odpowiedzi:

153

[*foo]lub Array(foo)będzie działać przez większość czasu, ale w niektórych przypadkach, takich jak hash, psuje.

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

Jedynym sposobem, w jaki mogę pomyśleć, że działa to nawet w przypadku skrótu, jest zdefiniowanie metody.

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
źródło
2
zamiast ensure_arrayprzedłużyćto_a
Dan Grahn
9
@screenmutt To wpłynęłoby na metody, które opierają się na oryginalnym użyciu to_a. Na przykład {a: 1, b: 2}.each ...działałoby inaczej.
sawa
1
Czy możesz wyjaśnić tę składnię? Przez wiele lat Rubiego nigdy nie spotkałem się z tego typu inwokacjami. Co robią nawiasy w nazwie klasy? Nie mogę znaleźć tego w dokumentach.
mastaBlasta
1
@mastaBlasta Array (arg) próbuje utworzyć nową tablicę, wywołując to_ary, a następnie to_a w argumencie. Jest to udokumentowane w oficjalnych dokumentach ruby. Dowiedziałem się o tym z książki Avdiego „Confident Ruby”.
mambo
2
@mambo W pewnym momencie po opublikowaniu mojego pytania znalazłem odpowiedź. Najtrudniejsze było to, że nie ma to nic wspólnego z klasą Array, ale jest to metoda w module jądra. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta
119

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

Jeśli nie używasz Railsów, możesz zdefiniować własną metodę podobną do źródła railsów .

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
źródło
12
class Array; singleton_class.send(:alias_method, :hug, :wrap); enddla dodatkowej urody.
rthbound
21

Najprostszym rozwiązaniem jest użycie [foo].flatten(1). W przeciwieństwie do innych proponowanych rozwiązań sprawdzi się dobrze w przypadku (zagnieżdżonych) tablic, skrótów i 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
źródło
niestety ten ma poważny problem z wydajnością w porównaniu z innymi podejściami. Kernel#Arrayie Array()jest najszybszym z nich wszystkich. Porównanie z Rubim 2.5.1: Array (): 7936825,7 i / s. Array.wrap: 4199036,2 i / s - 1,89x wolniej. owinięcie: 644030.4 i / s - 12,32x wolniej
Wasif Hossain
19

Array(whatever) powinien załatwić sprawę

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]
Benjamin Gruenbaum
źródło
14
nie zadziała dla Hash. Tablica ({a: 1, b: 2}) będzie miała wartość [[: a, 1], [: b, 2]]
davispuh
13

ActiveSupport (szyny)

ActiveSupport ma na to całkiem niezłą metodę. Jest załadowany Railsami, więc zdecydowanie najładniejszy sposób na zrobienie tego:

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

Splat (Ruby 1.9+)

Operator splat ( *) usuwa tablice z tablicy, jeśli może:

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

Oczywiście bez tablicy robi dziwne rzeczy, a obiekty, które „splata” trzeba umieścić w tablicach. To trochę dziwne, ale oznacza:

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

Jeśli nie masz ActiveSupport, możesz zdefiniować metodę:

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

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

Chociaż, jeśli planujesz mieć duże tablice i mniej rzeczy niezwiązanych z tablicami, możesz chcieć to zmienić - powyższa metoda jest powolna w przypadku dużych tablic i może nawet spowodować przepełnienie stosu (omg, więc meta). W każdym razie możesz to zrobić zamiast tego:

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]

Mam też kilka testów z operatorem dzisiejszego dnia i bez niego.

Ben Aubin
źródło
Nie będzie działać dla dużych tablic. SystemStackError: stack level too deepdla 1M elementów (ruby 2.2.3).
denis.peplin
@ denis.peplin wygląda na to, że wystąpił błąd StackOverflow: D - szczerze mówiąc, nie jestem pewien, co się stało. Przepraszam.
Ben Aubin
Niedawno próbowałem Hash#values_atz 1 mln argumentów (używając splat) i zgłasza ten sam błąd.
denis.peplin
@ denis.peplin Czy to działa object.is_a? Array ? object : [*object]?
Ben Aubin,
1
Array.wrap(nil)[]nie zwraca nil: /
Aeramor
7

Co powiesz na

[].push(anything).flatten
Bruno Meira
źródło
2
Tak, myślę, że skończyło się na użyciu [cokolwiek] .flatten w moim przypadku ... ale w ogólnym przypadku spowoduje to również spłaszczenie wszelkich zagnieżdżonych struktur tablicowych
xxjjnn
1
[].push(anything).flatten(1)pracowałbym! Nie spłaszcza tablic zagnieżdżonych!
xxjjnn
2

Ryzykując stwierdzenie oczywistego i wiedząc, że nie jest to najsmaczniejszy cukier syntaktyczny, jaki kiedykolwiek widziano na naszej planecie i otaczających ją obszarach, ten kod wydaje się robić dokładnie to, co opisujesz:

foo = foo.is_a?(Array) ? foo : foo.nil? ? [] : [foo]
Pellmeister
źródło
1

możesz nadpisać metodę tablicową Object

class Object
    def to_a
        [self]
    end
end

wszystko dziedziczy Object, dlatego to_a będzie teraz zdefiniowane dla wszystkiego pod słońcem

runub
źródło
3
bluźniercze łatanie małp! Pokutujcie!
xxjjnn,
1

Przejrzałem wszystkie odpowiedzi i przeważnie nie działają w Ruby 2+

Ale elado ma najbardziej eleganckie rozwiązanie, tj

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

Niestety, ale to również nie działa dla Ruby 2+, ponieważ pojawi się błąd

undefined method `wrap' for Array:Class

Aby to naprawić, musisz wymagać.

wymagaj „active_support / deprecation”

wymagają „active_support / core_ext / array / wrap”

Malware Skiddie
źródło
0

Ponieważ metoda #to_ajuż istnieje dla dwóch głównych klas problematycznych ( Nili Hash), po prostu zdefiniuj metodę dla pozostałych, rozszerzając Object:

class Object
    def to_a
        [self]
    end
end

a następnie możesz łatwo wywołać tę metodę na dowolnym obiekcie:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []
But
źródło
5
Naprawdę uważam, że należy unikać łatania przez małpę podstawowej klasy Ruby, a zwłaszcza obiektu. Dam ActiveSupport przepustkę, więc uważaj mnie za hipokrytę. Powyższe rozwiązania autorstwa @sawa są znacznie bardziej wykonalne niż to.
pho3nixf1re