Ruby - chuyển đổi một cách thanh lịch biến thành một mảng nếu chưa phải là một mảng


119

Cho một mảng, một phần tử đơn hoặc nil, thu được một mảng - hai mảng sau tương ứng là một mảng phần tử đơn và một mảng trống.

Tôi nhầm tưởng rằng Ruby sẽ hoạt động theo cách này:

[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

Nhưng những gì bạn thực sự nhận được là:

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

Vì vậy, để giải quyết vấn đề này, tôi cần phải sử dụng một phương thức khác hoặc tôi có thể lập trình meta bằng cách sửa đổi phương thức to_a của tất cả các lớp tôi định sử dụng - đây không phải là một lựa chọn cho tôi.

Vì vậy, một phương pháp nó là:

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

Vấn đề là nó hơi lộn xộn. Có một cách thanh lịch để làm điều này? (Tôi sẽ ngạc nhiên nếu đây là cách Ruby-ish để giải quyết vấn đề này)


Ứng dụng này có những gì? Tại sao thậm chí chuyển đổi thành một mảng?

Trong ActiveRecord của Rails, việc gọi say, user.postssẽ trả về một mảng các bài đăng, một bài đăng hoặc nil. Khi viết các phương thức hoạt động dựa trên kết quả của điều này, dễ dàng nhất là giả định rằng phương thức sẽ nhận một mảng, mảng có thể có 0, một hoặc nhiều phần tử. Phương pháp ví dụ:

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

2
user.postskhông bao giờ nên trả lại một bài. Ít nhất, tôi chưa bao giờ nhìn thấy nó.
Sergio Terrysev

1
Tôi nghĩ rằng trong hai khối mã đầu tiên của bạn, bạn muốn nói ==thay vì =, phải không?
Patrick Oscity


3
Btw, [1,2,3].to_akhông không quay trở lại [[1,2,3]]! Nó trở lại [1,2,3].
Patrick Oscity

Cảm ơn mái chèo, sẽ cập nhật câu hỏi ... facepalms tại tự
xxjjnn

Câu trả lời:


152

[*foo]hoặc Array(foo)sẽ hoạt động hầu hết thời gian, nhưng đối với một số trường hợp như băm, nó sẽ làm rối tung nó.

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

Cách duy nhất tôi có thể nghĩ rằng nó hoạt động ngay cả đối với một hàm băm là xác định một phương thức.

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
thay vì ensure_array, hãy mở rộngto_a
Dan Grahn

9
@screenmutt Điều đó sẽ ảnh hưởng đến các phương pháp dựa trên cách sử dụng ban đầu của to_a. Ví dụ, {a: 1, b: 2}.each ...sẽ hoạt động khác.
sawa

1
Bạn có thể giải thích cú pháp này? Trong nhiều năm của Ruby, tôi chưa bao giờ bắt gặp kiểu gọi này. Dấu ngoặc đơn trên tên lớp làm gì? Tôi không thể tìm thấy điều này trong tài liệu.
mastaBlasta

1
@mastaBlasta Array (arg) cố gắng tạo một mảng mới bằng cách gọi to_ary, sau đó to_a trên đối số. Điều này được ghi lại trong các tài liệu chính thức về ruby. Tôi biết được điều đó từ cuốn sách "Viên ngọc tự tin" của Avdi.
mambo,

2
@mambo Tại một số thời điểm sau khi tôi đăng câu hỏi của mình, tôi đã tìm thấy câu trả lời. Phần khó là nó không liên quan gì đến lớp Array mà nó là một phương thức trên mô-đun Kernel. ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta

119

Với 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}]

Nếu bạn không sử dụng Rails, bạn có thể xác định phương thức của riêng mình tương tự như nguồn 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); endđể thêm dễ thương.
rthbound

21

Giải pháp đơn giản nhất là sử dụng [foo].flatten(1). Không giống như các giải pháp được đề xuất khác, nó sẽ hoạt động tốt cho các mảng, hàm băm và 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'}]

Rất tiếc, cách này có vấn đề về hiệu suất nghiêm trọng so với các cách tiếp cận khác. Kernel#Arraytức Array()là nhanh nhất trong số họ. So sánh Ruby 2.5.1: Array (): 7936825.7 i / s. Array.wrap: 4199036,2 i / s - chậm hơn 1,89 lần. quấn: 644.030,4 i / s - 12.32x chậm
Wasif Hossain

19

