// book chapter
動かして学ぶRustプログラミング問題集第5章 タプルと配列・スライス.
この章では、複数の値をひとまとめに扱う型を学びます。
異なる型の値を並べるタプル、同じ型の値を固定長で並べる配列、そしてその一部を切り出すスライスを10問で身につけます。
第4章までは、1つの変数に1つの値だけを束縛してきました。複数の値をまとめて扱えるようになると、「3科目の点数」「座標」のようなデータをそのままの形でコードに落とし込めるようになります。
進め方は第4章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - タプルを作る
タプル型に関する問題です。
商品名・価格・在庫の有無をひとまとめにしたタプルitemを作り、それぞれの要素を出力してください。
商品名: チョコレート
価格: 150円
在庫あり: truefn main() {
// 商品名"チョコレート"・価格150・在庫ありtrueをまとめたタプルitemを作成せよ
println!("商品名: {}", item.0);
println!("価格: {}円", item.1);
println!("在庫あり: {}", item.2);
}Playgroundで開く解答例と解説
fn main() {
// 商品名"チョコレート"・価格150・在庫ありtrueをまとめたタプルitemを作成せよ
let item = ("チョコレート", 150, true);
println!("商品名: {}", item.0);
println!("価格: {}円", item.1);
println!("在庫あり: {}", item.2);
}Playgroundで開くタプルは(値, 値, 値)のように、値をカンマ区切りで括弧に並べて作ります。要素ごとに違う型の値を入れられるのが特徴で、ここでは文字列・整数・論理値という3つの型を1つにまとめています。
要素を取り出すにはitem.0のように.の後ろにインデックスを書きます。インデックスは0から始まる点に注意してください。1番目の要素はitem.1ではなくitem.0です。
型注釈を付けるならlet item: (&str, u32, bool) = ...;のように、要素の型を同じ順番で並べて書きます。要素の型の並びと個数はタプル型そのものの一部なので、(u32, bool)と(bool, u32)は別の型として扱われます。
02 - タプルを分解する
タプル型と変数に関する問題です。
座標を表すタプルを2つの変数x・yに分解してください。point.0のように1つずつ取り出すのではなく、letの左辺にパターンを書いてまとめて取り出します。
x座標: 3
y座標: 5
原点からの距離の2乗: 34fn main() {
let point = (3, 5);
// pointを分解し、1つ目の要素をx、2つ目の要素をyに束縛せよ
println!("x座標: {}", x);
println!("y座標: {}", y);
println!("原点からの距離の2乗: {}", x * x + y * y);
}Playgroundで開く解答例と解説
fn main() {
let point = (3, 5);
// pointを分解し、1つ目の要素をx、2つ目の要素をyに束縛せよ
let (x, y) = point;
println!("x座標: {}", x);
println!("y座標: {}", y);
println!("原点からの距離の2乗: {}", x * x + y * y);
}Playgroundで開くlet (x, y) = point;のように、letの左辺にタプルと同じ形を書くと、対応する位置の要素がそれぞれの変数に束縛されます。この「値の形に合わせて中身を取り出す書き方」をパターンと呼びます。
item.0のようなインデックスでのアクセスと結果は同じですが、x・yという意味のある名前を付けられるぶん、その後のコードが読みやすくなります。
パターンの変数の個数は、タプルの要素数とぴったり一致していなければなりません。let (x, y) = (3, 5, 7);のように数が合わないとコンパイルエラー(E0308)になります。使わない要素がある場合は、let (x, _) = point;のようにアンダースコア_を書いて「この要素は使わない」と示せます。
03 - 関数から複数の値を返す
タプル型と関数に関する問題です。
割り算の商と余りをまとめて返す関数divideを実装し、テストに合格させてください。divide(27, 4)なら商が6・余りが3なので、(6, 3)を返します。
// 割られる数dividendと割る数divisorを受け取り、(商, 余り)のタプルを返す関数divideを定義せよ
#[test]
fn test_divide() {
assert_eq!(divide(27, 4), (6, 3));
assert_eq!(divide(10, 5), (2, 0));
assert_eq!(divide(3, 8), (0, 3));
}Playgroundで開く解答例と解説
// 割られる数dividendと割る数divisorを受け取り、(商, 余り)のタプルを返す関数divideを定義せよ
fn divide(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
#[test]
fn test_divide() {
assert_eq!(divide(27, 4), (6, 3));
assert_eq!(divide(10, 5), (2, 0));
assert_eq!(divide(3, 8), (0, 3));
}Playgroundで開く第4章で「戻り値の型は1つしか書けない」と説明しました。複数の値を返したいときは、このようにタプルにまとめて1つの値として返します。
戻り値の型は-> (i32, i32)のように、タプル型をそのまま書きます。本体末尾の(dividend / divisor, dividend % divisor)はタプルを作る式なので、これがそのまま戻り値になります。
呼び出す側では、問題02のパターンを使って受け取るのが定番です。
fn divide(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
fn main() {
let (quotient, remainder) = divide(27, 4);
println!("27 ÷ 4 = {} あまり {}", quotient, remainder);
}Playgroundで開くassert_eq!はタプル同士もそのまま比較できます。要素がすべて等しければ等しいと判定されます。
04 - 配列を作る
配列型に関する問題です。
3科目の点数を配列にまとめ、1科目目と3科目目を出力してください。型注釈[i32; 3]も付けてください。
1科目目: 80点
3科目目: 70点fn main() {
// 80, 95, 70の3つの点数を、[i32; 3]の型注釈付きで配列scoresに束縛せよ
println!("1科目目: {}点", scores[0]);
println!("3科目目: {}点", scores[2]);
}Playgroundで開く解答例と解説
fn main() {
// 80, 95, 70の3つの点数を、[i32; 3]の型注釈付きで配列scoresに束縛せよ
let scores: [i32; 3] = [80, 95, 70];
println!("1科目目: {}点", scores[0]);
println!("3科目目: {}点", scores[2]);
}Playgroundで開く配列は[値, 値, 値]と角括弧で作り、型は[要素の型; 要素数]と書きます。タプルとの違いは次の2点です。
| タプル | 配列 | |
|---|---|---|
| 要素の型 | 要素ごとに違ってよい | すべて同じ |
| 要素の取り出し | item.0(リテラルのみ) |
scores[0](変数も可) |
要素へのアクセスはscores[0]のように角括弧にインデックスを書きます。タプルと違って変数や計算結果をインデックスに使えるのが大きな違いです。そのため、次の問題以降で見るようにループと組み合わせて全要素を順に処理できます。
型注釈の3という要素数も型の一部です。そのため[i32; 3]と[i32; 4]は別の型で、let scores: [i32; 3] = [80, 95];のように個数が合わないとコンパイルエラー(E0308)になります。
05 - 同じ値で初期化とlen
配列型に関する問題です。
0を5個並べた配列countsを作り、配列全体と要素数を出力してください。0を5回書く必要はありません。
[0, 0, 0, 0, 0]
要素数: 5fn main() {
// 0を5個並べた配列countsを作成せよ
println!("{:?}", counts);
println!("要素数: {}", counts.len());
}Playgroundで開く解答例と解説
fn main() {
// 0を5個並べた配列countsを作成せよ
let counts = [0; 5];
println!("{:?}", counts);
println!("要素数: {}", counts.len());
}Playgroundで開く[値; 個数]と書くと、同じ値を指定した個数だけ並べた配列ができます。[0; 5]は[0, 0, 0, 0, 0]と書いたのとまったく同じです。カウンタの初期化のように、同じ値で埋めた配列を用意したいときに使います。
counts.len()は要素数を返すメソッドです。.len()のように値の後ろにドットを付けて呼び出す関数をメソッドと呼びます。第3章のcount()や第4章の関数と考え方は同じで、「その値に対して呼び出す関数」という点だけが違います。
もう1つ新しい記法が出てきました。{:?}です。
println!の{}は「人に見せるための表示」を行うプレースホルダで、配列やタプルのようにまとまったデータには使えません({}で配列を出力しようとするとコンパイルエラーE0277になります)。代わりに{:?}と書くと「デバッグ用の表示」になり、[0, 0, 0, 0, 0]のように中身をそのままの形で確認できます。開発中に値を確かめる用途で頻繁に使う書き方です。
06 - 範囲外アクセス
配列型に関する問題です。
次のコードは配列の最後の要素を出力しようとしていますが、コンパイルエラーになります。エラーメッセージを読み、最後の要素が出力されるように修正してください。
最後の科目: 70点fn main() {
let scores = [80, 95, 70];
println!("最後の科目: {}点", scores[3]); // この行でコンパイルエラーになる
}Playgroundで開く解答例と解説
fn main() {
let scores = [80, 95, 70];
println!("最後の科目: {}点", scores[3]); // この行でコンパイルエラーになる
println!("最後の科目: {}点", scores[2]); // インデックスは0から始まる
}Playgroundで開くエラーメッセージは「this operation will panic at runtime」、続けて「index out of bounds: the length is 3 but the index is 3(範囲外のインデックス: 長さは3だがインデックスは3)」でした。
要素が3つの配列で使えるインデックスは0・1・2の3つだけです。インデックスが0から始まるため、最後の要素のインデックスは「要素数 - 1」になります。ここを1つずれて間違えるのはプログラミングでの定番のバグで、「off-by-oneエラー」と呼ばれます。
要素数から計算してscores[scores.len() - 1]と書く方法もあります。配列の要素数が変わっても書き換えずに済むぶん、こちらのほうが安全です。
07 - for式で配列を走査する
配列型とfor式に関する問題です。
配列の全要素を順に出力してください。
120円
250円
80円fn main() {
let prices = [120, 250, 80];
// for式でpricesの要素を順に取り出し、「120円」の形式で出力せよ
}Playgroundで開く解答例と解説
fn main() {
let prices = [120, 250, 80];
// for式でpricesの要素を順に取り出し、「120円」の形式で出力せよ
for price in prices {
println!("{}円", price);
}
}Playgroundで開く第3章ではfor i in 1..=5のように範囲式を走査しましたが、for式には配列もそのまま渡せます。for price in pricesと書くと、要素が先頭から順に1つずつpriceに束縛されます。
インデックスを使って書くこともできます。
fn main() {
let prices = [120, 250, 80];
for i in 0..prices.len() {
println!("{}円", prices[i]);
}
}Playgroundで開くこちらは「何番目か」を使いたい場合に便利ですが、要素の値だけが必要ならインデックスを介さないfor price in pricesのほうが簡潔で、問題06のような範囲外アクセスの心配もありません。まずは要素を直接受け取る書き方を選び、インデックスが必要なときだけ後者を使ってください。
08 - 配列の要素を書き換える
配列型と変数に関する問題です。
2科目目の点数を100に書き換えて、配列全体を出力してください。
[80, 100, 70]fn main() {
let scores = [80, 95, 70];
// 2科目目の点数を100に書き換えよ
println!("{:?}", scores);
}Playgroundで開く解答例と解説
fn main() {
let scores = [80, 95, 70];
let mut scores = [80, 95, 70]; // 書き換えるにはmutが必要
// 2科目目の点数を100に書き換えよ
scores[1] = 100;
println!("{:?}", scores);
}Playgroundで開く配列の要素を書き換えるには配列[インデックス] = 値;と書きます。2科目目のインデックスは1です。
ここでのポイントは、scores[1] = 100;だけを書き足しても動かないことです。第1章で学んだとおりRustの変数はデフォルトで不変なので、let mut scores = ...とmutを付けて可変にしなければなりません。付け忘れると「cannot assign to scores[_], as scores is not declared as mutable」というコンパイルエラー(E0594)になります。
可変性は配列全体に対して設定するもので、「この要素だけ書き換え可能」といった指定はできません。mutが付いていれば全要素を書き換えられますし、付いていなければ1要素も書き換えられません。
なお、書き換えられるのはあくまで要素の値だけです。scoresの要素数は型の一部なので、要素を追加したり削除したりすることはできません。
09 - スライスで一部を借りる
スライスと範囲式に関する問題です。
5科目の点数のうち、2科目目から4科目目(インデックス1から3)の3つだけを取り出して出力してください。
[95, 70, 60]
要素数: 3fn main() {
let scores = [80, 95, 70, 60, 88];
// scoresのインデックス1から3までの3要素を指すスライスmiddleを作成せよ
println!("{:?}", middle);
println!("要素数: {}", middle.len());
}Playgroundで開く解答例と解説
fn main() {
let scores = [80, 95, 70, 60, 88];
// scoresのインデックス1から3までの3要素を指すスライスmiddleを作成せよ
let middle = &scores[1..4]; // 1..=3 と書いてもよい
println!("{:?}", middle);
println!("要素数: {}", middle.len());
}Playgroundで開く&配列[範囲]と書くと、配列の一部だけを指すスライスが作れます。範囲の指定には第3章で学んだ範囲式をそのまま使えるので、&scores[1..4](1以上4未満)でも&scores[1..=3](1以上3以下)でも同じ3要素になります。
大事なのは、スライスがデータをコピーせず、元の配列の一部を指しているだけという点です。middleは「scoresの1番目から3要素ぶん」という位置と長さの情報を持っているだけで、点数のデータそのものはscores側にあります。だから「借りる」と表現します。
middleの型は&[i32]で、要素数が型に含まれません。要素数が型の一部だった配列型[i32; 5]との違いです。
| 型 | 要素数が型の一部 | データ |
|---|---|---|
配列 [i32; 5] |
○ | 自分で持つ |
スライス &[i32] |
✗ | 他の値から借りる |
この性質のおかげで、引数を&[i32]にした関数は要素数の違う配列のどれからでも呼び出せます。次の問題で実際に書いてみます。
10 - 応用: 合計と最大値
第5章の総復習として、タプル型・配列型・スライス・for式・関数を組み合わせた問題です。
点数のスライスを受け取り、(合計, 最大値)のタプルを返す関数summarizeを実装して、テストに合格させてください。
// 点数のスライス&[i32]を受け取り、(合計, 最大値)のタプルを返す関数summarizeを定義せよ
#[test]
fn test_summarize() {
assert_eq!(summarize(&[80, 95, 70]), (245, 95));
assert_eq!(summarize(&[3, 1, 4, 1, 5]), (14, 5));
assert_eq!(summarize(&[-5, -2, -9]), (-16, -2));
}Playgroundで開く解答例と解説
// 点数のスライス&[i32]を受け取り、(合計, 最大値)のタプルを返す関数summarizeを定義せよ
fn summarize(scores: &[i32]) -> (i32, i32) {
let mut total = 0;
let mut max = scores[0]; // 先頭の要素を暫定の最大値にする
for i in 0..scores.len() {
total += scores[i];
if scores[i] > max {
max = scores[i];
}
}
(total, max)
}
#[test]
fn test_summarize() {
assert_eq!(summarize(&[80, 95, 70]), (245, 95));
assert_eq!(summarize(&[3, 1, 4, 1, 5]), (14, 5));
assert_eq!(summarize(&[-5, -2, -9]), (-16, -2));
}Playgroundで開くこの章と前章の要素が一通り登場する問題です。順に見ていきましょう。
引数を&[i32]にする
テストは要素数3・5・3の異なる配列を渡しています。要素数が型の一部である[i32; 3]では3要素の配列しか受け取れませんが、スライス&[i32]なら要素数を問いません。呼び出し側の&[80, 95, 70]は「配列を作って、その全体を借りる」という意味です。
合計を求める
合計を貯める変数はlet mut total = 0;のように、ループの外側で宣言します。内側で宣言すると繰り返しのたびに0に戻ってしまいます。
最大値を求める
最大値の初期値を0にすると、3つ目のテスト(すべて負の数)で失敗します。すべての要素が0より小さいと1度も更新されず、答えが0になってしまうためです。先頭の要素を暫定の最大値としておき、それより大きい要素が見つかるたびに更新するのが定石です。
これで第5章は終わりです。複数の値をまとめて扱えるようになりました。次の章では、文字列を本格的に扱うStringと、要素数が実行時に変えられるベクタに進みます。