學習軌道
/
Ruby
Ruby
/
練習
/
火車駕駛員
火車駕駛員

火車駕駛員

學習練習

簡介

解構是指從集合(例如 Array 或 Hash)中取出元素的動作。解構後的值接著可以在同一個敘述中指定給變數。

多重賦值是指能在同一個敘述中,把解構後的值指定給多個變數。這能讓程式碼更簡潔、更好讀,做法是用逗號分隔要指定的變數,例如 first, second, third = [1, 2, 3]。

splat 運算子(*)和雙 splat 運算子(**)經常用於解構的情境中。

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

這也稱為「Parallel Assignment」,可以用來避免使用暫時變數。

如果變數比值多,多出來的變數會被指定為 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 '['

在這裡實驗一下,你會發現決定結果的是第一個模式,而不是右邊可用的值。語法錯誤和資料結構本身無關。

用單一 splat 運算子(*)解構陣列

解構陣列時,可以用 splat 運算子(*)捕捉「剩下的」值。這比切分陣列(在某些情況下比較不好讀)更清楚。例如,我們可以取出第一個元素,然後把剩下的值指定到一個不含第一個元素的新陣列:

>> 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 個以上的陣列/雜湊進行合併。

用 splat 運算子(*)組合陣列

組合陣列可以用 splat 運算子(*)來完成。這會把所有值打包成一個陣列。

>> 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"]

用雙 splat 運算子(**)組合雜湊

組合雜湊是用雙 splat 運算子(**)來完成。這會把一個雜湊中所有的鍵/值配對打包進另一個雜湊,或把兩個雜湊合併在一起。

>> 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}

splat 運算子(*)與雙 splat 運算子(**)搭配方法的使用方式

搭配方法參數的組合

當你建立一個接受任意數量引數的方法時,可以在方法定義中使用 *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)

解構進方法呼叫

你可以用 splat 運算子(*)把一組引數的陣列拆解進方法呼叫:

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

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

你也可以用雙 splat 運算子(**)把一組引數的雜湊拆解進方法呼叫:

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,全部免費。