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

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

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

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

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]

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.


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

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]

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


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”


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
# => []

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.