30 Days of Rust - Day 30
これがそれです、最終ブログです。長くて厳しいので、読むのに少し時間を取ってください。心配しないでください、私はまだブログを投稿し続けますが、おそらくRust言語の機能に関してではないでしょう - 私たちは今のところそれから休憩が必要だと思います。
昨日の質問に答えました
答える質問はありません
今日のオープンな質問
オープンな質問はありません
関連型とジェネリクス
Rustでは、ジェネリクスが関数やトレイトの再利用性を向上させる方法として既に見てきました。関連型は再利用性に別のアプローチを提供します。特定のトレイトを与えられたタイプに対して1回だけ実装する必要がある場合があります。これがトレイトに関連型の定義を持つ際により有用になるかもしれません。Rustの本からこの例を考えてみてください:
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Add
の実装ではタイプOutput
を宣言しています。実際にやっていることはAdd
トレイトの関連型を定義していることです。これがなければ、コードはコンパイルできません。関連型はこの関数で契約として機能します。トレイトの定義によると、関連型はadd
関数の戻り値として機能します。こちらがトレイトの宣言です:
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
Add
トレイトの興味深い点は、デフォルトの関連型も持っていることです。これはRhs
別名入力引数によって表されます。
この文脈でデフォルトのタイプを提供することは、ライブラリの作者に柔軟性を提供します:ライブラリの多くのユーザーは一通りの方法でトレイトを使用することが多いです。この典型的な使用パターンはトレイトのデフォルトタイプを通じて表現されることができます。しかしながら、これをパラメーターとして設定することによって、ライブラリを予見していなかった方法で使用するチャンスをパワーユーザーに提供することができます。
関数と関連関数の名前が重複しているため、完全修飾構文が必要です
RustではPythonのように、オブジェクトに同じ名前のメソッドを複数定義することに制限はありません。それはおそらく、過負荷のメソッドを除いて、そのユーザーがそうすることを意図したプログラミング言語はないからでしょう。
しかし、Rustでは、struct
が同じ名前の複数のメソッドまたは関連関数を持っているという比較的一般的な状況があります。外部ライブラリからtrait
を実装する際、既に定義しているメソッド名を使用することが強制されるかもしれません。
メソッドコールのための構文
多スレッドについて話したときを思い出してください。私たちはSend
(およびSync
)トレイトについて簡単に話しました。標準ライブラリからだけでなく、異なる方法でスレッドをインスタンス化するサードパーティライブラリからもSend
を実装したコードを想像してみてください。突然、send
メソッドの2つのバージョンがあります。
これらのメソッドを呼び出すとき、完全修飾構文を使用する必要があります。これはtrait
とmethod name
から構成されます:
let thread_struct = MyCustomThread::new();
StdLibrarySendTrait::send(&thread_struct);
ExternalLibrarySendTrait::send(&thread_struct);
関連関数のための構文
struct
のメソッドが最初の引数としてstruct
への参照を含むため、完全修飾構文を使用してメソッドを呼び出すとき、Rustはself
のタイプを知っているのでどの実装ブロックが呼ばれているかわかります:
impl ExternalLibrarySendTrait for MyCustomThread {
fn send(&self) {
// 何かする
}
}
impl ExternalLibrarySendTrait for SomeOtherType {
fn send(&self) {
// 何かする
}
}
ExternalLibrarySendTrait::send(&thread_struct);
しかし、self
を取らない関連関数の場合は異なります:
trait Animal {
fn baby_name() -> String;
}
struct Dog;
struct Cat;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
impl Animal for Cat {
fn baby_name() -> String {
String::from("kitten")
}
}
Dog::baby_name() // 問題なし
Animal::baby_name() // CatまたはDog?
ここでは、メソッドに対する完全修飾構文が不十分であること(また、コードはコンパイルされません)がわかります。実際には、_完全_修飾構文を使用していなかったからです。以下は上記のコードをコンパイル可能にする完全修飾構文の使用方法です:
<Dog as Animal>::baby_name() // 子犬です
<Cat as Animal>::baby_name() // 子猫です
この構文はメソッドにも使用できますが、非常に冗長です。
スーパートレイト:トレイトの依存関係を宣言する
trait
を定義する際に、任意のsupertrait
を宣言することができ、それが実装されなければそのtrait
は期待通りに動作しません。supertrait
を実装していないstruct
を使用すると、コードのコンパイルが失敗します:
trait PrettyPrint: fmt::Display {
// 何かする
}
このコードはDisplay
を実装するstructs
上でのみ実装できるtrait
を宣言します。
スーパートレイトは、コードの重複を減らすのを助けることができますが、コードの結合と複雑さを増加させるコストがかかります。
タイプの魔法
これまでtype
キーワードについてほとんど触れていませんでした。このキーワードはタイプエイリアスを宣言するために使用されます:
type Kilometer = i32;
重要なのはKilometer
がまだi32
であるということです:
let a: Kilometer = 1;
let b = 1;
asserteq!(a + b, 2);
Rustでは newtype パターンがよく見られます。これはRustのtrait
実装ルールのために人気があります:
トレイトは、そのトレイト または タイプがあなたのコードにローカルである場合にのみ、タイプに対して実装することができます。
上記のコードをnewtypeパターンを使用して実装できます:
struct Kilometer(i32);
let a = Kilometer(1);
let b = 1;
asserteq!(a + b, 2);
このコードはコンパイルできません。なぜならKilometer
はAdd
トレイトを実装していないからです。i32
はそのトレイトを実装しており、このことがtype
エイリアスが本当にi32
を囲むエイリアスであることの証明です。
エイリアスとnewtypeをいつ使うか?
エイリアスは次の場合に意味があります:
- 長いタイプ(入れ子になったResultやOptionsなど)を短縮できる場合
- その一部が常に同じである複合型で、他の部分をジェネリックとして扱うべき場合
- 既存のタイプ(例えば
i32
)に意味のある名前を与えることで、コードを読みやすくすることができる場合
_Newtype_は次の場合に意味があります:
- 外部のトレイトを実装する必要がある場合
- パブリックAPIを使用して内部コードへのアクセスを制御する場合
あなたが知らなかったタイプ
機能が!
を返す場合、いわゆる_never_タイプを返します。_never_タイプはRustの_empty_タイプの別名です。neverタイプのポイントは、戻り値がない関数を表現することです。
このような関数は、他のどのタイプにも強制できる結果を生成します。これは、match
ステートメントを使用するときに便利です。これらは常に1つのタイプを返さなければなりません。continue
、panic!
、loop
などのキーワードとマクロはneverタイプを返します。
サイズのあるタイプ
サイズのあるタイプに関する短いメモ。デフォルトでは、ジェネリックな関数は_サイズのある_タイプしか取れません。タイプがコンパイル時にそのサイズがわかっている場合、そのタイプは_サイズのある_ものです。サイズのあるタイプはデフォルトでSized
トレイトを実装しています。
ジェネリックで動的サイズの型を使用したい場合は、?Trait
表記を使用する必要があります:
fn generic<T: ?Sized>(t: &T) {}
動的サイズの型も参照を介してヒープ上に保存する必要があるため、動的サイズの型への参照も受け取ることに注意してください。
関数の受け渡しと返戻
fn
タイプを使用することで関数を簡単に関数に渡すことができます。これは引数としてclosure
が渡されるべきことを示すために使用されるtrait
制約と異なります。fn
型のものは3つのFn
トレイトをすべて実装するため、関数を受け入れる場所では常に関数を渡すことができます。
関数からclosure
を返す場合、closure
のサイズは実行時に決定されるため、それをBox
でラップする必要があります。
最後に:マクロ
最も高度なRust機能はmacro
です。これらはRustコードを使用してさらに多くのRustコードを生成する関数です。このパターンは一般に_metaprogramming_として知られています。
Rustには異なる種類のマクロがあります。これらは異なる実装の詳細を持ち、それぞれが異なるユースケースに適しています。
macro_rules!
マクロ
Rustで最も一般的なマクロのタイプは宣言的マクロです。これはmacro_rules!
マクロを使用して作成されます。宣言的マクロでは、Rustソースコードをパターンに一致させ、一致するパターンの種類に基づいて異なる種類のコードを生成することができます。これにより、これらのマクロは定義されていない数の引数を取ることができます。
手続き型マクロ
残りの3種類のマクロは手続き型です。これらのマクロはRustソースコードを入力として取り、それを返さなければなりません。
これらの最初のものは_derive_マクロです。deriveマクロを作成するには、標準ライブラリのproc_macro_derive
マクロからderiveする必要があります。Deriveマクロは**structs
とenums
**にのみ機能します。
Deriveマクロはパッケージングに関するいくつかの制約があります。このようなマクロを開発するとき、開発者はそれをメインライブラリの依存関係として含めることを奨励されます。その後、マクロは単にメインライブラリの一部として再エクスポートされ、ユーザーがインポートするために使われます。
Attributeマクロはderiveマクロに似ていますが、関数や他のデータ型などにも使用できるようにもっと柔軟性があります。これらはproc_macro_attribute
マクロから派生します。
最後に、関数のようなマクロがあります。手続き的な性質にもかかわらず
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/johnnylarner/30-days-of-rust-day-30-4dan