Ruby - แปลงตัวแปรเป็นอาร์เรย์อย่างหรูหราหากยังไม่มีอาร์เรย์


120

รับอาร์เรย์องค์ประกอบเดียวหรือศูนย์รับอาร์เรย์ - สองตัวหลังเป็นอาร์เรย์องค์ประกอบเดียวและอาร์เรย์ว่างตามลำดับ

ฉันคิดผิดว่า Ruby จะทำงานในลักษณะนี้:

[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

แต่สิ่งที่คุณจะได้รับคือ:

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

ดังนั้นเพื่อแก้ปัญหานี้ฉันจำเป็นต้องใช้วิธีอื่นหรือฉันสามารถโปรแกรมเมตาได้โดยการแก้ไขวิธีการ to_a ของทุกคลาสที่ฉันตั้งใจจะใช้ซึ่งไม่ใช่ทางเลือกสำหรับฉัน

ดังนั้นวิธีการคือ:

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

ปัญหาคือมันเป็นเรื่องยุ่งเหยิง มีวิธีที่สวยงามในการทำเช่นนี้หรือไม่? (ฉันจะประหลาดใจถ้านี่เป็นวิธี Ruby-ish ในการแก้ปัญหานี้)


แอปพลิเคชันนี้มีอะไรบ้าง? ทำไมถึงแปลงเป็นอาร์เรย์

ใน ActiveRecord ของ Rails การโทรบอกว่าuser.postsจะส่งกลับอาร์เรย์ของโพสต์โพสต์เดียวหรือศูนย์ เมื่อเขียนวิธีการที่ทำงานกับผลลัพธ์นี้เป็นเรื่องง่ายที่สุดที่จะสมมติว่าเมธอดนั้นใช้อาร์เรย์ซึ่งอาจมีองค์ประกอบเป็นศูนย์หนึ่งหรือหลายองค์ประกอบ ตัวอย่างวิธีการ:

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

2
user.postsไม่ควรส่งคืนโพสต์เดียว อย่างน้อยฉันก็ไม่เคยเห็นมัน
Sergio Tulentsev

1
ฉันคิดว่าในสองรหัสบล็อกแรกของคุณคุณหมายถึง==แทนที่จะเป็น=ใช่ไหม
Patrick Oscity

2
udplicate ที่เป็นไปได้: stackoverflow.com/questions/385912/ruby-object-to-a-replacement
Dan Grahn

3
Btw, [1,2,3].to_aไม่ได้กลับมา[[1,2,3]]! มันกลับ[1,2,3]มา
Patrick Oscity

ขอบคุณพายจะอัปเดตคำถาม ... facepalms ที่ตนเอง
xxjjnn

คำตอบ:


153

[*foo]หรือArray(foo)จะทำงานเกือบตลอดเวลา แต่สำหรับบางกรณีเช่นแฮชมันจะทำให้มันยุ่งเหยิง

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

วิธีเดียวที่ฉันคิดได้ว่ามันใช้ได้ผลกับแฮชคือการกำหนดวิธีการ

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
แทนที่จะensure_arrayขยายto_a
Dan Grahn

9
@screenmutt นั่นจะส่งผลต่อวิธีการที่อาศัยการใช้งานto_aไฟล์. ตัวอย่างเช่น{a: 1, b: 2}.each ...จะทำงานแตกต่างกัน
sawa

1
คุณสามารถอธิบายไวยากรณ์นี้ได้หรือไม่? ในช่วงหลายปีของ Ruby ฉันไม่เคยเจอคำขอร้องประเภทนี้ วงเล็บบนชื่อคลาสมีไว้ทำอะไร? ฉันไม่พบสิ่งนี้ในเอกสาร
mastaBlasta

1
@mastaBlasta Array (arg) พยายามสร้างอาร์เรย์ใหม่โดยเรียก to_ary จากนั้น to_a บนอาร์กิวเมนต์ เอกสารนี้ถูกบันทึกไว้ในเอกสารทับทิมอย่างเป็นทางการ ฉันได้เรียนรู้เรื่องนี้จากหนังสือ "Confident Ruby" ของ Avdi
mambo

2
@mambo เมื่อถึงจุดหนึ่งหลังจากที่ฉันโพสต์คำถามฉันก็พบคำตอบ ส่วนที่ยากคือมันไม่มีส่วนเกี่ยวข้องกับคลาส Array แต่เป็นวิธีการในโมดูลเคอร์เนล ruby-doc.org/core-2.3.1/Kernel.html#method-i-Array
mastaBlasta

119

ด้วย 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}]

หากคุณไม่ได้ใช้ 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เพื่อความน่ารักเป็นพิเศษ
rthbound

21

[foo].flatten(1)ทางออกที่ง่ายที่สุดคือการใช้งาน ซึ่งแตกต่างจากโซลูชันที่เสนออื่น ๆ มันจะทำงานได้ดีสำหรับอาร์เรย์ (ซ้อนกัน) แฮชและ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'}]

น่าเสียดายที่ปัญหานี้มีปัญหาด้านประสิทธิภาพที่ร้ายแรงเมื่อเทียบกับวิธีอื่น ๆ Kernel#Arrayนั่นArray()คือเร็วที่สุดของพวกเขาทั้งหมด การเปรียบเทียบ Ruby 2.5.1: Array (): 7936825.7 i / s Array.wrap: 4199036.2 i / s - ช้ากว่า 1.89 เท่า ห่อ: 644030.4 i / s - ช้ากว่า 12.32 เท่า
Wasif Hossain

