轨道
/
Crystal
Crystal
/
练习
/
厨房计算器
厨房计算器

厨房计算器

学习练习

简介

一个元组是一个有限的、有序的、不可变的元素列表。元组要求每个位置都有固定的类型。这也意味着编译器知道每个位置上是什么类型。元组中各个位置可以有不同的类型,但这些类型必须在编译时确定。

创建元组

根据元组中值的类型能否在编译时推断出来,可以用不同的方式创建元组。如果这些值在编译时已知,就可以使用元组字面量语法创建元组;否则需要显式声明类型。同样重要的是,值的类型要与元组中指定的类型匹配,值的个数也要与指定的类型个数匹配。下面是通过元组字面量语法定义的例子:

tuple = {1, "foo", 'c'} # Tuple(Int32, String, Char)

也可以使用 Tuple 类来创建元组。

tuple = Tuple(Int32, String, Char).new(1, "foo", 'c')

你还可以显式指定赋给元组的变量的类型。

tuple : Tuple(Int32, String, Char) = {1, "foo", 'c'}

显式指定元组的类型很有用,因为这样可以定义某个位置存放联合类型。也就是说,一个位置可以存放多种类型。

tuple : Tuple(Int32 | String, String, Char) = {1, "foo", 'c'}

转换

从数组创建元组

你可以使用 Tuple 类的 from 方法从数组创建元组。这要求指定元组的类型。

array = [1, "foo", 'c']
tuple = Tuple(Int32, String, Char).from(array)

转换为数组

你可以使用 to_a 方法把元组转换为数组。得到的数组,其元素类型是元组中各个字段类型的联合。

tuple = {1, "foo", 'c'}
array = tuple.to_a
array # => [1, "foo", 'c']

访问元素

和数组一样,元组的下标从零开始,也就是说第一个元素的下标是 0。但与数组不同的是,每个元素的类型都是固定的,并且在编译时已知,因此对元组取下标时,元素的类型取决于具体位置。要访问元组中的元素,可以使用 [] 运算符。

array = [1, "foo", 'c']
array[0]         # => 1
typeof(array[0]) # => Int32 | String | Char

tuple = {1, "foo", 'c'}
tuple[0]         # => 1
typeof(tuple[0]) # => Int32

访问数组元素时还有一个不同之处:如果下标是写死的,编译器会检查该下标是否在元组范围内。这意味着你会得到编译时错误,而不是运行时错误。

tuple = {1, "foo", 'c'}
tuple[3]
# => Error: index out of bounds for Tuple(Int32, String, Char) (3 not in -3..2)

但是,如果下标存放在变量里,编译器就无法在编译时检查该下标是否在元组范围内,这时会改为给出运行时错误。

子元组

使用带范围的 [] 运算符,可以取得元组的一个子元组。返回的是一个新元组,包含指定范围内的元素。范围必须在编译时给出,否则编译器无法知道子元组中元素的类型。因此范围必须是范围字面量,不能赋给变量。

tuple = {1, "foo", 'c'}
subtuple = tuple[0..1] # Tuple(Int32, String)

i = 0..1
tuple[i]
# Error: Tuple#[](Range) can only be called with range literals known at compile-time

何时使用元组

当你想把固定数量的值组合在一起,并且这些值的类型在编译时已知时,元组很有用。这是因为元组不可变,所以比数组占用更少内存,速度也更快。另一个使用场景是从方法中返回多个值。如果这些值的类型各不相同,元组尤其方便,因为元组中每个位置都可以有不同的类型。

当需要的数据结构会增长或缩小,或者经常需要修改时,就不应该使用元组。

符号是具名的标识符,可以用来引用某个值。符号通过符号字面量创建,也就是在名称前加上 : 字符,例如 :foo。符号也可以用引号写,例如 :"foo",这样名称中就可以包含空格。

:foo           # => :foo
:"foo boo"     # => :"foo boo"
:foo == :"foo" # => true

符号在语言中的许多地方都会用到,包括作为 NamedTuple 的键,以及表示方法和变量的名称。

Crystal 中的符号

Crystal 中的符号和 Ruby 差别很大。在 Crystal 中,符号是一种常量,因此在编译时就被赋值。这意味着符号无法动态创建,而在 Ruby 中这是可以的。

Crystal 中的符号用 Int32 表示,因此非常高效。

转换

符号可以转换为字符串,但反过来不行。这是因为符号在编译时创建,而字符串在运行时创建。

:foo.to_s # => "foo"

说明

在准备给朋友们烤饼干时,你发现食谱里的某些用量需要换算。你只熟悉公制,所以得想个办法,把美国烘焙中常见的计量单位换算成毫升(mL),好让自己用起来方便些。

解答时请使用下面这张换算表:

要换算的单位 容积 毫升(mL)
mL 1 1
美制杯 1 240
美制液量盎司 1 30
美制茶匙 1 5
美制汤匙 1 15

作为一名有天赋的程序员学员,你决定用毫升作为中间单位,方便把表中任意一个单位换算成另一个单位(甚至它自己)。

1. 从体积对中取出数值部分

实现KitchenCalculator.get_volume方法。 给定一个体积对元组,它应该只返回其中的数值部分。

KitchenCalculator.get_volume({:cup, 2.0})
# => 2.0

2. 把体积对换算成毫升

实现KitchenCalculator.to_milliliter方法。 给定一个体积对元组,它应该使用换算表把体积换算成毫升。

KitchenCalculator.to_milliliter({:cup, 2.5})
# => {:milliliter, 600.0}

3. 把毫升体积对换算成其他单位

实现KitchenCalculator.from_milliliter方法。 给定一个体积对元组和目标单位,它应该使用换算表把体积换算成目标单位。

KitchenCalculator.from_milliliter({:milliliter, 1320.0}, :cup)
# => {:cup, 5.5}

4. 在任意单位之间换算

实现KitchenCalculator.convert方法。 给定一个体积对元组和目标单位,它应该把给定的体积换算成目标单位。

KitchenCalculator.convert({:teaspoon, 9.0}, :tablespoon)
# => {:tablespoon, 3.0}
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Crystal Exercism

准备好开始 厨房计算器 了吗?

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