Ruby - dengan elegan mengubah variabel menjadi array jika belum menjadi array

120

Diberikan sebuah array, satu elemen, atau nil, dapatkan sebuah array - dua yang terakhir masing-masing menjadi array elemen tunggal dan sebuah array kosong.

Saya salah mengira Ruby akan bekerja seperti ini:

[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

Tapi yang sebenarnya Anda dapatkan adalah:

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

Jadi untuk mengatasi ini, saya perlu menggunakan metode lain, atau saya bisa meta program dengan memodifikasi metode to_a dari semua kelas yang ingin saya gunakan - yang bukan merupakan pilihan bagi saya.

Jadi Metode itu adalah:

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

Masalahnya adalah ini agak berantakan. Adakah cara elegan untuk melakukan ini? (Saya akan kagum jika ini adalah cara Ruby-ish untuk menyelesaikan masalah ini)


Aplikasi apa yang dimilikinya? Mengapa bahkan mengonversi ke array?

Di Rails 'ActiveRecord, memanggil say, user.postsakan mengembalikan array posting, posting tunggal, atau nil. Saat menulis metode yang bekerja pada hasil ini, paling mudah mengasumsikan bahwa metode tersebut akan mengambil array, yang mungkin memiliki nol, satu, atau banyak elemen. Metode contoh:

current_user.posts.inject(true) {|result, element| result and (element.some_boolean_condition)}
xxjjnn
sumber
2
user.poststidak boleh mengembalikan satu pun postingan. Setidaknya, saya tidak pernah melihatnya.
Sergio Tulentsev
1
saya pikir di dua blok kode pertama Anda Anda maksud ==bukan =, kan?
Patrick Oscity
3
Btw, [1,2,3].to_atidak tidak kembali [[1,2,3]]! Ia kembali [1,2,3].
Patrick Oscity
Terima kasih dayung, akan perbarui pertanyaan ... facepalms at self
xxjjnn

Jawaban:

153

[*foo]atau Array(foo)akan bekerja di sebagian besar waktu, tetapi untuk beberapa kasus seperti hash, itu mengacaukannya.

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

Satu-satunya cara yang dapat saya pikirkan yang berfungsi bahkan untuk hash adalah dengan menentukan metode.

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
sumber
2
alih-alih ensure_array, perpanjangto_a
Dan Grahn
9
@screenmutt Itu akan memengaruhi metode yang mengandalkan penggunaan asli to_a. Misalnya, {a: 1, b: 2}.each ...akan bekerja secara berbeda.
sawa
1
Bisakah Anda menjelaskan sintaks ini? Selama bertahun-tahun di Ruby, saya tidak pernah menemukan jenis doa ini. Apa fungsi tanda kurung pada nama kelas? Saya tidak dapat menemukan ini di dokumen.
mastaBlasta
1
@mastaBlasta Array (arg) mencoba membuat array baru dengan memanggil to_ary, lalu to_a pada argumen. Ini didokumentasikan dalam dokumen ruby ​​resmi. Saya mempelajarinya dari buku "Confident Ruby" Avdi.
mambo
2
@mambo Pada beberapa titik setelah saya memposting pertanyaan saya, saya menemukan jawabannya. Bagian yang sulit adalah tidak ada hubungannya dengan kelas Array tetapi ini adalah metode pada modul Kernel. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta
119

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

Jika Anda tidak menggunakan Rails, Anda dapat menentukan metode Anda sendiri yang mirip dengan sumber 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
sumber
12
class Array; singleton_class.send(:alias_method, :hug, :wrap); enduntuk kelucuan ekstra.
r
21

Solusi paling sederhana adalah menggunakan [foo].flatten(1). Tidak seperti solusi lain yang diusulkan, ini akan bekerja dengan baik untuk array (nested), hashes dan 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
sumber
sayangnya yang satu ini memiliki masalah kinerja yang serius dibandingkan dengan pendekatan lain. Kernel#Arrayyaitu Array()yang tercepat dari semuanya. Ruby 2.5.1 perbandingan: Array (): 7936825.7 i / s. Array.wrap: 4199036.2 i / s - 1.89x lebih lambat. bungkus: 644030,4 i / s - 12,32x lebih lambat
Wasif Hossain
19

Array(whatever) harus melakukan triknya

Array([1,2,3]) # [1,2,3]
Array(nil) # []
Array(1337)   # [1337]
Benjamin Gruenbaum
sumber
14
tidak akan berhasil untuk Hash. Array ({a: 1, b: 2}) akan menjadi [[: a, 1], [: b, 2]]
davispuh
13

ActiveSupport (Rails)

ActiveSupport memiliki metode yang cukup bagus untuk ini. Itu sarat dengan Rails, jadi cara terbaik untuk melakukan ini:

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

Gambar percikan (Ruby 1.9+)

Operator percikan ( *) menghapus array, jika bisa:

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

Tentu saja, tanpa sebuah larik, ia melakukan hal-hal aneh, dan objek yang Anda "percikan" perlu diletakkan dalam larik. Ini agak aneh, tapi artinya:

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

Jika Anda tidak memiliki ActiveSupport, Anda dapat menentukan metode:

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

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

Meskipun, jika Anda berencana untuk memiliki array besar, dan lebih sedikit hal-hal non-array, Anda mungkin ingin mengubahnya - metode di atas lambat dengan array besar, dan bahkan dapat menyebabkan Stack Anda Melimpah (omg so meta). Bagaimanapun, Anda mungkin ingin melakukan ini sebagai gantinya:

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]

