轨道
/
Ruby
Ruby
/
练习
/
火车司机
火车司机

火车司机

学习练习

简介

解构是指从集合(例如Array或Hash)中提取元素的操作。解构出来的值可以在同一条语句里赋给变量。

多重赋值是指在一个语句中把多个变量赋给解构出的值的能力。它能让代码更简洁、更易读,做法是用逗号分隔要赋值的变量,例如first, second, third = [1, 2, 3]。

展开运算符(*)和双展开运算符(**)常用于解构场景。

Caution

*<variable_name>和**<variable_name>不应与*和**混淆。*和**分别用于乘法和幂运算,而*<variable_name>和**<variable_name>则用作组合和解构运算符。

多重赋值

多重赋值让你可以在一行中给多个变量赋值。用逗号,分隔各个值:

>> a, b = 1, 2
=> [1, 2]
>> a
=> 1

多重赋值并不局限于一种数据类型:

>> x, y, z = 1, "Hello", true
=> [1, "Hello", true]
>> x
=> 1
>> y
=> 'Hello'
>> z
=> true

多重赋值还可以用来交换数组中的元素。这种做法在排序算法中很常见。例如:

>> numbers = [1, 2]
=> [1, 2]
>> numbers[0], numbers[1] = numbers[1], numbers[0]
=> [2, 1]
>> numbers
=> [2, 1]
Note

这也称为“并行赋值”,可以用来避免使用临时变量。

如果变量的数量多于值的数量,多出来的变量会被赋值为nil:

>> a, b, c = 1, 2
=> [1, 2]
>> b
=> 2
>> c
=> nil

解构

在 Ruby 中,可以把数组/哈希中的元素解构成不同的变量。由于值在数组中按下标顺序出现,它们会按同样的顺序解包到变量中:

>> fruits = ["apple", "banana", "cherry"]
>> x, y, z = fruits
>> x
=> "apple"

如果有些值用不到,可以用_表示“已收集但不使用”:

>> fruits = ["apple", "banana", "cherry"]
>> _, _, z = fruits
>> z
=> "cherry"

深层解构

从数组内部的数组(也称为嵌套数组)中解构并赋值,方式与浅层解构相同,但需要用带分隔符的解构表达式(())来明确值的上下文或位置:

>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> (a, b), (c, d) = fruits_vegetables
>> a
=> "apple"
>> d
=> "potato"

你也可以只深层解包嵌套数组的一部分:

>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> a, (c, d) = fruits_vegetables
>> a
=> ["apple", "banana"]
>> c
=> "carrot"

如果解构中变量的位置不对,或者值的数量不对,就会出现语法错误:

fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]

(a, b), (d) = fruits_vegetables
# syntax error, unexpected '=', expecting '.' or &. or :: or '['

((a, b), (d)) = fruits_vegetables
# syntax error, unexpected ')', expecting '.' or &. or :: or '['

在这里动手试试,你会发现起决定作用的是第一个模式,而不是右侧可用的值。这个语法错误与数据结构无关。

用单展开运算符(*)解构数组

在解构数组时,你可以用展开运算符(*)捕获“剩下的”值。这比对数组切片更清晰(切片在某些情况下可读性更差)。例如,我们可以取出第一个元素,再把剩下的值赋给一个不含第一个元素的新数组:

>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *last = fruits
>> x
=> "apple"
>> last
=> ["banana", "cherry", "orange", "kiwi", "melon", "mango"]

也可以取出数组开头和结尾的值,同时把中间的值归成一组:

>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *middle, y, z = fruits
>> y
=> "melon"
>> middle
=> ["banana", "cherry", "orange", "kiwi"]

*在深层解构中同样适用:

>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]]
>> (a, *rest), b = fruits_vegetables
>> a
=> "apple"
>> rest
=> ["banana", "melon"]

解构Hash

解构哈希与解构数组略有不同。要解包哈希,需要先把它转换成数组。否则无法进行解构:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory
>> x
=> {:apple=>6, :banana=>2, :cherry=>3}
>> y
=> nil

要把Hash强制转换成数组,可以使用to_a方法:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> fruits_inventory.to_a
=> [[:apple, 6], [:banana, 2], [:cherry, 3]]
>> x, y, z = fruits_inventory.to_a
>> x
=> [:apple, 6]

