轨道
/
Crystal
Crystal
/
练习
/
任务处理器
任务处理器

任务处理器

学习练习

简介

块是一段可以传给方法的代码。 在其他编程语言中,这被称为匿名函数或 lambda。 方法可以把块转换成 Proc,Proc 表示一段代码。

def my_method(&block)
  1 + yield
end

my_method { 2 }
# => 3

块和 Proc 在 Crystal 中非常常用,比如用于迭代和回调。 它们也常用来创建 DSL(领域特定语言),DSL 的例子包括 Web 框架 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是一个关键字,用来执行传给方法的块。 也就是说,如果没有调用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,可以使用 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 传给方法

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 }

说明

你正在为一个托管文章、博客文章和其他文字内容的网站开发内容管理系统。 你的任务是实现一个任务处理器,用来处理优先队列中的任务。 这个任务处理器会接收任务,并根据服务器的负载和这些任务的优先级,决定接下来执行哪个任务。 如果服务器负载很高,那么任务的优先级数字必须更高,才会被执行。 服务器会根据负载动态地改变负载。

1. 初始化TaskHandler

需要用队列和一个默认的 load_level 来初始化 TaskHandler。 实现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>>

2. 更新任务条件逻辑

服务器必须能够根据服务器的负载来更新任务条件逻辑。

实现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>>

3. 执行下一个任务

服务器必须能够执行队列中的下一个任务。

@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"
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Crystal Exercism

准备好开始 任务处理器 了吗?

注册 Exercism,借助 26 个概念142 个练习 和真人导师指导,学习并掌握 Crystal,全部免费。