區塊與程序¶
方法可以接受一段程式碼區塊,該程式碼區塊會使用 yield
關鍵字執行。例如
def twice(&)
yield
yield
end
twice do
puts "Hello!"
end
以上程式會印出 "Hello!" 兩次,每次 yield
一次。
要定義一個接收區塊的方法,只需在其內部使用 yield
,編譯器就會知道。您可以宣告一個虛擬的區塊參數,以 & 符號 (&
) 為首碼的最後一個參數來使其更加明顯。在上面的範例中,我們這樣做了,使引數匿名(僅寫入 &
)。但它可以給定一個名稱
def twice(&block)
yield
yield
end
在此範例中,區塊參數名稱並不重要,但在更進階的使用中會很重要。
要叫用方法並傳遞區塊,您可以使用 do ... end
或 { ... }
。所有這些都是等效的
twice() do
puts "Hello!"
end
twice do
puts "Hello!"
end
twice { puts "Hello!" }
使用 do ... end
和 { ... }
之間的差異在於 do ... end
會繫結到最左邊的呼叫,而 { ... }
會繫結到最右邊的呼叫
foo bar do
something
end
# The above is the same as
foo(bar) do
something
end
foo bar { something }
# The above is the same as
foo(bar { something })
這樣做的原因是允許使用 do ... end
來建立網域特定語言 (DSL),使其讀起來像簡單的英語
open file "foo.cr" do
something
end
# Same as:
open(file("foo.cr")) do
something
end
您不會希望以上內容是
open(file("foo.cr") do
something
end)
重載¶
兩個方法,一個產生值而另一個不產生值,會被視為不同的重載,如 重載 一節所述。
Yield 引數¶
yield
表達式類似於呼叫,可以接收引數。例如
def twice(&)
yield 1
yield 2
end
twice do |i|
puts "Got #{i}"
end
以上會印出 "Got 1" 和 "Got 2"。
也有大括號表示法可用
twice { |i| puts "Got #{i}" }
您可以 yield
許多值
def many(&)
yield 1, 2, 3
end
many do |x, y, z|
puts x + y + z
end
# Output: 6
區塊可以指定比產生值的引數更少的參數
def many(&)
yield 1, 2, 3
end
many do |x, y|
puts x + y
end
# Output: 3
指定比產生值的引數更多的區塊參數是錯誤的
def twice(&)
yield
yield
end
twice do |i| # Error: too many block parameters
end
每個區塊參數都具有該位置中每個 yield 表達式的型別。例如
def some(&)
yield 1, 'a'
yield true, "hello"
yield 2, nil
end
some do |first, second|
# first is Int32 | Bool
# second is Char | String | Nil
end
底線 也允許作為區塊參數
def pairs(&)
yield 1, 2
yield 2, 4
yield 3, 6
end
pairs do |_, second|
print second
end
# Output: 246
單一參數簡短語法¶
如果區塊只有一個參數並在其上叫用方法,則該區塊可以用簡短語法引數取代。
這個
method do |param|
param.some_method
end
和
method { |param| param.some_method }
都可以寫成
method &.some_method
或像這樣
method(&.some_method)
在任一情況下,&.some_method
都是傳遞給 method
的引數。此引數在語法上等同於區塊變體。它只是語法糖,沒有任何效能上的損失。
如果方法有其他必要的引數,則簡短語法引數也應在方法的引數清單中提供。
["a", "b"].join(",", &.upcase)
等同於
["a", "b"].join(",") { |s| s.upcase }
引數也可以與簡短語法引數一起使用
["i", "o"].join(",", &.upcase(Unicode::CaseOptions::Turkic))
也可以叫用運算子
method &.+(2)
method(&.[index])
yield 值¶
yield
表達式本身具有一個值:區塊的最後一個表達式。例如
def twice(&)
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
i + 1
end
以上會印出 "2" 和 "3"。
yield
表達式的值主要用於轉換和篩選值。這方面最好的範例是 Enumerable#map 和 Enumerable#select
ary = [1, 2, 3]
ary.map { |x| x + 1 } # => [2, 3, 4]
ary.select { |x| x % 2 == 1 } # => [1, 3]
虛擬轉換方法
def transform(value, &)
yield value
end
transform(1) { |x| x + 1 } # => 2
最後一個表達式的結果是 2
,因為 transform
方法的最後一個表達式是 yield
,其值是區塊的最後一個表達式。
型別限制¶
可以使用 &block
語法來限制使用 yield
的方法中區塊的型別。例如
def transform_int(start : Int32, &block : Int32 -> Int32)
result = yield start
result * 2
end
transform_int(3) { |x| x + 2 } # => 10
transform_int(3) { |x| "foo" } # Error: expected block to return Int32, not String
break¶
區塊內的 break
表達式會提早從方法中退出
def thrice(&)
puts "Before 1"
yield 1
puts "Before 2"
yield 2
puts "Before 3"
yield 3
puts "After 3"
end
thrice do |i|
if i == 2
break
end
end
以上會印出 "Before 1" 和 "Before 2"。thrice
方法由於 break
的關係,沒有執行 puts "Before 3"
表達式。
break
也可以接受引數:這些會變成方法的回傳值。例如
def twice(&)
yield 1
yield 2
end
twice { |i| i + 1 } # => 3
twice { |i| break "hello" } # => "hello"
第一個呼叫的值是 3,因為 twice
方法的最後一個表達式是 yield
,它會取得區塊的值。第二個呼叫的值是 "hello",因為已執行 break
。
如果有條件的 break,則呼叫的回傳值型別會是區塊值的型別和許多 break
的型別的聯集
value = twice do |i|
if i == 1
break "hello"
end
i + 1
end
value # :: Int32 | String
如果 break
收到許多引數,它們會自動轉換為 Tuple
values = twice { break 1, 2 }
values # => {1, 2}
如果 break
沒有收到引數,則與收到單一 nil
引數相同
value = twice { break }
value # => nil
如果在多個巢狀區塊中使用 break
,則只會跳脫最接近的封閉區塊
def foo(&)
pp "before yield"
yield
pp "after yield"
end
foo do
pp "start foo1"
foo do
pp "start foo2"
break
pp "end foo2"
end
pp "end foo1"
end
# Output:
# "before yield"
# "start foo1"
# "before yield"
# "start foo2"
# "end foo1"
# "after yield"
請注意,您不會取得兩個 "after yield"
也不會取得 "end foo2"
。
next¶
區塊內的 next
表達式會提早從區塊 (而非方法) 中退出。例如
def twice(&)
yield 1
yield 2
end
twice do |i|
if i == 1
puts "Skipping 1"
next
end
puts "Got #{i}"
end
# Output:
# Skipping 1
# Got 2
next
表達式接受引數,這些引數會給出叫用區塊的 yield
表達式的值
def twice(&)
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
if i == 1
next 10
end
i + 1
end
# Output
# 10
# 3
如果 next
收到許多引數,它們會自動轉換為 Tuple。如果它沒有收到引數,則與收到單一 nil
引數相同。
with ... yield¶
可以使用 with
關鍵字來修改 yield
表達式,以指定一個物件作為區塊中方法呼叫的預設接收者
class Foo
def one
1
end
def yield_with_self(&)
with self yield
end
def yield_normally(&)
yield
end
end
def one
"one"
end
Foo.new.yield_with_self { one } # => 1
Foo.new.yield_normally { one } # => "one"
解包區塊參數¶
區塊參數可以指定括在括號中的子參數
array = [{1, "one"}, {2, "two"}]
array.each do |(number, word)|
puts "#{number}: #{word}"
end
以上只是這個的語法糖
array = [{1, "one"}, {2, "two"}]
array.each do |arg|
number = arg[0]
word = arg[1]
puts "#{number}: #{word}"
end
這表示任何以整數回應 []
的型別都可以在區塊參數中解包。
參數解包可以是巢狀的。
ary = [
{1, {2, {3, 4}}},
]
ary.each do |(w, (x, (y, z)))|
w # => 1
x # => 2
y # => 3
z # => 4
end
支援 Splat 參數。
ary = [
[1, 2, 3, 4, 5],
]
ary.each do |(x, *y, z)|
x # => 1
y # => [2, 3, 4]
z # => 5
end
對於 Tuple 參數,您可以利用自動 Splat,而不需要括號
array = [{1, "one", true}, {2, "two", false}]
array.each do |number, word, bool|
puts "#{number}: #{word} #{bool}"
end
Hash(K, V)#each 將 Tuple(K, V)
傳遞給區塊,因此迭代鍵值對會使用自動 Splat
h = {"foo" => "bar"}
h.each do |key, value|
key # => "foo"
value # => "bar"
end
效能¶
當使用帶有 yield
的區塊時,區塊會**永遠**內嵌:不會涉及閉包、呼叫或函式指標。這表示這個
def twice(&)
yield 1
yield 2
end
twice do |i|
puts "Got: #{i}"
end
與寫這個完全相同
i = 1
puts "Got: #{i}"
i = 2
puts "Got: #{i}"
舉例來說,標準函式庫在整數型別中包含了 times
方法,讓你可以這樣寫:
3.times do |i|
puts i
end
這看起來很炫,但它有像 C 語言的 for 迴圈一樣快嗎?答案是:是的!
這是 Int#times
的定義:
struct Int
def times(&)
i = 0
while i < self
yield i
i += 1
end
end
end
由於沒有捕捉變數的區塊永遠會被內聯,所以上面的方法調用與寫成這樣完全相同:
i = 0
while i < 3
puts i
i += 1
end
不用害怕為了可讀性或程式碼重複使用而使用區塊,這不會影響最終可執行檔的效能。