如果你想解包键,可以使用keys方法:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.keys
>> x
=> :apple

如果你想解包值,可以使用values方法:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.values
>> x
=> 6

组合

组合是指把多个值归拢成一个数组并赋给某个变量的能力。当你想要_解构_值、做点修改,再把结果_组合_回一个变量时,它很有用。它还让对 2 个或更多数组/哈希的合并成为可能。

用展开运算符(*)组合数组

组合数组可以用展开运算符(*)来完成。它会把所有值打包进一个数组。

>> fruits = ["apple", "banana", "cherry"]
>> more_fruits = ["orange", "kiwi", "melon", "mango"]

# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>> combined_fruits = *fruits, *more_fruits

>> combined_fruits
=> ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]

用双展开运算符(**)组合哈希

组合哈希要用双展开运算符(**)来完成。它会把一个哈希中所有的键/值对打包进另一个哈希,或者把两个哈希合并在一起。

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> more_fruits_inventory = {orange: 4, kiwi: 1, melon: 2, mango: 3}

# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}

# then the pairs are packed into combined_fruits_inventory
>> combined_fruits_inventory
=> {:apple=>6, :banana=>2, :cherry=>3, :orange=>4, :kiwi=>1, :melon=>2, :mango=>3}

展开运算符(*)和双展开运算符(**)在方法中的用法

使用方法形参进行组合

当你要创建一个接受任意数量实参的方法时,可以在方法定义中使用*arguments或**keyword_arguments。*arguments用于打包任意数量的位置(非关键字)实参,**keyword_arguments用于打包任意数量的关键字实参。

*arguments的用法:

# This method is defined to take any number of positional arguments
# (Using the single line form of the definition of a method.)

>> def my_method(*arguments)= arguments

# Arguments given to the method are packed into an array

>> my_method(1, 2, 3)
=> [1, 2, 3]

>> my_method("Hello")
=> ["Hello"]

>> my_method(1, 2, 3, "Hello", "Mars")
=> [1, 2, 3, "Hello", "Mars"]

**keyword_arguments的用法:

# This method is defined to take any number of keyword arguments

>> def my_method(**keyword_arguments)= keyword_arguments

# Arguments given to the method are packed into a dictionary

>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}

如果所定义的方法没有任何用于关键字实参的形参(**keyword_arguments或<key_word>: <value>),那么关键字实参会被打包成一个哈希,并赋给最后一个形参。

>> def my_method(a)= a

>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}

*arguments和**keyword_arguments也可以相互搭配使用:

def my_method(*arguments, **keyword_arguments)
  p arguments.sum
  for (key, value) in keyword_arguments.to_a
    p key.to_s + " = " + value.to_s
  end
end


my_method(1, 2, 3, a: 1, b: 2, c: 3)
6
"a = 1"
"b = 2"
"c = 3"

你也可以在*arguments的前后写实参,以便指定特定的位置实参。这和数组的解构方式一样。

Caution

实参必须按特定的顺序组织:

def my_method(<positional_arguments>, *arguments, <positional_arguments>, <keyword_arguments>, **keyword_arguments)

如果不按这个顺序来,就会出错。

def my_method(a, b, *arguments)
  p a
  p b
  p arguments
end

my_method(1, 2, 3, 4, 5)
1
2
[3, 4, 5]

你可以在*arguments前后写位置实参:

>> def my_method(a, *middle, b)= middle

>> my_method(1, 2, 3, 4, 5)
=> [2, 3, 4]

你也可以把位置实参、*arguments、关键字实参和**keyword_arguments 组合到一起:

>> def my_method(first, *many, last, a:, **keyword_arguments)
     p first
     p many
     p last
     p a
     p keyword_arguments
     end

>> my_method(1, 2, 3, 4, 5, a: 6, b: 7, c: 8)
1
[2, 3, 4]
5
6
{:b => 7, :c => 8}

按错误的顺序写实参会导致错误:

def my_method(a:, **keyword_arguments, first, *arguments, last)
  arguments
end

my_method(1, 2, 3, 4, a: 5)

syntax error, unexpected local variable or method, expecting & or '&'
... my_method(a:, **keyword_arguments, first, *arguments, last)

在方法调用中解构

你可以用展开运算符(*)把一个数组形式的实参解包到方法调用中:

