はじめに
型ごとに同じような関数を書き続けていると、どこかで「これ、まとめられないのか」と思い始めます。i32 用の max を書いたら次は f64 用、その次は u16 用――本体のコードは同じなのに、型が違うだけで別の関数を用意することになる。あるいは、長方形の面積を計算する関数と円の面積を計算する関数を、まったく別物として書いてしまう。
Rust がこれに用意している答えがトレイトとジェネリクスです。トレイトが「共通の振る舞いの仕様」を決め、ジェネリクスが「任意の型を受け取る仕組み」を提供し、トレイト境界がその2つを結びつけます。この3点セットが分かると、コード量が目に見えて減ります。
この記事では、トレイトの定義 → 実装 → デフォルト実装 → 引数としてのトレイト → ジェネリクス → ジェネリックな構造体・列挙型 → derive 属性までを1本にまとめました。最後に練習問題も用意しています。
このシリーズでは、環境構築・基礎文法・制御構文・所有権・モジュールも別記事で解説しています。
この記事で分かること
- トレイトが何を定義するもので、インターフェースとどう似ていて何が違うのかが分かる
- 任意の型にトレイトを実装し、共通のメソッドを呼び出せる
- デフォルト実装で、実装側の負担を減らせる
-
&impl Traitを使って「そのトレイトを実装した任意の型」を引数に取れる - ジェネリクスとトレイト境界(
<T: Trait>とwhere)を書き分けられる - ジェネリックな構造体・列挙型を定義し、特定の型限定のメソッドも書ける
-
#[derive(...)]で標準的なトレイトの実装を自動生成できる - 章ごとの練習問題(解答例つき)で、理解できているかを自分で確認できる
第1章:トレイトとは
トレイトは、共通の振る舞いの仕様を定義するためのものです。トレイトにはメソッドのシグネチャだけを書き、処理の中身は実装しません。
定義した共通の振る舞いを任意の型に紐づけると、その型に対してトレイトのメソッドの実装が強制されます。この「型とトレイトを紐づけること」をトレイトを実装すると言います。
イメージとしては、他の言語のインターフェースや抽象クラスに近い概念です。ただし後述するように、Rust にはクラスの継承がありません。トレイトは「共通の振る舞いを共有する」ための仕組みであって、親クラスから性質を受け継ぐものではない、という点は押さえておいてください。
トレイトを定義する
trait トレイト名 { ... } という形で書きます。ここでは図形を表す Shape トレイトを、ライブラリクレート側に定義してみます。
// src/lib.rs
pub mod sample_trait {
pub trait Shape {
/// 面積を計算する
fn calc_area(&self) -> f64;
/// 周囲の長さを計算する
fn calc_perimeter(&self) -> f64;
/// self を取らない関連関数も定義できる
fn do_something();
}
}
メソッドなので、引数には &self(共有参照)を取ります。戻り値の型は f64 としました。トレイトに書くのはシグネチャのみで、波括弧は付けずセミコロンで終えます。
構造体のときと同様に、self を取らない関連関数も定義できます。
第2章:トレイトを実装する
定義したトレイトを、実際の型に実装します。まずは長方形を表す構造体を作ります。
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
実装は impl トレイト名 for 型名 { ... } という形です。
impl Shape for Rectangle {
fn calc_area(&self) -> f64 {
self.width * self.height
}
fn calc_perimeter(&self) -> f64 {
self.width * 2.0 + self.height * 2.0
}
fn do_something() {
println!("This is Rectangle fn");
}
}
**トレイトを実装するときは、トレイトで定義されたメソッドをすべて実装する必要があります。**1つでも欠けているとコンパイルエラーになり、「calc_perimeter が足りない」と具体的に指摘されます。実装漏れがコンパイル時に分かるのは、トレイトの大きな利点です。
同じ要領で円も実装してみましょう。円周率は標準ライブラリに定数として定義されています。
pub struct Circle {
pub radius: f64,
}
impl Shape for Circle {
fn calc_area(&self) -> f64 {
self.radius * self.radius * std::f64::consts::PI
}
fn calc_perimeter(&self) -> f64 {
2.0 * self.radius * std::f64::consts::PI
}
fn do_something() {
println!("This is Circle fn");
}
}
呼び出すときの注意点
main から使ってみます。ここに1つ落とし穴があります。
// src/main.rs
use rust_lesson::sample_trait::{Circle, Rectangle, Shape};
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
let circle = Circle { radius: 2.0 };
println!("{}", rect.calc_area());
println!("{}", circle.calc_area());
Rectangle::do_something();
}
**型をインポートしただけでは、その型に実装したトレイトのメソッドは呼び出せません。**上のコードで Shape を use から外すと、rect.calc_area() がエラーになります。トレイトのメソッドを使うには、型だけでなく実装元のトレイトもスコープに入れる必要があります。第3章で use rand::RngExt; が必要だったのと同じ理由です。
トレイトさえインポートすれば、あとは通常のメソッドと同じように扱えます。
トレイトの実装には**孤児ルール(orphan rule)**という制約があります。「トレイトか型のどちらかが自分のクレートのものでなければ実装できない」というもので、外部クレートの型に外部クレートのトレイトを実装することはできません。これがないと、別々のクレートが同じ組み合わせに矛盾する実装を与えられてしまうためです。
第3章:デフォルト実装
トレイトはシグネチャだけを定義して中身は実装側に任せる、というのが基本です。しかし、すべて、または一部のメソッドにあらかじめ振る舞いを実装しておくこともできます。これをデフォルト実装と呼びます。
pub trait Shape {
fn calc_area(&self) -> f64;
fn calc_perimeter(&self) -> f64;
fn do_something();
// デフォルト実装
fn default_something(&self) -> &str {
"This is default method"
}
}
デフォルト実装があるメソッドは、実装側で次のどちらかを選べます。
Rectangle にも Circle にも default_something を実装せずに呼び出すと、どちらもデフォルト実装の内容が使われます。
println!("{}", rect.default_something()); // This is default method
println!("{}", circle.default_something()); // This is default method
Rectangle 側で上書きしてみます。
impl Shape for Rectangle {
// ...他のメソッドは省略
fn default_something(&self) -> &str {
"This is Rectangle default"
}
}
This is Rectangle default ← 上書きされた
This is default method ← Circle は省略したのでデフォルトのまま
デフォルト実装を使うと、型ごとに共通する処理を毎回書かずに済み、実装の負担を減らせます。
第4章:引数としてトレイトを使う
関数やメソッドの引数の型にトレイトを指定すると、「そのトレイトを実装した何らかの型」として扱えます。
たとえば、渡された図形の面積を2倍にして返す double_area 関数を考えます。具体的な型を引数に取ると、Rectangle 用と Circle 用で2つの関数定義が必要になってしまいます。型が増えるたびに関数も増えていく形です。
しかし、Shape トレイトを実装した型なら必ず calc_area を持っているはずです。「Shape を実装した任意の型」という制約を付けられれば、関数定義は1つで済みます。
pub fn double_area(shape: &impl Shape) -> f64 {
shape.calc_area() * 2.0
}
引数の型を &impl トレイト名 と書くことで、そのトレイトを実装した任意の型の共有参照を受け付けられるようになりました。
impl Trait(引数位置)は、実質的にジェネリクスの糖衣構文です。次の2つはほぼ同じ意味になります。
fn double_area(shape: &impl Shape) -> f64 { /* ... */ }
fn double_area<T: Shape>(shape: &T) -> f64 { /* ... */ }
違いは、後者は呼び出し側で double_area::<Rectangle>(&rect) のように型を明示できるのに対し、前者は明示できず推論に任せる点です。制約が単純なら impl Trait、型を複数箇所で使い回すなら名前付きのジェネリクス、と考えれば十分です。
なお、実行時に異なる型を1つの変数に入れたい場合は &dyn Shape や Box<dyn Shape>(トレイトオブジェクト)を使います。こちらは動的ディスパッチになり、Vec<Box<dyn Shape>> のように異なる図形をまとめて持てます。
第5章:ジェネリクスとトレイト境界
型ごとに関数を書く問題
2つの値を比較して大きい方を返す max 関数を考えます。
fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
これは i32 の比較しかできません。f64 を比較したければ別の関数が必要で、u16 や i64 と増えていくと、中身がまったく同じ関数を大量に作ることになります。
引数の型が変わっても関数本体のコードが同じなら、任意の型を受け取れる関数を定義してコードの重複を避けられます。この仕組みがジェネリクスで、そこで使う任意の型をジェネリック型と呼びます。
書き換える
ジェネリクスを使うには、関数名の後ろに <> でジェネリック型名を指定します。名前は任意ですが、大文字の T を使うのが慣習です。複数定義したい場合はカンマで区切ります。
fn max<T>(a: T, b: T) -> T {
if a > b { a } else { b } // エラー: T は比較できるとは限らない
}
引数と戻り値を T にすると、比較演算子の部分でエラーが出ます。T は任意の型なので、大小比較できない型が渡される可能性が残っているからです。Rust は「エラーが起こるかもしれない部分」できちんとエラーを出してくれるので、事前に問題を検知できます。
トレイト境界
このエラーを消すには、「渡される型は大小比較が可能である」という制約を付けます。この制約をジェネリック境界またはトレイト境界と呼びます。書き方は2通りあります。
// 方法1: 山括弧の中に書く
fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// 方法2: where 句で書く
fn max<T>(a: T, b: T) -> T
where
T: PartialOrd,
{
if a > b { a } else { b }
}
PartialOrd は大小比較のためのトレイトで、これを実装していれば比較演算子が使えることが保証されます。
意味はどちらも同じですが、使い分けの目安は次のとおりです。
| 書き方 | 向いている場面 |
|---|---|
<T: Trait> |
制約がシンプルなとき |
where T: Trait |
ジェネリック型が複数あるとき、制約が複雑なとき |
制約は + でつなげて複数指定できます。
fn max<T: PartialOrd + Debug>(a: T, b: T) -> T {
if a > b { a } else { b }
}
これは「PartialOrd を実装しているかつ Debug を実装している任意の型」という意味になります。
整数型でも浮動小数点数でも文字列リテラルでも PartialOrd は実装されているので、1つの関数定義ですべて動くようになりました。
fn main() {
println!("{}", max(1, 2)); // 2
println!("{}", max(1.5, 0.5)); // 1.5
println!("{}", max("apple", "banana")); // banana
}
第6章:ジェネリックな構造体・列挙型
ジェネリクスは関数だけでなく、列挙型や構造体にも使えます。
実はもう使っている
ジェネリックな列挙型は、すでに登場しています。Option です。
enum Option<T> {
None,
Some(T),
}
列挙型名の後ろに <T> が続いており、これによってバリアント Some が任意の型の値を保持できるようになっています。Some(3) でも Some("hello") でも同じ Option で扱えるのは、ジェネリクスのおかげです。
ジェネリックな構造体
構造体も、構造体名の後ろに <T> を付けるだけです。
use std::fmt::{Debug, Display};
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
メソッドを追加するときは、impl の後ろにもジェネリック型を書く必要があります。
impl<T: PartialOrd + Debug> Point<T> {
fn max(&self) -> &T {
if self.x > self.y { &self.x } else { &self.y }
}
}
トレイト境界を指定する場所に注意してください。構造体名の後ろの <T> ではなく、impl キーワードの直後の <T> に書きます。impl<T: PartialOrd> Point<T> という並びです。
また、戻り値を &T にしているのは、T がムーブの起こる型である可能性があるためです。値をそのまま返すと所有権が動いてしまうので、共有参照を返しています。
これで、Point のフィールドは整数型でも浮動小数点数型でも文字列リテラル型でも、トレイト境界さえ満たしていれば任意の型を取れます。
let p1 = Point { x: 1, y: 5 };
let p2 = Point { x: 1.5, y: 0.5 };
println!("{:?}", p1.max()); // 5
println!("{:?}", p2.max()); // 1.5
メソッド独自のジェネリック型
メソッドのジェネリック型は、構造体のジェネリック型と一致している必要はありません。構造体とは独立した型を取れます。
impl<T: PartialOrd + Debug> Point<T> {
fn print_arg<U: Display>(&self, value: U) {
println!("{:?} / {}", self.x, value);
}
}
U は構造体の T とは完全に独立しており、制約も別々に付けられます。ここでは U に Display(通常の {} での表示が可能)の制約を与えているので {} で表示でき、T には Debug の制約があるので {:?} で表示できます。境界さえ満たしていれば、構造体の型とは別の型を渡せます。
特定の型のときだけ使えるメソッド
もう少し特殊な定義として、特定の型の場合のみ使えるメソッドも書けます。この場合、impl の後ろにジェネリック型は書かず、構造体名の後ろに具体的な型名を書きます。
impl Point<i32> {
fn min(&self) -> &i32 {
if self.x < self.y { &self.x } else { &self.y }
}
}
この impl ブロックで定義したメソッドは、Point<i32> でのみ使えます。
let p1 = Point { x: 1, y: 5 }; // Point<i32>
let p2 = Point { x: 1.5, y: 0.5 }; // Point<f64>
println!("{:?}", p1.min()); // OK
// println!("{:?}", p2.min()); // エラー: Point<f64> に min はない
第7章:derive 属性
属性とは
Rust における属性とはメタデータのことで、さまざまな宣言に付与できます。書き方は、対象の直前に #[属性名] です。
derive 属性を使うと、標準的によく使われるトレイトの機能を、impl を書かずに自動生成できます。
derive は「継承」ではありません。Rust にクラスの継承はなく、derive は「そのトレイトの標準的な実装をコンパイラに自動生成させる」仕組み(マクロ)です。親から性質を受け継ぐのではなく、定型的な実装を書く手間を省くもの、と理解してください。
Debug の例
タプルのように複数の要素を持つ型は {:?} で全体を表示できました。同じことを構造体でやろうとするとエラーになります。
struct S {
value_one: i32,
value_two: i32,
}
fn main() {
let s1 = S { value_one: 1, value_two: 2 };
println!("{:?}", s1); // エラー: S は Debug を実装していない
}
エラーメッセージには「Debug トレイトが実装されていない」と書かれています。そこで構造体定義の上に derive 属性を付けます。
#[derive(Debug)]
struct S {
value_one: i32,
value_two: i32,
}
これでエラーが消え、S { value_one: 1, value_two: 2 } のように全要素が表示されます。derive の引数に渡したトレイトの共通機能が、構造体に対して実装されたわけです。
もちろん impl で独自のデバッグ表示を実装することもできますが、特別な必要がなければ derive のほうが手軽です。
PartialEq の例
等価性を比較する PartialEq も見てみましょう。
fn main() {
let s1 = S { value_one: 1, value_two: 2 };
let s2 = S { value_one: 1, value_two: 3 };
println!("{}", s1 == s2); // エラー: PartialEq が必要
}
derive にはカンマ区切りで複数のトレイトを渡せます。
#[derive(Debug, PartialEq)]
struct S {
value_one: i32,
value_two: i32,
}
これで s1 == s2 が動き、value_two が異なるので false が表示されます。値を揃えれば true になります。
derive できる主なトレイト
| トレイト | 提供される機能 |
|---|---|
Debug |
{:?} でのデバッグ表示 |
Clone |
clone() による明示的な複製 |
Copy |
ムーブではなくコピーのセマンティクス |
PartialEq / Eq
|
== != による等価比較 |
PartialOrd / Ord
|
< > <= >= による大小比較 |
Hash |
ハッシュ値の計算(HashMap のキーに必要) |
Default |
Default::default() による既定値の生成 |
これらには依存関係があります。
-
PartialOrdはPartialEqも必要 -
OrdはPartialOrdとEq(さらにEqはPartialEq)も必要 -
CopyはCloneも必要 - 浮動小数点数は
NaNがあるためEqとOrdを実装できません(PartialEq/PartialOrdは可能)
また、derive は各フィールドに対して同じトレイトを要求します。Clone を derive するなら、全フィールドが Clone である必要があります。
第8章:練習問題
トレイトとジェネリクスは、エラーメッセージが「何が足りないか」を的確に教えてくれる分野です。cargo run で確かめながら進めてください。解答例は折りたたんであります。
問題1(第1章・第2章:トレイトの定義と実装)
あいさつを表す Greet トレイトを定義し、Dog と Cat の2つの構造体に実装してください。トレイトには「鳴き声を返す sound メソッド(戻り値は String)」を1つ定義すること。
解答例
trait Greet {
fn sound(&self) -> String;
}
struct Dog;
struct Cat;
impl Greet for Dog {
fn sound(&self) -> String {
String::from("ワン")
}
}
impl Greet for Cat {
fn sound(&self) -> String {
String::from("ニャー")
}
}
fn main() {
let d = Dog;
let c = Cat;
println!("{} {}", d.sound(), c.sound());
}
フィールドを持たない構造体は struct Dog; と書けます(ユニット構造体)。トレイトに定義したメソッドは、実装側ですべて実装する必要があります。
問題2(第2章:トレイトのインポート)
別のクレートで定義された Shape トレイトと Rectangle 型を使おうとして、次のように書いたところ「メソッドが見つからない」というエラーになりました。原因と修正方法を答えてください。
use rust_lesson::sample_trait::Rectangle;
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
println!("{}", rect.calc_area());
}
解答例
calc_area は Rectangle 自身のメソッドではなく、Shape トレイトのメソッドだからです。トレイトのメソッドを呼ぶには、トレイト自体もスコープに入れる必要があります。
use rust_lesson::sample_trait::{Rectangle, Shape};
fn main() {
let rect = Rectangle { width: 3.0, height: 4.0 };
println!("{}", rect.calc_area());
}
use rand::RngExt; を書かないと random_range が呼べなかったのも、まったく同じ理由です。
問題3(第3章:デフォルト実装)
次のコードの出力を予想してから、実行して確かめてください。
trait Greet {
fn name(&self) -> String {
String::from("名無し")
}
}
struct Dog;
struct Cat;
impl Greet for Dog {
fn name(&self) -> String {
String::from("ポチ")
}
}
impl Greet for Cat {}
fn main() {
println!("{}", Dog.name());
println!("{}", Cat.name());
}
解答例
ポチ
名無し
Dog は name を上書きしているので独自の値が返ります。Cat は impl Greet for Cat {} と中身を空にしているため、デフォルト実装がそのまま使われます。
デフォルト実装があるメソッドは、実装を省略しても「メソッドが足りない」というエラーになりません。ここがデフォルト実装のない場合との大きな違いです。
問題4(第4章:impl Trait 引数)
次の2つの関数は、中身がまったく同じです。Shape トレイトを使って1つの関数にまとめてください。
fn print_rect_area(shape: &Rectangle) {
println!("{}", shape.calc_area());
}
fn print_circle_area(shape: &Circle) {
println!("{}", shape.calc_area());
}
解答例
fn print_area(shape: &impl Shape) {
println!("{}", shape.calc_area());
}
&impl Shape は「Shape を実装した任意の型の共有参照」を意味します。これで Rectangle でも Circle でも、今後追加する図形でも、この1つの関数で対応できます。
ジェネリクスで書いても同じ意味になります。
fn print_area<T: Shape>(shape: &T) {
println!("{}", shape.calc_area());
}
問題5(第5章:ジェネリクスとトレイト境界)
次のコードはコンパイルエラーになります。理由を説明し、2通りの書き方(山括弧の中と where 句)で修正してください。
fn largest<T>(a: T, b: T) -> T {
if a > b { a } else { b }
}
解答例
T は任意の型なので、大小比較できない型が渡される可能性があります。そのため比較演算子 > が使えず、エラーになります。「比較できる型に限る」というトレイト境界が必要です。
// 方法1: 山括弧の中に書く
fn largest<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// 方法2: where 句で書く
fn largest<T>(a: T, b: T) -> T
where
T: PartialOrd,
{
if a > b { a } else { b }
}
制約がシンプルなうちは方法1、ジェネリック型が増えたり制約が複雑になったら方法2、と使い分けると読みやすくなります。
問題6(第6章:ジェネリック構造体)
次の Pair<T> に、Pair<i32> のときだけ使える sum メソッド(2つのフィールドの合計を返す)を追加してください。
struct Pair<T> {
first: T,
second: T,
}
解答例
struct Pair<T> {
first: T,
second: T,
}
impl Pair<i32> {
fn sum(&self) -> i32 {
self.first + self.second
}
}
fn main() {
let p1 = Pair { first: 1, second: 2 };
println!("{}", p1.sum()); // 3
let p2 = Pair { first: 1.5, second: 2.5 };
// println!("{}", p2.sum()); // エラー: Pair<f64> に sum はない
}
特定の型限定のメソッドを書くときは、impl の後ろに <T> を書かず、構造体名の後ろに具体的な型名を書きます。impl<T> Pair<T> と impl Pair<i32> の違いがポイントです。
問題7(第7章:derive 属性)
次のコードは2箇所でコンパイルエラーになります。derive 属性を使って修正してください。
struct Book {
title: String,
pages: u32,
}
fn main() {
let b1 = Book { title: String::from("Rust"), pages: 300 };
let b2 = Book { title: String::from("Rust"), pages: 300 };
println!("{:?}", b1);
println!("{}", b1 == b2);
}
解答例
{:?} での表示には Debug、== での比較には PartialEq が必要です。どちらも実装されていないためエラーになります。
#[derive(Debug, PartialEq)]
struct Book {
title: String,
pages: u32,
}
Book { title: "Rust", pages: 300 }
true
derive にはカンマ区切りで複数のトレイトを渡せます。なお derive は各フィールドにも同じトレイトを要求しますが、String も u32 も Debug と PartialEq を実装しているので問題ありません。
総合問題:図形を扱うプログラムを作る
次の要件を満たすプログラムを書いてください。
-
Shapeトレイトを定義し、面積を返すareaメソッドと、名前を返すnameメソッド(デフォルト実装で"図形"を返す)を持たせる -
Rectangle(幅・高さ)とCircle(半径)にShapeを実装する。nameはRectangleだけ"長方形"に上書きする - 図形を受け取って「名前: 面積」を表示する関数を、トレイトを実装した任意の型を受け取れる形で1つだけ定義する
-
Rectangleは{:?}で表示できるようにする
解答例
use std::f64::consts::PI;
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String {
String::from("図形")
}
}
#[derive(Debug)]
struct Rectangle {
width: f64,
height: f64,
}
struct Circle {
radius: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> String {
String::from("長方形")
}
}
impl Shape for Circle {
fn area(&self) -> f64 {
self.radius * self.radius * PI
}
// name は省略 → デフォルト実装が使われる
}
fn print_shape(shape: &impl Shape) {
println!("{}: {:.2}", shape.name(), shape.area());
}
fn main() {
let r = Rectangle { width: 3.0, height: 4.0 };
let c = Circle { radius: 2.0 };
print_shape(&r);
print_shape(&c);
println!("{:?}", r);
}
長方形: 12.00
図形: 12.57
Rectangle { width: 3.0, height: 4.0 }
ポイントは4つです。
-
nameにデフォルト実装があるので、Circle側では省略できます -
print_shape(&impl Shape)により、図形の種類が増えても関数は1つのままです -
{:?}を使うために#[derive(Debug)]が必要です -
use std::f64::consts::PI;でインポートすれば、std::f64::consts::PIとフルパスで書かずに済みます
まとめ:トレイトとジェネリクス チェックリスト
- トレイトはメソッドのシグネチャだけを定義するもの
-
実装は
impl トレイト名 for 型名。トレイトのメソッドは原則すべて実装が必要 -
トレイトのメソッドを呼ぶには、型だけでなくトレイトも
useする - デフォルト実装があるメソッドは、省略も上書きもできる
-
&impl Traitは「そのトレイトを実装した任意の型」を受け取る書き方 -
実行時に型を切り替えたいなら
&dyn Trait/Box<dyn Trait>(トレイトオブジェクト) -
ジェネリクスは
fn max<T>(...)。慣習として型名にはTを使う -
トレイト境界は
<T: Trait>かwhere T: Trait。複数なら+でつなぐ -
Option<T>はジェネリックな列挙型の代表例 -
ジェネリックな構造体のメソッドは、境界を
implの直後の<T>に書く -
impl Point<i32>と書けば、その型のときだけ使えるメソッドになる -
#[derive(...)]はトレイト実装の自動生成であり、継承ではない -
derive できる主なもの:
DebugCloneCopyPartialEqEqPartialOrdOrdHashDefault
最後に
プログラミングの初級レベルの問題に取り組めるドリルアプリを作りました。
Rustも取り扱っているので、ぜひアウトプットに活用してみて下さい!