Array(whatever) nên làm thủ thuật

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

14
sẽ không hoạt động đối với Hash. Mảng ({a: 1, b: 2}) sẽ là [[: a, 1], [: b, 2]]
davispuh

13

ActiveSupport (Rails)

ActiveSupport có một phương pháp khá hay cho việc này. Nó được tải bằng Rails, vì vậy cách tốt nhất để làm điều này là:

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

Splat (Ruby 1.9+)

Toán tử splat ( *) hủy mảng một mảng, nếu nó có thể:

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

Tất nhiên, không có mảng, nó sẽ làm những điều kỳ lạ, và các đối tượng mà bạn "giật gân" cần phải được đặt trong mảng. Nó hơi kỳ lạ, nhưng nó có nghĩa là:

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

Nếu bạn không có ActiveSupport, bạn có thể xác định phương pháp:

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

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

Mặc dù, nếu bạn dự định có nhiều mảng lớn và ít thứ không phải là mảng hơn, bạn có thể muốn thay đổi nó - phương pháp trên chậm với các mảng lớn và thậm chí có thể khiến Ngăn xếp của bạn bị Tràn (omg so meta). Dù sao, bạn có thể muốn làm điều này thay thế:

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]

Tôi cũng có một số điểm chuẩn có và không có toán tử teneray.


Sẽ không hoạt động cho các mảng lớn. SystemStackError: stack level too deepcho các phần tử 1M (ruby 2.2.3).
denis.peplin

@ denis.peplin có vẻ như bạn gặp lỗi StackOverflow: D - thành thật mà nói, tôi không chắc chuyện gì đã xảy ra. Lấy làm tiếc.
Ben Aubin

Gần đây tôi đã thử Hash#values_atvới 1 triệu đối số (bằng cách sử dụng splat) và nó gây ra lỗi tương tự.
denis.peplin

@ denis.peplin Nó có hoạt động với object.is_a? Array ? object : [*object]không?
Ben Aubin

1
Array.wrap(nil)[]không trả lại nil: /
Aeramor

7

Làm thế nào về

[].push(anything).flatten

2
Vâng tôi nghĩ rằng tôi đã kết thúc bằng [gì] .flatten trong trường hợp của tôi ... nhưng đối với các trường hợp chung này cũng sẽ san bằng bất kỳ cấu trúc mảng lồng nhau
xxjjnn

1
[].push(anything).flatten(1)sẽ hoạt động! Nó không làm phẳng các mảng lồng nhau!
xxjjnn

2

Với rủi ro nêu rõ điều hiển nhiên, và biết rằng đây không phải là loại đường cú pháp ngon nhất từng thấy trên hành tinh và các khu vực xung quanh, đoạn mã này dường như làm chính xác những gì bạn mô tả:

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

1

bạn có thể ghi đè phương thức mảng của Object

class Object
    def to_a
        [self]
    end
end

mọi thứ kế thừa Object, do đó to_a bây giờ sẽ được định nghĩa cho mọi thứ dưới ánh mặt trời


3
con khỉ phạm thượng vá lỗi! Hãy ăn năn!
xxjjnn

1

Tôi đã xem qua tất cả các câu trả lời và hầu hết không hoạt động trong ruby ​​2+

Nhưng elado có giải pháp thanh lịch nhất, tức là

Với 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}]

Đáng buồn là điều này cũng không hoạt động đối với ruby ​​2+ vì bạn sẽ gặp lỗi

undefined method `wrap' for Array:Class

Vì vậy, để khắc phục điều đó bạn cần yêu cầu.

yêu cầu 'active_support / không dùng nữa'

yêu cầu 'active_support / core_ext / array / wrap'


0

Vì phương thức #to_ađã tồn tại cho hai lớp có vấn đề chính ( NilHash), chỉ cần xác định một phương thức cho phần còn lại bằng cách mở rộng Object:

class Object
    def to_a
        [self]
    end
end

và sau đó bạn có thể dễ dàng gọi phương thức đó trên bất kỳ đối tượng nào:

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

5
Tôi thực sự nghĩ rằng việc vá một lớp Ruby lõi, đặc biệt là đối tượng, là điều nên tránh. Mặc dù vậy, tôi sẽ cho ActiveSupport vượt qua vì vậy hãy coi tôi là kẻ đạo đức giả. Các giải pháp ở trên của @sawa khả thi hơn nhiều.
pho3nixf1re 07/07
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.