はじめに
こんにちは。細々とプログラミングをしているsotanengelです。
この記事は以下の記事の連載です。
他の連載記事 (詳細)
- Day 1:型システムを使ってデータ構造を再現しよう
- Day 2:型システムを用いて共通の挙動を表現しよう
- Day 3:OptionとResultに対してはmatchを用いずに変換しよう
- Day 4:標準のErrorを使おう
- Day 5:型変換を理解しよう
- Day 6:newtypeパターンを活用しよう
- Day 7:複雑な型にはビルダを使おう
- Day 8:明示的なループの代わりにイテレータ変換を使用することを検討しよう
- Day 9:標準トレイトに習熟しよう
- Day 10:RIIパターンにはDropトレイトを実装しよう
- Day 11:ジェネリクスとトレイトオブジェクトのトレードオフを理解しよう
- Day 12:デフォルト実装を用いて、実装しなければならないトレイトメソッドを最小限にしよう
- Day 13:Don't panic
- Day 14:リフレクションを避けよう
- Day 15:可視範囲を最小化しよう
また本記事はEffective Rust(David Drysdale (著), 中田 秀基 (翻訳))を参考に作成されております。とてもいい書籍ですので興味を持った方は、ぜひ読んでみてください!
今日の内容
概要
Rustでは既存の型を一つだけ持つタプル構造体のことをnewtypeパターン
と呼んでいます。
うまく活用することでコンパイル時にエラーを発見することができます。
newtypeパターンを使って意図しないバグをコンパイル時に発見しよう
問(リンク)
メートルと重さを管理する独自の型を実装し、既存の型(f64
)を使う場合よりも安全なコードを書いてみましょう。
コード (詳細)
// TODO: 長さを表すメートルの型を定義しましょう。
// TODO: 重さを表すキログラムの型を定義しましょう。
// f64 から Meters への変換
impl From<f64> for Meters {
fn from(value: f64) -> Self {
Meters(value)
}
}
// f64 から Kilograms への変換
impl From<f64> for Kilograms {
fn from(value: f64) -> Self {
Kilograms(value)
}
}
// メートルをスケーリングする関数
fn scale_meters(m: Meters, factor: f64) -> Meters {
Meters(m.0 * factor)
}
// キログラムをグラムに変換する関数
fn convert_to_grams(k: Kilograms) -> f64 {
k.0 * 1000.0
}
fn main() {
let height = Meters(1.75);
let weight = Kilograms(68.0);
let double_height = scale_meters(height, 2.0);
println!("Double height: {} m", double_height.0);
let weight_in_grams = convert_to_grams(weight);
println!("Weight in grams: {} g", weight_in_grams);
}
/// テストモジュール
/// コンパイルエラーを期待するテスト
///
/// ```compile_fail
/// let m = Meters::from(3.0);
/// // Kilograms 型を要求している関数に Meters を渡す -> エラー
/// let _ = convert_to_grams(m);
/// ```
fn compile_fail_example() {}
解答(リンク)
コード参照。
コード (詳細)
// 長さを表すメートルの型
#[repr(transparent)] // この属性を追加することでメモリ表現を内部の型(f64)と同じにすることができ、効率化になる。
struct Meters(f64);
// 重さを表すキログラムの型
#[repr(transparent)]
struct Kilograms(f64);
// f64 から Meters への変換
impl From<f64> for Meters {
fn from(value: f64) -> Self {
Meters(value)
}
}
// f64 から Kilograms への変換
impl From<f64> for Kilograms {
fn from(value: f64) -> Self {
Kilograms(value)
}
}
// メートルをスケーリングする関数
fn scale_meters(m: Meters, factor: f64) -> Meters {
Meters(m.0 * factor)
}
// キログラムをグラムに変換する関数
fn convert_to_grams(k: Kilograms) -> f64 {
k.0 * 1000.0
}
fn main() {
let height = Meters(1.75);
let weight = Kilograms(68.0);
let double_height = scale_meters(height, 2.0);
println!("Double height: {} m", double_height.0);
let weight_in_grams = convert_to_grams(weight);
println!("Weight in grams: {} g", weight_in_grams);
}
/// テストモジュール
/// コンパイルエラーを期待するテスト
///
/// ```compile_fail
/// let m = Meters::from(3.0);
/// // Kilograms 型を要求している関数に Meters を渡す -> エラー
/// let _ = convert_to_grams(m);
/// ```
#[allow(dead_code)]
fn compile_fail_example() {}
トレイト孤児ルールを回避しよう
問(リンク)
Rustではある型にトレイトを実装するためには以下の条件を満たす必要がある。
- そのクレートでそのトレイトを定義している
- そのクレートでその型を定義している
そこでnewtype
を使うことでトレイト孤児ルールを回避しましょう。
コード (詳細)
struct MyVec(Vec<i32>);
trait Sum {
fn sum(&self) -> i32;
}
// TODO: 自分で作成したnewTypeにSumトレイトを実装しましょう。
fn main() {
let numbers = MyVec(vec![1, 2, 3, 4]);
println!("Sum: {}", numbers.sum());
}
解答(リンク)
コード参照。
コード (詳細)
struct MyVec(Vec<i32>);
trait Sum {
fn sum(&self) -> i32;
}
// 自分で作成したnewTypeのため、Sumを実装することができる。
impl Sum for MyVec {
fn sum(&self) -> i32 {
self.0.iter().sum()
}
}
fn main() {
let numbers = MyVec(vec![1, 2, 3, 4]);
println!("Sum: {}", numbers.sum());
}
さいごに
もしも本リポジトリで不備などあれば、リポジトリのissueやPRなどでご指摘いただければと思います。