Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

การปิดใน Ruby:Blocks, Procs และ Lambdas

ใน Ruby Magic เราชอบที่จะดำดิ่งสู่ความมหัศจรรย์เบื้องหลังสิ่งที่เราใช้ทุกวันเพื่อทำความเข้าใจวิธีการทำงาน ในฉบับนี้ เราจะมาสำรวจความแตกต่างระหว่างบล็อก โปรค และแลมบ์ดา

ในภาษาโปรแกรมที่มีฟังก์ชันระดับเฟิร์สคลาส ฟังก์ชันสามารถเก็บไว้ในตัวแปรและส่งต่อเป็นอาร์กิวเมนต์ไปยังฟังก์ชันอื่นๆ ฟังก์ชันยังใช้ฟังก์ชันอื่นเป็นค่าที่ส่งกลับได้อีกด้วย

การปิดเป็นฟังก์ชันชั้นหนึ่งที่มีสภาพแวดล้อม สภาพแวดล้อมคือการแมปกับตัวแปรที่มีอยู่เมื่อมีการสร้างการปิด การปิดจะคงการเข้าถึงตัวแปรเหล่านี้ไว้ แม้ว่าจะมีการกำหนดไว้ในขอบเขตอื่น

Ruby ไม่มีฟังก์ชันระดับเฟิร์สคลาส แต่มีฝาปิดในรูปแบบของบล็อก โปรค และแลมบ์ดา บล็อกใช้สำหรับส่งบล็อกของโค้ดไปยังเมธอด และ procs และ lambda อนุญาตให้จัดเก็บบล็อกของโค้ดในตัวแปร

บล็อค

ใน Ruby บล็อก เป็นส่วนย่อยของรหัสที่สามารถสร้างเพื่อดำเนินการในภายหลัง บล็อกถูกส่งไปยังวิธีการที่ให้ผลลัพธ์ภายใน do และ end คีย์เวิร์ด หนึ่งในตัวอย่างมากมายคือ #each วิธีการซึ่งวนรอบวัตถุที่นับได้

[1,2,3].each do |n|
  puts "#{n}!"
end
 
[1,2,3].each { |n| puts "#{n}!" } # the one-line equivalent.

ในตัวอย่างนี้ บล็อกจะถูกส่งไปยัง Array#each เมธอด ซึ่งรันบล็อกสำหรับแต่ละรายการในอาร์เรย์และพิมพ์ไปยังคอนโซล

def each
  i = 0
  while i < size
    yield at(i)
    i += 1
  end
end

ในตัวอย่างแบบง่ายของ Array#each ใน while วนซ้ำ yield ถูกเรียกให้ดำเนินการบล็อกที่ส่งผ่านสำหรับทุกรายการในอาร์เรย์ โปรดทราบว่าเมธอดนี้ไม่มีอาร์กิวเมนต์ เนื่องจากบล็อกจะถูกส่งไปยังเมธอดโดยปริยาย

การบล็อกโดยนัยและ yield คีย์เวิร์ด

ใน Ruby วิธีการสามารถบล็อกโดยปริยายและชัดเจน การบล็อกโดยปริยายทำงานโดยการเรียก yield คีย์เวิร์ดในเมธอด yield คีย์เวิร์ดเป็นพิเศษ ค้นหาและเรียกบล็อกที่ส่งผ่าน ดังนั้นคุณไม่จำเป็นต้องเพิ่มบล็อกในรายการอาร์กิวเมนต์ที่วิธีการยอมรับ

เนื่องจาก Ruby อนุญาตให้มีการบล็อกโดยปริยาย คุณจึงสามารถเรียกวิธีการทั้งหมดที่มีบล็อกได้ ถ้าไม่เรียก yield บล็อกจะถูกละเว้น

irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]

หากเมธอดที่เรียก ไม่ Yield พบบล็อกที่ส่งผ่านและเรียกด้วยอาร์กิวเมนต์ใดๆ ที่ส่งผ่านไปยัง yield คีย์เวิร์ด

def each
  return to_enum(:each) unless block_given?
 
  i = 0
  while i < size
    yield at(i)
    i += 1
  end
end

