[Rust 入門] #07 トレイトとジェネリクス — Java の interface と並べて動かして確かめる
はじめに
第3回から第6回まで、説明を後回しにした書き方が、いくつか残っています。
| 出てきた回 | 書き方 | 今回の節 |
|---|---|---|
| 第3回 | does not implement the Copy trait、impl Drop for Noisy | 標準のトレイト |
| 第4回 | &[T]・Vec<T> の T | ジェネリクス |
| 第5回 | #[derive(Debug)]、#[derive(Clone, Copy)] | 標準のトレイト |
| 第6回 | Option<T> の T、? の裏の From | ジェネリクス、標準のトレイト |
今回は、トレイト(型に共通の動作を約束させる仕組み)と、ジェネリクス(どんな型にも使える関数や型を書く仕組み)です。
トレイト(trait)
「この動作ができる型」を、名前を付けて決めるのが、トレイトです。
trait Describe {
fn describe(&self) -> String;
fn describe_twice(&self) -> String {
format!("{0} / {0}", self.describe())
}
}
struct Item {
name: String,
qty: u32,
}
enum Shape {
Circle(f64),
Point,
}
impl Describe for Item {
fn describe(&self) -> String {
format!("{} × {}", self.name, self.qty)
}
}
impl Describe for Shape {
fn describe(&self) -> String {
match self {
Shape::Circle(r) => format!("半径 {r} の円"),
Shape::Point => String::from("点"),
}
}
fn describe_twice(&self) -> String {
String::from("(Shape は独自の describe_twice)")
}
}
fn show<T: Describe>(x: &T) {
println!("{}", x.describe());
}
fn show_impl(x: &impl Describe) {
println!("[impl] {}", x.describe());
}
fn main() {
let item = Item {
name: String::from("りんご"),
qty: 3,
};
let shape = Shape::Circle(1.5);
show(&item);
show(&shape);
show_impl(&item);
println!("{}", item.describe_twice());
println!("{}", shape.describe_twice());
println!("{}", Shape::Point.describe());
}
りんご × 3
半径 1.5 の円
[impl] りんご × 3
りんご × 3 / りんご × 3
(Shape は独自の describe_twice)
点
| 部分 | 意味 |
|---|---|
trait Describe { fn describe(&self) -> String; } | トレイトの定義。「describe を持つ」という約束。中身は書かず、形だけ |
fn describe_twice(&self) -> String { ... } | 中身つきで書くと、デフォルトになる。実装側が書かなければ、これが使われる |
impl Describe for Item { ... } | Item が、その約束を果たす宣言。中身を書く |
fn show<T: Describe>(x: &T) | 「Describe を実装した型 T」なら、何でも受ける関数。T: Describe をトレイト境界と言う |
fn show_impl(x: &impl Describe) | 同じ意味の、短い書き方 |
format!("{0} / {0}", ...) | {0} は、1つ目の引数。同じ値を2回使える |
Item は、デフォルトの describe_twice を使い、Shape は、自分で書き直しています。
実装を書き忘れたとき
約束の関数を、1つも書かないと、エラーです。
impl Describe for Item {}
error[E0046]: not all trait items implemented, missing: `describe`
--> t2.rs:9:1
|
2 | fn describe(&self) -> String;
| ----------------------------- `describe` from trait
...
9 | impl Describe for Item {}
| ^^^^^^^^^^^^^^^^^^^^^^ missing `describe` in implementation
実装していない型には、使えない
Describe を実装していない型を、show に渡すと、エラーです。
struct Other;
show(&Item { name: String::from("りんご") });
show(&Other);
error[E0277]: the trait bound `Other: Describe` is not satisfied
--> t3.rs:23:10
|
23 | show(&Other);
| ---- ^^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
|
help: the trait `Describe` is implemented for `Item`
--> t3.rs:11:1
|
11 | impl Describe for Item {
| ^^^^^^^^^^^^^^^^^^^^^^
note: required by a bound in `show`
--> t3.rs:17:12
|
17 | fn show<T: Describe>(x: &T) {
| ^^^^^^^^ required by this bound in `show`
メソッドを直接呼んでも、同じです。
let o = Other;
println!("{}", o.describe());
error[E0599]: no method named `describe` found for struct `Other` in the current scope
--> t3b.rs:9:22
|
9 | println!("{}", o.describe());
| ^^^^^^^^ method not found in `Other`
|
= help: items from traits can only be used if the trait is implemented and in scope
第3回の E0382 に出ていた does not implement the Copy trait の trait は、この仕組みのことです。
既存の型にも、実装できる
自分で作った型だけでなく、i32 や String にも、自分のトレイトを実装できました。
impl Describe for i32 {
fn describe(&self) -> String {
format!("整数 {self}")
}
}
impl Describe for String {
fn describe(&self) -> String {
format!("文字列 「{self}」")
}
}
fn main() {
println!("{}", 5.describe());
println!("{}", String::from("abc").describe());
}
整数 5
文字列 「abc」
ただし、標準のトレイトを、標準の型に実装することは、できません。
impl fmt::Display for Vec<i32> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "vec")
}
}
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> t5.rs:3:1
|
3 | impl fmt::Display for Vec<i32> {
| ^^^^^^^^^^^^^^^^^^^^^^--------
| |
| `Vec` is not defined in the current crate
|
= note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
= note: define and implement a trait or new type instead
Display も Vec も、自分のプロジェクト(エラーメッセージでは crate)の外にあるので、止まります。自分のトレイトなら、外の型(i32)にも実装できました。自分の型なら、標準のトレイト(Display)も実装できました(あとの Item の例)。
型の違うものを、まとめて扱う: dyn
違う型の値を、同じ配列に入れて、同じように呼ぶには、dyn を使います。
let item = Item;
let shape = Shape;
let list: [&dyn Describe; 2] = [&item, &shape];
for d in list {
println!("{}", d.describe());
}
品物
図形
(この例の Item と Shape は、中身のない型で、describe は「品物」「図形」を返します)
&dyn Describe は、「Describe を実装した、何かへの参照」です。Item と Shape という、別の型が、1つの配列に入りました。
Java の interface と並べる
Java で、同じ形を書きます。
interface Describe {
String describe();
default String describeTwice() {
return describe() + " / " + describe();
}
}
static class Item implements Describe {
String name;
int qty;
Item(String name, int qty) { this.name = name; this.qty = qty; }
public String describe() { return name + " x " + qty; }
}
static <T extends Describe> void show(T x) {
System.out.println(x.describe());
}
apple x 3
apple x 3 / apple x 3
apple x 3
(show(item)、item.describeTwice()、Describe d = item; d.describe() の順に出力)
| Java(21) | Rust | |
|---|---|---|
| 約束の書き方 | interface Describe { ... } | trait Describe { ... } |
| 実装する場所 | クラスの定義に implements Describe と書く | 型の定義とは別に、impl Describe for 型 と書く |
| デフォルトの中身 | default メソッド | トレイトの中に、そのまま書く |
| 「この約束を果たす型」だけ受ける | <T extends Describe> | <T: Describe> |
| 実装が足りないとき | コンパイルエラー | E0046(上の例) |
ジェネリクス
型を決めずに、「どんな型でも」と書くのが、ジェネリクスです。第4回の &[T] の T も、第6回の Option<T> の T も、これです。
2つの値のうち、大きいほうを返す関数を、書いてみます。
fn largest<T>(a: T, b: T) -> T {
if a > b { a } else { b }
}
error[E0369]: binary operation `>` cannot be applied to type `T`
--> t7.rs:2:10
|
2 | if a > b { a } else { b }
| - ^ - T
| |
| T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
| ++++++++++++++++++++++
T は、何の型かわからないので、> で比べられるか、わかりません。help が、「PartialOrd というトレイトを持つ型に、制限してください」と言っています。これが、先ほどのトレイト境界です。
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", largest(3, 5));
println!("{}", largest(2.5, -1.0));
println!("{}", largest('a', 'z'));
println!("{}", largest("apple", "banana"));
println!("{}", largest(String::from("b"), String::from("a")));
}
5
2.5
z
banana
b
整数、小数、文字、文字列と、5種類の型で、同じ関数が使えました。PartialOrd を持たない自作の型を渡すと、止まります。
error[E0277]: can't compare `Item` with `Item`
--> t8b.rs:12:13
|
12 | let c = largest(a, b);
| ^^^^^^^ no implementation for `Item < Item` and `Item > Item`
|
= help: the trait `PartialOrd` is not implemented for `Item`
note: required by a bound in `largest`
--> t8b.rs:1:15
|
1 | fn largest<T: PartialOrd>(a: T, b: T) -> T {
| ^^^^^^^^^^ required by this bound in `largest`
help: consider annotating `Item` with `#[derive(PartialOrd)]`
|
5 + #[derive(PartialOrd)]
6 | struct Item {
ジェネリックな構造体
構造体にも、T が書けます。
struct Pair<T> {
a: T,
b: T,
}
impl<T> Pair<T> {
fn swap(self) -> Pair<T> {
Pair { a: self.b, b: self.a }
}
}
fn main() {
let p = Pair { a: 1, b: 2 };
let p = p.swap();
println!("{} {}", p.a, p.b);
let q = Pair { a: String::from("x"), b: String::from("y") };
let q = q.swap();
println!("{} {}", q.a, q.b);
}
2 1
y x
Pair<T> の a と b は、同じ型 Tです。違う型を入れると、エラーになります。
let p = Pair { a: 1, b: "x" };
error[E0308]: mismatched types
--> t9b.rs:7:29
|
7 | let p = Pair { a: 1, b: "x" };
| ^^^ expected integer, found `&str`
impl<T> Pair<T> の、最初の <T> は、「この impl の中で T を使う」という宣言です。
型ごとに、別の関数ができる
show<T: Describe> を、Item と Shape で呼ぶ、小さなプログラムを作って、コンパイルした実行ファイルの、関数の一覧を見ました(nm)。
0000000000014460 t t10::show::<t10::Item>
0000000000014500 t t10::show::<t10::Shape>
show が、型ごとに別の関数として、できていました(デバッグビルド、#[inline(never)] を付けて確認)。
Java のジェネリクスと並べる
Java にも、ジェネリクスがあります。トレイト境界にあたるのは、<T extends ...> です。
static <T extends Comparable<T>> T largest(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
largest(3, 5) は 5、largest("apple", "banana") は banana でした。Comparable を持たない自作クラスを渡すと、コンパイルエラーです。
J2.java:9: error: method largest in class J2 cannot be applied to given types;
System.out.println(largest(new Item(), new Item()));
^
required: T,T
found: Item,Item
reason: inference variable T has incompatible bounds
upper bounds: Comparable<T>
lower bounds: Item
違いが出るのは、基本の型です。
List<int> v = new ArrayList<>();
J3.java:4: error: unexpected type
List<int> v = new ArrayList<>();
^
required: reference
found: int
Java では、List<int> が書けず、List<Integer> と書きます。Rust の Vec<i32> は、そのまま書けます(第3回から使っています)。
また、Java では、型の引数が、実行時には区別されません。
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());
true
List<String> と List<Integer> が、実行時には、同じクラスでした。
標準のトレイト
ここまでに出てきた、Debug、Clone、Copy、Drop、From は、すべて、標準のトレイトです。
Debug と Display
println! の {:?} は Debug、{} は Display を使います。第5回の #[derive(Debug)] が、前者でした。Display を持たない型を {} で出そうとすると、エラーです。
println!("{}", item);
error[E0277]: `Item` doesn't implement `std::fmt::Display`
--> u1.rs:8:20
|
8 | println!("{}", item);
| -- ^^^^ `Item` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
help: the trait `std::fmt::Display` is not implemented for `Item`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Debug は、derive で自動で付けられました。Display は、今回は自分で書きます。
use std::fmt;
struct Item {
name: String,
qty: u32,
}
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{} × {}", self.name, self.qty)
}
}
fn main() {
let item = Item { name: String::from("りんご"), qty: 3 };
println!("{}", item);
println!("{item}");
let s = item.to_string();
println!("{} 文字", s.chars().count());
}
りんご × 3
りんご × 3
7 文字
write! は、println! と同じ書式で、f に書き込みます。Display を実装すると、to_string() も使えるようになりました。Display のない型に to_string() を呼ぶと、Item: std::fmt::Display が満たされていない、という E0599 になります。
Clone と Copy
第3回で、「String を持つ型は、ムーブされる。clone で複製する」と見ました。自作の型で clone を呼ぶと、次のとおりです。
let a = Item { name: String::from("りんご"), qty: 3 };
let b = a.clone();
error[E0599]: no method named `clone` found for struct `Item` in the current scope
|
= note: the following trait defines an item `clone`, perhaps you need to implement it:
candidate #1: `Clone`
#[derive(Clone)] を付けると、通ります。複製した b.name は、第3回と同じく、別のメモリでした(as_ptr() が、false)。
Copy は、Clone が前提です。
#[derive(Copy)]
struct P {
x: i32,
}
error[E0277]: the trait bound `P: Clone` is not satisfied
--> u3.rs:2:8
|
1 | #[derive(Copy)]
| ---- in this derive macro expansion
2 | struct P {
| ^ the trait `Clone` is not implemented for `P`
|
help: consider annotating `P` with `#[derive(Clone)]`
String のように、Copy でない値を持つ型には、Copy を付けられません。
#[derive(Clone, Copy)]
struct Item {
name: String,
qty: u32,
}
error[E0204]: the trait `Copy` cannot be implemented for this type
--> u3b.rs:2:8
|
1 | #[derive(Clone, Copy)]
| ---- in this derive macro expansion
2 | struct Item {
| ^^^^
3 | name: String,
| ------------ this field does not implement `Copy`
第3回の「整数・小数・&str は Copy、String は Copy でない」と、同じ話です。Copy でないフィールドが1つでもあると、その型に Copy は付けられませんでした。整数だけの P は、付けられました(第5回)。
Drop
第3回から使っている impl Drop for Noisy は、Drop というトレイトの実装でした。スコープの終わりに、Rust が drop を呼びます。自分で a.drop() と呼ぶことは、できません。
let a = Noisy("a");
a.drop();
error[E0040]: explicit use of destructor method
--> u4.rs:11:7
|
11 | a.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
11 - a.drop();
11 + drop(a);
help のとおり、drop(a) と書くと、その場で捨てられます。
let a = Noisy("a");
let _b = Noisy("b");
drop(a);
println!("main の終わり");
drop: a
main の終わり
drop: b
drop(a) で、a がその場で捨てられ、b は、main の終わりで捨てられました。
From と ?
第6回で、? が、エラーの型を変換しようとして、止まりました。
error[E0277]: `?` couldn't convert the error to `String`
|
| --------------^ the trait `From<ParseIntError>` is not implemented for `String`
From は、「ある型から、別の型を作る」トレイトです。第3回から使っている String::from("りんご") も、From です。第6回の help の一覧にも、String が From<&str> を実装していると出ていました。
自分のエラー型に From を実装すると、? が、自動で変換してくれます。
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
Zero,
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> AppError {
AppError::Parse(e)
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Parse(e) => write!(f, "数ではありません({e})"),
AppError::Zero => write!(f, "0 は指定できません"),
}
}
}
fn parse_qty(s: &str) -> Result<u32, AppError> {
let n = s.parse::<u32>()?;
if n == 0 {
return Err(AppError::Zero);
}
Ok(n)
}
fn main() {
for s in ["3", "0", "abc"] {
match parse_qty(s) {
Ok(n) => println!("{s} -> {n}"),
Err(e) => println!("{s} -> エラー: {e}"),
}
}
println!("{:?}", parse_qty("abc"));
}
3 -> 3
0 -> エラー: 0 は指定できません
abc -> エラー: 数ではありません(invalid digit found in string)
Err(Parse(ParseIntError { kind: InvalidDigit }))
s.parse::<u32>()? の ParseIntError が、From で AppError::Parse(...) に変わり、Err(AppError) として返っています。第5回の enum、第6回の Result と ?、今回の From と Display が、1つにつながりました。
サンプル: 買い物リストに「元に戻す」を付ける
買い物リストから消した品物を、ごみ箱に入れて、後から戻せるようにします。ごみ箱は、ジェネリクスの Stack<T>(あとに入れたものから、先に出る入れ物)で作ります。
cargo new shop5
cd shop5
src/main.rs を、次のようにします。
use std::fmt;
struct Item {
name: String,
qty: u32,
}
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} × {}", self.name, self.qty)
}
}
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Stack<T> {
Stack { items: Vec::new() }
}
fn push(&mut self, x: T) {
self.items.push(x);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn len(&self) -> usize {
self.items.len()
}
}
fn print_all<T: fmt::Display>(items: &[T]) {
for item in items {
println!(" {item}");
}
println!(" ({} 件)", items.len());
}
fn main() {
let mut list = vec![
Item {
name: String::from("りんご"),
qty: 3,
},
Item {
name: String::from("みかん"),
qty: 5,
},
Item {
name: String::from("ぶどう"),
qty: 2,
},
];
let mut removed: Stack<Item> = Stack::new();
println!("最初");
print_all(&list);
for _ in 0..2 {
if let Some(item) = list.pop() {
println!("削除: {item}");
removed.push(item);
}
}
println!("削除したあと(ごみ箱 {} 件)", removed.len());
print_all(&list);
if let Some(item) = removed.pop() {
println!("元に戻す: {item}");
list.push(item);
}
print_all(&list);
let mut numbers: Stack<i32> = Stack::new();
numbers.push(10);
numbers.push(20);
println!(
"{:?} {:?} {:?}",
numbers.pop(),
numbers.pop(),
numbers.pop()
);
print_all(&[1.5, 2.5]);
}
cargo run
最初
りんご × 3
みかん × 5
ぶどう × 2
(3 件)
削除: ぶどう × 2
削除: みかん × 5
削除したあと(ごみ箱 2 件)
りんご × 3
(1 件)
元に戻す: みかん × 5
りんご × 3
みかん × 5
(2 件)
Some(20) Some(10) None
1.5
2.5
(2 件)
| 部分 | 意味 |
|---|---|
impl fmt::Display for Item | {} で表示できるようにする(この節の Display) |
struct Stack<T> | どんな型でも入れられる、入れ物 |
Stack<Item>、Stack<i32> | 同じ Stack を、Item 用と i32 用に使う |
fn pop(&mut self) -> Option<T> | 空のとき None(第6回)。最後の numbers.pop() が None |
fn print_all<T: fmt::Display>(items: &[T]) | Display を持つ型の、スライス(第4回の &[T])なら何でも。Item の Vec も、小数の配列も通る |
removed.pop() が Option を返すので、if let Some(item) = ...(第5回・第6回)で受けています。取り出した item は、list.push(item) で、list にムーブされます(第3回)。
cargo clippy は、指摘なしで通りました。cargo fmt --check も、整形のあとで、指摘なしで通りました。
歴史
| 年 | できごと |
|---|---|
| 1988年10月 | Philip Wadler と Stephen Blott が、論文「How to make ad-hoc polymorphism less ad hoc」を書く。型クラス(type classes)を提案した |
| 1989年1月 | その論文が、POPL(ACM のプログラミング言語の原理のシンポジウム)で発表される |
| 2004年 | ジェネリクスが、Java に加わる(J2SE 5.0) |
論文の要約は、型クラスを、「アドホック多相への新しい取り組み」と説明しています。Hindley/Milner の型システムを拡張し、演算子の多重定義などを扱えるようにするものです。この仕事は、Haskell の設計の中から出てきて、Haskell に入りました。論文の著者たちは、型クラスは、Haskell とは独立に評価されるべきで、Standard ML のような別の言語にも入れられる、とも書いています。
Java のジェネリクスについては、Wikipedia の記事に、型情報はコンパイル時に取り除かれ(型消去)、実行時には、型引数を決められない、とあります。基本の型は使えず、Integer のような包んだ型を使う、とも書かれています。
動作確認
確認できたこと
- 上のコードと出力は、すべて、Linux(Rust 1.97.0、Cargo 1.97.0、edition 2024)で、実際に実行した結果です
- Java のコードは、同じ環境の Java(OpenJDK 21)で、実際に実行した結果です。日本語のメッセージは、この環境のコンソールで文字化けしたので、Java の例は、英字のメッセージで実行しました
- 13個のエラーのメッセージ(
E0046、E0277を4種類の形、E0599を3種類の形、E0117、E0369、E0308、E0204、E0040) - トレイトの定義、
impl、デフォルトメソッドとその上書き、<T: Trait>と&impl Trait、&dyn Traitの配列 - 自分のトレイトを、
i32とStringに実装できること。標準のトレイト(Display)を、標準の型(Vec<i32>)に実装するとE0117になること largest<T: PartialOrd>が、整数・小数・文字・&str・Stringで動くことshow<T>が、型ごとに別の関数としてできること(nmの出力で確認。デバッグビルドの1回の実行)Displayを実装するとto_string()が使えること。Displayがないとto_string()がE0599になること#[derive(Clone)]で複製した値が、別のメモリになること(as_ptr())drop(a)が、その場で捨てることFromを実装した型で、?が、エラーの型を変換すること- Java の
interface・defaultメソッド・<T extends Describe>の例。implementsしたinterfaceのメソッドを書かないと、コンパイルエラー(does not override abstract method)になること。<T extends Comparable<T>>が、Comparableを持たないクラスでコンパイルエラーになること。List<int>が書けないこと。List<String>とList<Integer>のgetClass()が同じになること - 買い物リストのサンプルが、
cargo runで動くこと。cargo clippyとcargo fmt --checkが、指摘なしで通ること
歴史の出典
- 型クラスの論文のタイトル、著者、日付(1988年10月、POPL 1989年1月)、要約の内容は、Tufts 大学の Web サイトに置かれた論文の PDF の記述です。取得ツールが返した要約を通して確認しました。論文の全文は、読んでいません
- Java のジェネリクスが 2004 年(J2SE 5.0)に加わったこと、型消去、基本の型が使えないことは、Wikipedia の「Generics in Java」の記述です。ミラーのページを使い、取得ツールの要約を通して確認しました。1つの出典で、他の資料とは、突き合わせていません
確認できていないこと
- Rust のトレイトが、Haskell の型クラスから、どの程度影響を受けているか。この記事では、型クラスの論文の内容と、Rust のトレイトの動きを、別々に書いています
dynで、どのメソッドが呼ばれるかを決める仕組み(実行時に決まる、など)。配列に別の型を入れて呼べることだけを、確認しましたnmで見た「型ごとに別の関数ができる」が、最適化をかけたビルド(--release)でも同じか- 「標準のトレイトを標準の型に実装できない」規則(孤児規則)の、細かい条件。
E0117のメッセージと、1つの例だけを確認しました - Java で、既存のクラスに、あとから
interfaceを付けられるかどうか - Windows と macOS での実行結果
次回
次回は、ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)です。第4回の E0106(missing lifetime specifier)と E0597(does not live long enough)、第2回の &'static str の 'static を回収します。入門編の最終回として、ここまでの所有権・借用・struct・enum・Result・トレイトを使った、ファイル一覧のコマンドラインツールを作ります。
連載の目次(予定)
入門編(8回)
- Rust とは / 環境構築 / Hello World
- 変数・型・関数(Java との対比)
- 所有権(ムーブ)
- 借用と参照
- 構造体・enum・match
- Result / Option とエラー処理
- トレイトとジェネリクス(今回)
- ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)