區塊是一段可以傳遞給方法的程式碼。 在其他程式語言中,這被稱為匿名函式或 lambda。 方法可以將區塊轉換成 Proc,它代表一段程式碼。
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
區塊和 Proc 在 Crystal 中非常常用,例如用於疊代和回呼。 不過也能用來建立 DSL(領域特定語言),DSL 的例子包括網頁框架 Kemal 或測試框架 Spec(這正是用來測試你的 Crystal 解答的工具)。
若要建立可接受區塊的方法,你需要在最後一個引數前加上 & 符號。
這會將區塊轉換成 Proc 並指定給該引數。
接著使用 yield 就會執行傳給該方法的區塊。
def my_method(&block)
1 + yield
end
傳遞區塊有兩種方式:使用 do ... end 關鍵字,或使用大括號({})。
兩者的主要差異在於優先順序:do ... end 支援多行區塊,而大括號({})則不支援。
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yield是一個關鍵字,會執行傳給該方法的區塊。
這表示如果沒有呼叫 yield,區塊就永遠不會被執行。
它也可以接收引數,這些引數會傳遞給區塊。
可以傳給區塊的引數數量沒有限制。
若要在區塊中使用這些引數,可以使用 |x, y, ...| 語法。
其中 x、y、…… 代表引數的名稱。
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
在上面的範例中,區塊會收到引數 2,區塊將它乘以 3 並回傳 6。
接著方法會將 1 加到結果上並回傳 7。
如果傳給 yield 的引數數量多於區塊預期的引數數量,就會引發錯誤。
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
區塊可以明確或隱含地撰寫。 隱含區塊最為常見,上面範例中展示的就是這種。 你甚至可以讓區塊接收不同型別的引數,編譯器會推斷這些型別並建立聯合型別。
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
撰寫明確區塊時,你可以指定引數的型別和回傳型別。
這是透過 Arg1Type, Arg2Type, ... -> ResultType 語法完成的。
其中引數以逗號(,)分隔,結果型別則以 -> 分隔。
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
與其他明確型別一樣,編譯器會強制型別符合預期。 如果區塊與傳給方法的型別不符,就會引發錯誤。
Proc與區塊有非常密切的關聯。
關鍵差異在於 Proc是一種型別,而區塊不是。
Proc代表函式指標,可以像變數一樣四處傳遞。
若要定義 Proc,可以使用 proc 字面量 ->(args) { ... } 語法。
引數以逗號(,)分隔,區塊則以 { ... } 分隔。
若要呼叫 Proc,可以使用 call 方法並將引數傳給它。
參數的型別是必需的。
回傳型別會從 proc 的主體推斷,但也可以明確提供。
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
捕捉區塊是一種從區塊建立 Proc 的方式。
這是在方法的最後一個引數前使用 & 運算子來完成的。
這會將區塊轉換成 Proc 並指定給該引數。
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Proc 可以傳給預期接收區塊的方法。 當你想根據某些條件傳遞不同的區塊時,這會很方便。
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
宣告變數時,你可以使用 : Type 語法來宣告變數的型別。
對於 proc,則使用 Proc((ArgType, ...), ResultType) 語法。
最後一個型別一定是回傳型別,其餘的則是引數型別。
也就是說,如果只有一個型別,它就是回傳型別,而該 proc 不接受任何引數。
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
你正在為一個網站開發內容管理系統,這個網站會刊登文章、部落格文章以及其他文字內容。 你的任務是實作一個任務處理器,用來處理優先佇列中的任務。 任務處理器會接收任務,並根據伺服器的負載與任務的優先級,決定接下來要執行哪個任務。 如果伺服器負載很重,任務的優先級數字就必須更高,才會被執行。 伺服器會根據負載動態調整負載。
TaskHandler
TaskHandler 需要以佇列和預設的 load_level 來初始化。
實作 TaskHandler 類別的初始化方法,讓它接受一個由任務名稱組成的 String Array 作為引數,並將其存放在名為 @tasks 的實例變數中。
初始化方法也應該建立實例變數 @task_condition_logic,用來存放一個 Proc,這個 Proc 接受一個 Int32,並在引數等於或大於 0 時回傳 true,小於 0 時回傳 false。
task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>
伺服器必須能夠根據伺服器的負載更新任務條件邏輯。
實作 TaskHandler#update_task_condition_logic 方法,讓它接受一個區塊作為引數,這個區塊應該存放在實例變數 @task_condition_logic 中。
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
# => #<TaskHandler:0x7f068118cf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562df99d0370>>
伺服器必須能夠執行佇列中的下一個任務。
@tasks 陣列中的第一個任務就是下一個要執行的任務。
如果將任務的優先級傳入 @task_condition_logic proc 後回傳 true,就應該執行該任務。
它會根據任務是否被執行回傳對應的訊息。
如果任務被執行,應該回傳:"Completed <task>"。
如果任務沒有被執行,應該回傳:"Could not complete <task>"。
如果任務被執行,就應該從 @tasks 陣列中移除。
實作 TaskHandler#execute_next_task 方法,讓它接受下一個任務的優先級 Int32 作為引數,並根據任務是否被執行回傳 String。
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
task_handler.execute_next_task(2)
# => "Completed task1"
task_handler.execute_next_task(1)
# => "Could not complete task2"