ตัวอย่างนี้ส่งคืนอินสแตนซ์ของ Enumerator เว้นแต่จะได้รับบล็อก

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

การบล็อกอย่างชัดแจ้ง

เราสามารถยอมรับการบล็อกในวิธีการได้อย่างชัดเจนโดยเพิ่มเป็นอาร์กิวเมนต์โดยใช้พารามิเตอร์เครื่องหมายและ (ปกติเรียกว่า &block ). เนื่องจากตอนนี้บล็อกมีความชัดเจน เราจึงสามารถใช้ #call เมธอดโดยตรงบนวัตถุผลลัพธ์แทนที่จะอาศัย yield .

&block อาร์กิวเมนต์ไม่ใช่อาร์กิวเมนต์ที่เหมาะสม ดังนั้นการเรียกเมธอดนี้ด้วยอย่างอื่นที่ไม่ใช่บล็อกจะสร้าง ArgumentError .

def each_explicit(&block)
  return to_enum(:each) unless block
 
  i = 0
  while i < size
    block.call at(i)
    i += 1
  end
end

เมื่อบล็อกถูกส่งผ่านแบบนี้และเก็บไว้ในตัวแปร บล็อกนั้นจะถูกแปลงเป็น proc . โดยอัตโนมัติ .

ขั้นตอนการทำงาน

"proc" คือตัวอย่างของ Proc คลาสซึ่งมีโค้ดบล็อกที่จะดำเนินการและสามารถเก็บไว้ในตัวแปรได้ ในการสร้าง proc คุณเรียก Proc.new และส่งผ่านบล็อก

proc = Proc.new { |n| puts "#{n}!" }

เนื่องจาก proc สามารถเก็บไว้ในตัวแปรได้ จึงสามารถส่งผ่านไปยังเมธอดได้เหมือนกับอาร์กิวเมนต์ปกติ ในกรณีนั้น เราจะไม่ใช้เครื่องหมายแอมเพอร์แซนด์ เนื่องจากมีการส่งต่อ proc อย่างชัดเจน

def run_proc_with_random_number(proc)
  proc.call(random)
end
 
proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)

แทนที่จะสร้าง proc แล้วส่งต่อไปยังเมธอด คุณสามารถใช้ไวยากรณ์พารามิเตอร์เครื่องหมายแอมเพอร์แซนด์ของ Ruby ที่เราเห็นก่อนหน้านี้และใช้บล็อกแทนได้

def run_proc_with_random_number(&proc)
  proc.call(random)
end
 
run_proc_with_random_number { |n| puts "#{n}!" }

สังเกตเครื่องหมายและที่เพิ่มไปยังอาร์กิวเมนต์ในเมธอด การดำเนินการนี้จะแปลงบล็อกที่ส่งผ่านเป็นอ็อบเจ็กต์ proc และเก็บไว้ในตัวแปรในขอบเขตเมธอด

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

#to_proc

สัญลักษณ์ แฮช และเมธอดสามารถแปลงเป็น proc ได้โดยใช้ #to_proc วิธีการ การใช้สิ่งนี้บ่อยครั้งคือการส่งต่อ proc ที่สร้างจากสัญลักษณ์ไปยังเมธอด

[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }

ตัวอย่างนี้แสดงวิธีการเรียก #to_s . ที่เทียบเท่ากัน 3 วิธี ในแต่ละองค์ประกอบของอาร์เรย์ ในอันแรก สัญลักษณ์ที่นำหน้าด้วยเครื่องหมายและจะถูกส่งต่อ ซึ่งจะแปลงเป็น proc โดยอัตโนมัติโดยเรียก #to_proc กระบวนการ. สองรายการสุดท้ายแสดงให้เห็นว่า proc นั้นเป็นอย่างไร

class Symbol
  def to_proc
    Proc.new { |i| i.send(self) }
  end
end

แม้ว่านี่จะเป็นเพียงตัวอย่างแบบง่าย แต่การใช้งาน Symbol#to_proc แสดงให้เห็นว่าเกิดอะไรขึ้นภายใต้ประทุน เมธอดส่งคืน proc ซึ่งรับอาร์กิวเมนต์หนึ่งตัวแล้วส่ง self กับมัน ตั้งแต่ self เป็นสัญลักษณ์ในบริบทนี้ โดยเรียก Integer#to_s วิธีการ

