軌道
/
Ruby
Ruby
/
課程大綱
/
多重賦值與解構
多重

多重賦值與解構 在 Ruby

1 個練習

關於 多重賦值與解構

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

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

splat 運算子(*)和 double splat 運算子(**)經常用於解構的情境。 splat 運算子(*)可以把多個陣列合併成一個陣列,方法是把每一個_解構_進一個新的共同陣列。 double splat 運算子(**)可以把多個Hash合併成一個Hash,方法是把每一個_解構_進一個新的共同Hash。 這種語法用來區分把多個值當作位置引數接受的情況,以及接受任意數量關鍵字引數的情況。

當 splat 運算子(*)沒有搭配集合使用時,它會把若干個值_打包_(也就是組合)成一個陣列。 這在多重賦值中很常見到,用來把所有沒有各自賦值的「剩餘」元素蒐集到單一變數裡。

在 Ruby 中,使用或定義可接受任意數量位置引數或關鍵字引數的方法時,經常會用到這種解構/組合的行為。 你經常會看到這些引數定義成 def some_method(*arguments, **keyword_arguments),並以 some_method(*some_array, **some_hash) 這樣的方式傳入。

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

如果值比變數多,多出來的值會被忽略:

>> a, b, c = 1, 2, 3, 4
>> b
=> 2
>> c
=> 3

解構

在 Ruby 中,可以把陣列/Hash的元素解構到不同的變數裡。 由於值在陣列中是按索引順序出現的,它們會以相同順序被拆解到變數中:

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

在這裡試試看,你會發現決定一切的是第一個模式,而不是右邊可用的值喔。 這個語法錯誤和資料結構無關。

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

[解構陣列][decomposition]時,可以用 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

解構Hash和解構陣列有點不同。 想拆解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

組合

組合是指把多個值群組成一個陣列並賦值給變數的能力。 當你想把值_解構_、做些修改,再_組合_回一個變數時,這就很有用。 它也讓你能合併兩個以上的陣列/Hash。

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

用 double splat 運算子(**)組合 Hash

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

>> 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 運算子(*)與 double 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>),那麼關鍵字引數會被打包成一個 Hash 並賦值給最後一個參數。

>> 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>, <key-word_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

你也可以用 double splat 運算子(**)把一個 Hash 的引數拆解進方法呼叫:

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
透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 多重賦值與解構