Saya juga memiliki beberapa benchmark dengan dan tanpa operator hari ini.

Ben Aubin
sumber
Tidak akan berfungsi untuk array besar. SystemStackError: stack level too deepuntuk elemen 1 juta (ruby 2.2.3).
denis.peplin
@ denis.peplin sepertinya Anda mendapat kesalahan StackOverflow: D - sejujurnya, saya tidak yakin apa yang terjadi. Maaf.
Ben Aubin
Saya baru-baru ini mencoba Hash#values_atdengan argumen 1M (menggunakan splat), dan itu membuat kesalahan yang sama.
denis.peplin
@ denis.peplin Apakah berfungsi dengan object.is_a? Array ? object : [*object]?
Ben Aubin
1
Array.wrap(nil)kembali []tidak nil: /
Aeramor
7

Bagaimana tentang

[].push(anything).flatten
Bruno Meira
sumber
2
Ya, saya pikir saya akhirnya menggunakan [apa saja] .flatten dalam kasus saya ... tetapi untuk kasus umum ini juga akan meratakan struktur array bersarang
xxjjnn
1
[].push(anything).flatten(1)akan berhasil! Itu tidak meratakan array bersarang!
xxjjnn
2

Dengan risiko menyatakan hal yang sudah jelas, dan mengetahui bahwa ini bukan gula sintaksis paling enak yang pernah ada di planet ini dan sekitarnya, kode ini tampaknya melakukan persis seperti yang Anda gambarkan:

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

Anda dapat menimpa metode array dari Object

class Object
    def to_a
        [self]
    end
end

semuanya mewarisi Object, oleh karena itu to_a sekarang akan didefinisikan untuk semua yang ada di bawah matahari

runub
sumber
3
menambal monyet yang menghujat! Bertobatlah!
xxjjnn
1

Saya telah memeriksa semua jawaban dan sebagian besar tidak berfungsi di ruby ​​2+

Tapi elado memiliki solusi paling elegan yaitu

Dengan ActiveSupport (Rails): Array.wrap

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

Array.wrap (1) # => [1]

Array.wrap (nihil) # => []

Array.wrap ({a: 1, b: 2}) # => [{: a => 1,: b => 2}]

Sayangnya tetapi ini juga tidak berfungsi untuk ruby ​​2+ karena Anda akan mendapatkan kesalahan

undefined method `wrap' for Array:Class

Jadi untuk memperbaikinya Anda perlu membutuhkan.

membutuhkan 'active_support / deprecation'

membutuhkan 'active_support / core_ext / array / wrap'

Skiddie Malware
sumber
0

Karena metode #to_asudah ada untuk dua kelas bermasalah utama ( Nildan Hash), cukup tentukan metode untuk sisanya dengan memperluas Object:

class Object
    def to_a
        [self]
    end
end

lalu Anda dapat dengan mudah memanggil metode itu pada objek apa pun:

"Hello world".to_a
# => ["Hello world"]
123.to_a
# => [123]
{a:1, b:2}.to_a
# => [[:a, 1], [:b, 2]] 
nil.to_a
# => []
Sepatu
sumber
5
Saya pikir monyet menambal kelas inti Ruby, terutama objek, harus dihindari. Saya akan memberi ActiveSupport izin jadi anggap saya munafik. Solusi di atas oleh @sawa jauh lebih layak daripada ini.
pho3nixf1re