农场

农场

学习练习

简介

在理想情况下,一切都能正常运行。但在现实世界里,事情难免出错,而如何处理这些情况,直接决定了我们的软件是否健壮可靠。异常是编程中的一个关键概念,它让我们能够从容地处理错误和意外情况。

抛出错误后,如果不加处理,程序就会中止并输出一条错误信息。大多数情况下,你不希望程序一出错就中止,而是希望处理这个错误,让程序继续运行。

抛出异常

在 Crystal 中,异常通过raise关键字抛出,它既可以接收一个String,也可以接收一个Exception对象。如果没有处理,程序会中止并打印错误信息。

raise "This is an error"

Crystal 内置了多种异常,例如ArgumentError、IndexError、KeyError、IOError、SystemCallError、TypeError、ZeroDivisionError等等。这些异常要求你为它传入一条消息。

raise ArgumentError.new("This is an argument error")

处理异常

我们不希望程序在抛出异常时崩溃。因此,当我们知道某段代码容易出错时,可以把它包在begin块里,再用rescue块捕获异常。begin块标记了可能抛出异常的代码的起点,rescue块则负责处理异常。

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
end

rescue块还可以指定一个变量,用来获取异常对象。

begin
 raise "This is an error"
rescue ex
 puts "An error occurred: #{ex.message}"
end

rescue块也可以只指定某种具体的异常类型,从而只捕获该类型的异常。

begin
 raise ArgumentError.new("This is an argument error")
rescue ArgumentError
 puts "An argument error occurred!"
end

# or

begin
 raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
 puts "An argument error occurred: #{ex.message}"
end

可以用多个rescue块来处理不同类型的异常。在下面的例子中,第一个rescue块会捕获ArgumentError,第二个rescue块则会捕获其他任何异常。

begin
 raise ArgumentError.new("This is an argument error")
rescue ArgumentError
 puts "An argument error occurred!"
rescue
 puts "An error occurred!"
end

begin块还可以带一个else块,当没有异常抛出时就会执行它。

begin
 puts "No error occurred"
rescue
 puts "An error occurred!"
else
 puts "No error occurred"
end

最后还有一个ensure块,无论是否抛出异常,它都一定会执行。

begin
 raise "This is an error"
rescue
 puts "An error occurred!"
ensure
 puts "This is always executed"
end

方法约定

有些方法有两个版本,一个带!,另一个不带。这可能表示两种不同的含义:一是该方法会修改对象,二是该方法可能抛出异常。

不过,布尔概念中还提到了另一个约定,即以?结尾的方法。有些方法默认会抛出异常,但也提供一个以?结尾的版本,它不抛出异常,而是返回nil。

当你希望避免抛出错误时,这种方式很理想。它还有利于性能,因为不必生成堆栈跟踪,而且如果使用得当,还能让代码更安全。

自定义异常

你也可以通过继承 Exception 类来创建自己的异常。这样做时,你可以选择重写initialize方法,以设置异常消息。具体做法是给名为@message的实例变量赋上该消息。

class MyException < Exception
 def initialize
   @message = "This is my exception"
 end
end

raise MyException.new

说明

你期盼已久的这一天终于到来了,现在你自豪地成为了阿尔卑斯山上一座美丽农场的主人。

不过,你仍然不喜欢一大早起床去喂牛。 因为你是一名出色的工程师,所以你造了一台自动喂食器,就叫FEED-M-ALL。

完成这个项目还差最后一步:一段计算每头牛应该分到多少饲料的代码。 每头牛分到的量必须相同,否则会引发冲突。 牛可是非常敏感的。

幸运的是,你不需要自己推导计算饲料用量的所有公式。 你用了一个从网上找到的神秘外部库,据说它能让牛过上最幸福的生活。 这个库对外暴露了一个类,它实现了以下接口。 你自己写的代码会依赖它。

class FodderCalculator
  def fodder_amount! : Number 
  end

  def fattening_factor! : Number
  end
end

在编写代码的过程中,你会逐步改进错误处理,让代码更健壮,日后在日常农场生活中使用时也更容易调试。

1. 平均分配食物

首先,你专注于编写计算每头牛饲料用量所需的代码。

实现一个方法TheFarm.divide_food,它的参数是一个FodderCalculator对象和一个表示牛数量的整数。 在这个任务中,你可以假定传入的牛的数量总是大于 0。 该方法应返回每头牛的食物量。

要进行计算,你首先需要获取所有牛的总饲料量。 这通过调用fodder_amount!方法并传入牛的数量来完成。 此外,你还需要一个系数,用来乘以这个总量。 这个系数可以通过调用fattening_factor!方法获得。 有了这两个值和牛的数量,你现在就可以计算出每头牛的食物量(类型为float64)。

如果你调用的某个方法返回错误,执行应立即停止并返回0。

fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0

fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0

2. 检查牛的数量

在完成上面第一个任务的过程中,你发现所用的外部库并没有你想象中那么高质量。 比如,它无法正确处理无效输入。 你想在自己的代码里加一道输入检查来绕过这个限制。

编写一个函数TheFarm.validate_input_and_divide_food,它的签名与上面的TheFarm.divide_food相同。

  • 如果传入的牛的数量大于 0,函数应调用TheFarm.divide_food并返回该调用的结果。
  • 如果牛的数量小于等于 0,函数应抛出错误,错误消息为"Number of cows must be greater than 0"。
TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15

TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0

3. 改进错误处理

在传入之前先检查牛的数量,这个做法很不错,但你对那条含糊的错误消息不太满意。 你决定做得更好一些,创建一个名为InvalidCowsError的自定义错误类。

这个自定义错误应保存牛的数量(Int32)和一条自定义消息(String),而Expection方法应按以下格式序列化数据:

{number of cows} cows are invalid: {custom message}

有了自定义错误,实现一个函数TheFarm.validate_number_of_cows,它接受整数形式的牛的数量,并返回一个错误(或 nil)。

  • 如果牛的数量小于 0,函数返回一个InvalidCowsError,其自定义消息为"there are no negative cows"。
  • 如果牛的数量为 0,函数返回一个InvalidCowsError,其自定义消息为"no cows don't need food"。
  • 其他情况下,函数返回nil,表示验证通过。
TheFarm.validate_number_of_cows(5)
# => nil

TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows

辛辛苦苦搭好这个验证函数之后,你发现天色已晚,于是离开书桌,去欣赏山间的日落。 至于真正把这个新的验证函数加进代码里,你决定改天再说。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Crystal Exercism

准备好开始 农场 了吗?

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