def my_method(a, b, c)
  p c
  p b
  p a
end

numbers = [1, 2, 3]
my_method(*numbers)
3
2
1

你也可以用双展开运算符(**)把一个哈希形式的实参解包到方法调用中:

def my_method(a:, b:, c:)
  p c
  p b
  p a
end

numbers = {a: 1, b: 2, c: 3}
my_method(**numbers)
3
2
1

说明

你的朋友 Linus 是一名火车司机,驾驶货运列车往返于城市之间。 他开火车很在行,但在物流和电脑方面就不太行了。 他想请你用编程帮他整理列车信息,并修正路线数据中的错误。

Note

这道练习用切片、下标和各种 hash 方法就能轻松解决。 不过,我们希望你在完成下面每个任务时,练习打包、解包和多重赋值。

1. 创建所有车厢的数组

你的朋友一直在记录每节车厢的标识符(ID),但他从来不确定系统在任意时刻需要处理多少节车厢。 如果把这些数据打包成一个统一的数组,物流程序的其他部分处理起来会轻松得多。

实现一个方法generate_list_of_wagons(),它接受任意数量的车厢 ID。 每个 ID 都是一个正整数。 然后,该方法应当把这些 ID 作为单个数组return 出去。

LocomotiveEngineer.generate_list_of_wagons(1, 7, 12, 3, 14, 8, 5)
# => [1, 7, 12, 3, 14, 8, 5]

2. 修正车厢数组

到这里,你已经对数据以及它在物流程序中的用法有了初步感觉。 ID 系统总是把火车头的 ID 分配为1,列车中其余车厢则随机分配一个大于1的 ID。

你的朋友需要给列车接上两节新车厢,却忘了更新系统! 现在,列车数组中前两节车厢必须移到末尾,否则所有顺序都会乱掉。

更麻烦的是,你的朋友刚刚又发现了一个数组,里面似乎装着缺失的车厢 ID。 他只记得,新车厢移好之后,第二个数组里的 ID 应当直接放在指定火车头的后面。

如果你能帮他改正这些错误并整合数据,Linus 会非常感激。

实现一个方法fix_list_of_wagons(),它接受两个装有车厢 ID 的数组。 它应当把第一个数组的前两项移到末尾,并把第二个数组中的值插入到火车头 ID(1)的后面(即它的右侧)。 然后,该方法应当 return 修改后的数组。

LocomotiveEngineer.fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15])
# => [1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5]

3. 补充缺失的停靠站

现在车厢数据都正确了,Linus 希望你更新系统的路线信息。 在一段运输路线上,列车可能会在几个不同的车站停靠,装货或卸货。 每趟行程的中间配送点数量可能各不相同。 你的朋友希望你用缺失或额外的配送信息更新系统的路线哈希。

实现一个方法add_missing_stops(),它接受一个路线哈希,后面跟可变数量的关键字参数。 这些参数可以是一个包含一个或多个停靠站的哈希,也可以是任意数量的 stop_<number>: "city" 关键字对。 然后,你的方法应当返回更新后的路线哈希,其中新增一个键,它的值是一个按顺序包含所有新增停靠站的数组。

LocomotiveEngineer.add_missing_stops({from: "New York", to: "Miami"},
                    stop_1: "Washington, DC", stop_2: "Charlotte", stop_3: "Atlanta",
                    stop_4: "Jacksonville", stop_5: "Orlando")
# => {from: "New York", to: "Miami", stops: ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]}
Note

停靠站总是会按正确的顺序传入,所以你不必操心排序。

4. 扩展路线信息

Linus 一直在开发这个路线程序,他发现某些路线缺少一些重要的细节。 初始路线信息已经构造成一个哈希,你的朋友希望你把这个哈希更新,补上所有可能缺失的内容。 系统中每条路线所需的细节都略有不同,所以 Linus 很希望有一个通用的解决方案。

实现一个名为extend_route_information()的方法,它接受两个哈希。 第一个哈希包含这条列车路线往返的起点城市和终点城市。

第二个哈希包含其他路线细节,比如列车速度、长度或温度。 该方法应当返回一个包含所有路线信息的整合哈希。

Note

第二个哈希可以包含与示例中不同的或更多的属性。

LocomotiveEngineer.extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
# => {"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Ruby Exercism

准备好开始 火车司机 了吗?

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