แลมบ์ดาส

แลมบ์ดาเป็นปัจจัยสำคัญที่มีปัจจัยแตกต่างบางประการ พวกเขาเป็นเหมือนวิธีการ "ปกติ" ในสองวิธี:พวกเขาบังคับใช้จำนวนอาร์กิวเมนต์ที่ส่งผ่านเมื่อมีการเรียกและใช้การส่งคืน "ปกติ"

เมื่อเรียกแลมบ์ดาที่คาดหวังอาร์กิวเมนต์โดยไม่มีอาร์กิวเมนต์ หรือหากคุณส่งอาร์กิวเมนต์ไปยังแลมบ์ดาที่ไม่คาดหมาย Ruby จะเพิ่ม ArgumentError .

irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
        from (irb):8:in `block in irb_binding'
        from (irb):8
        from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'

นอกจากนี้แลมบ์ดายังปฏิบัติต่อคีย์เวิร์ด return ในลักษณะเดียวกับเมธอด เมื่อเรียกใช้ proc โปรแกรมจะควบคุมบล็อกโค้ดใน proc ดังนั้น ถ้า proc กลับมา ขอบเขตปัจจุบันจะกลับมา หาก proc ถูกเรียกใช้ภายในฟังก์ชันและเรียก return , ฟังก์ชันจะกลับทันทีเช่นกัน

def return_from_proc
  a = Proc.new { return 10 }.call
  puts "This will never be printed."
end

ฟังก์ชันนี้จะให้การควบคุมกับ proc ดังนั้นเมื่อกลับมา ฟังก์ชันจะส่งคืน การเรียกใช้ฟังก์ชันในตัวอย่างนี้จะไม่พิมพ์ผลลัพธ์และคืนค่า 10

def return_from_lambda
  a = lambda { return 10 }.call
  puts "The lambda returned #{a}, and this will be printed."
end

เมื่อใช้แลมบ์ดา จะ จะพิมพ์ กำลังโทร return ในแลมบ์ดาจะมีพฤติกรรมเหมือนโทรกลับ return ในวิธีการ ดังนั้น a ตัวแปรถูกเติมด้วย 10 และบรรทัดจะถูกพิมพ์ไปที่คอนโซล

บล็อก โปรค และแลมบ์ดา

ตอนนี้เราได้เข้าไปในทั้งสองบล็อก procs และ lambdas แล้ว เรามาย่อและสรุปการเปรียบเทียบกัน

  • มีการใช้บล็อคอย่างกว้างขวางใน Ruby เพื่อส่งบิตของโค้ดไปยังฟังก์ชันต่างๆ โดยใช้ yield คีย์เวิร์ด บล็อกสามารถส่งผ่านโดยปริยายโดยไม่ต้องแปลงเป็น proc
  • เมื่อใช้พารามิเตอร์ที่นำหน้าด้วยเครื่องหมายแอมเพอร์แซนด์ การส่งผ่านบล็อกไปยังเมธอดจะส่งผลให้เกิด proc ในบริบทของเมธอด Procs ทำงานเหมือนบล็อก แต่สามารถเก็บไว้ในตัวแปรได้
  • แลมบ์ดาคือ procs ที่ทำงานเหมือนเมธอด ซึ่งหมายความว่าพวกมันบังคับใช้ arity และส่งคืนเป็นเมธอด แทนที่จะอยู่ในขอบเขตพาเรนต์

นี่เป็นการสรุปลักษณะการปิดของเราใน Ruby มีอะไรอีกมากมายให้เรียนรู้เกี่ยวกับการปิด เช่น ขอบเขตคำศัพท์และการผูก แต่เราจะเก็บไว้สำหรับตอนต่อไป ในระหว่างนี้ โปรดแจ้งให้เราทราบว่าคุณต้องการอ่านเกี่ยวกับอะไรในอนาคตของ Ruby Magic การปิดหรืออื่นๆ ที่ @AppSignal