19

Array(whatever) ควรทำเคล็ดลับ

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

14
ใช้ไม่ได้กับ Hash อาร์เรย์ ({a: 1, b: 2}) จะเป็น [[: a, 1], [: b, 2]]
davispuh

13

ActiveSupport (ราง)

ActiveSupport มีวิธีการที่ดีสำหรับสิ่งนี้ มันเต็มไปด้วย Rails ดังนั้นวิธีที่ดีที่สุดในการทำสิ่งนี้:

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

Splat (รูบี้ 1.9+)

ตัวดำเนินการ Splat ( *) ยกเลิกการจัดเรียงอาร์เรย์หากสามารถ:

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

แน่นอนว่าถ้าไม่มีอาร์เรย์มันก็มีอะไรแปลก ๆ และต้องใส่วัตถุที่คุณ "แยก" ไว้ในอาร์เรย์ มันค่อนข้างแปลก แต่หมายความว่า:

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

หากคุณไม่มี ActiveSupport คุณสามารถกำหนดวิธีการ:

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

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

แม้ว่าคุณวางแผนที่จะมีอาร์เรย์ขนาดใหญ่และมีสิ่งที่ไม่ใช่อาร์เรย์น้อยกว่าคุณอาจต้องการเปลี่ยนแปลงวิธีการข้างต้นทำงานช้าด้วยอาร์เรย์ขนาดใหญ่และอาจทำให้ Stack ของคุณล้น (ดังนั้น meta) อย่างไรก็ตามคุณอาจต้องการทำสิ่งนี้แทน:

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]

ฉันยังมีเกณฑ์มาตรฐานที่มีและไม่มีตัวดำเนินการ teneray


จะไม่ทำงานกับอาร์เรย์ขนาดใหญ่ SystemStackError: stack level too deepสำหรับองค์ประกอบ 1M (ทับทิม 2.2.3)
denis.peplin

@ denis.peplin ดูเหมือนว่าคุณมีข้อผิดพลาด StackOverflow: D - พูดตามตรงฉันไม่แน่ใจว่าเกิดอะไรขึ้น ขอโทษ
Ben Aubin

ฉันเพิ่งลองHash#values_atใช้อาร์กิวเมนต์ 1M (โดยใช้splat) และมันแสดงข้อผิดพลาดเดียวกัน
denis.peplin

@ denis.peplin ใช้ได้กับobject.is_a? Array ? object : [*object]?
Ben Aubin

1
Array.wrap(nil)[]ไม่ส่งคืนnil: /
Aeramor

7

เกี่ยวกับ

[].push(anything).flatten

2
ใช่ฉันคิดว่าฉันใช้ [อะไรก็ได้]. flatten ในกรณีของฉัน ... แต่สำหรับกรณีทั่วไปสิ่งนี้จะทำให้โครงสร้างอาร์เรย์ที่ซ้อนกันแบนราบ
xxjjnn

1
[].push(anything).flatten(1)จะทำงาน! มันไม่แบนอาร์เรย์ที่ซ้อนกัน!
xxjjnn

2

ด้วยความเสี่ยงที่จะระบุสิ่งที่ชัดเจนและรู้ว่านี่ไม่ใช่น้ำตาลซินแทติกที่อร่อยที่สุดที่เคยเห็นบนโลกและบริเวณโดยรอบรหัสนี้ดูเหมือนจะทำตามที่คุณอธิบาย:

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

1

คุณสามารถเขียนทับเมธอดอาร์เรย์ของ Object ได้

class Object
    def to_a
        [self]
    end
end

ทุกอย่างสืบทอด Object ดังนั้น to_a จะถูกกำหนดสำหรับทุกสิ่งภายใต้ดวงอาทิตย์


3
ปะลิงดูหมิ่น! จงกลับใจ!
xxjjnn

1

ฉันได้ตอบคำถามทั้งหมดแล้วและส่วนใหญ่ใช้ไม่ได้กับ Ruby 2+

แต่ elado มีทางออกที่หรูหราที่สุดคือ

ด้วย ActiveSupport (Rails): Array.wrap

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

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

Array.wrap (ศูนย์) # => []

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

น่าเศร้า แต่นี่ก็ใช้ไม่ได้กับ Ruby 2+ เนื่องจากคุณจะได้รับข้อผิดพลาด

undefined method `wrap' for Array:Class

ดังนั้นเพื่อแก้ไขปัญหาที่คุณจำเป็นต้องใช้

ต้องการ 'active_support / deprecation'

ต้องการ 'active_support / core_ext / array / wrap'


0

เนื่องจากมีวิธีการ#to_aอยู่แล้วสำหรับคลาสที่มีปัญหาหลักสองคลาส ( NilและHash) เพียงกำหนดวิธีการสำหรับส่วนที่เหลือโดยการขยายObject:

class Object
    def to_a
        [self]
    end
end

จากนั้นคุณสามารถเรียกใช้เมธอดนั้นบนวัตถุใดก็ได้:

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

5
ฉันคิดว่าลิงที่แพตช์คลาสรูบี้หลักโดยเฉพาะอ็อบเจกต์เป็นสิ่งที่ควรหลีกเลี่ยง ฉันจะให้ ActiveSupport ผ่านแม้ว่าจะถือว่าฉันเป็นคนหน้าซื่อใจคด โซลูชันข้างต้นโดย @sawa มีประสิทธิภาพมากกว่านี้
pho3nixf1re
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.