オンライン読書アドレス:Rust 言語聖典
読書進捗:60%
開発ツール#
VSCode#
プラグイン
- rust-analyzer、Rust 言語プラグイン
- Even Better TOML、.toml ファイルの完全な機能をサポート
- Error Lens、エラー表示をより良くする
- CodeLLDB、デバッガプログラム
RustRover#
プラグイン
設定
- コードフォーマット
Cargo#
cargo
ツールを使用する最大の利点は、プロジェクトのさまざまな依存関係を便利で統一的かつ柔軟に管理できることです。
Cargo.toml と Cargo.lock#
Cargo.toml
はcargo
特有のプロジェクトデータ記述ファイルです。プロジェクトのすべてのメタ設定情報を保存しており、Rust 開発者が Rust プロジェクトを期待通りに構築、テスト、実行したい場合、合理的な方法でCargo.toml
を構築する必要があります。Cargo.lock
ファイルはcargo
ツールが同じプロジェクトのtoml
ファイルに基づいて生成したプロジェクト依存詳細リストであるため、一般的には変更する必要はなく、Cargo.toml
ファイルに従って作業すれば良いです。
Cargo.toml
では、さまざまな依存段落を通じてプロジェクトのさまざまな依存関係を記述します:
[dependencies]
rand = "0.3"
hammer = { version = "0.5.0"} // Rust 公式リポジトリ crates.io に基づき、バージョン指定で記述
color = { git = "https://github.com/bjz/color-rs" } // プロジェクトソースコードの git リポジトリアドレスに基づき、URL で記述
geometry = { path = "crates/geometry" } // ローカルプロジェクトの絶対パスまたは相対パスに基づき、Unix 形式のパスで記述
-
Cargo.toml ファイル内容
Cargo.toml
ファイルは Rust プロジェクトで依存関係、コンパイルオプション、メタデータなどの情報を構成および管理するための設定ファイルです。以下はCargo.toml
ファイルの一般的な部分とその用途です:1.
[package]
#この部分はパッケージの基本情報を定義します。名前、バージョン、著者、説明などを含みます。
[package] name = "my_project" # プロジェクト名 version = "0.1.0" # プロジェクトバージョン edition = "2021" # Rust 言語バージョン authors = ["Your Name <[email protected]>"] # 著者 description = "A brief description of the project" # プロジェクト説明 license = "MIT" # プロジェクト使用ライセンス repository = "<https://github.com/yourusername/my_project>" # プロジェクトソースコードリポジトリ
2.
[dependencies]
#この部分では、プロジェクトが依存するライブラリとそのバージョンを定義します。
[dependencies] serde = "1.0" # serde ライブラリを依存として追加 rand = { version = "0.8", features = ["std"] } # バージョンと追加機能を指定
3.
[dev-dependencies]
#この部分は開発環境で必要な依存関係を定義します。これらの依存関係は通常、テストや開発ツールでのみ使用されます。
[dev-dependencies] tokio = { version = "1", features = ["full"] } # 開発段階でのみ使用
4.
[build-dependencies]
#この部分はビルドスクリプト (
build.rs
) 内の依存関係を定義します。[build-dependencies] cc = "1.0" # ネイティブコードをビルドするためのライブラリ
5.
[features]
#プロジェクトのオプション機能を定義します。これらの機能はユーザーが選択的に有効または無効にできます。
[features] default = ["serde"] # デフォルトで有効な機能 extras = ["tokio"] # オプション機能
6.
[package.metadata]
#この部分は通常、カスタムツールの設定に使用され、その内容と構造はツールによって異なります。
[package.metadata] docs = { rsdoc = true } # カスタムドキュメント生成器の設定
7.
[workspace]
#プロジェクトがワークスペース(複数のサブプロジェクトを含む)である場合、この部分を使用してワークスペースの構造を定義します。
[workspace] members = ["project1", "project2"] # ワークスペース内のメンバプロジェクト
8.
[patch]
と[replace]
#依存関係のバージョンを置き換えたり修正したりするために使用され、通常は依存関係の衝突や開発中に使用されます。
[patch.crates-io] serde = { git = "<https://github.com/serde-rs/serde>", branch = "master" } [replace] foo = { path = "../foo" } # 依存ライブラリを置き換え
9.
[profile]
#この部分では、異なるコンパイル設定(例えば
dev
,release
)のためにコンパイルオプションをカスタマイズできます。[profile.dev] opt-level = 1 # 開発モードで使用する最適化レベル [profile.release] opt-level = 3 # リリースモードで使用する最適化レベル
10.
[badges]
#ドキュメント生成ツールや他のサービスでステータスバッジ(ビルドステータス、コードカバレッジなど)を表示するために使用されます。
[badges] travis-ci = { repository = "rust-lang/crates.io" } # Travis CI バッジを表示
これは
Cargo.toml
ファイルの一般的な部分とその用途です。プロジェクトの複雑さやニーズに応じて、ファイルの内容はさらに豊富またはシンプルになることがあります。
ミラー加速#
依存関係のダウンロードが遅い? - Rust 言語聖典 (Rust Course)
[http]
check-revoke = false
# proxy = "http://x.x.x.x:8888" // 仮想マシン内でネットワークに接続されていない開発時にプロキシとして使用
[source.crates-io]
replace-with = 'rsproxy'
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
ジェネリクスとトレイト#
ジェネリクスは多態性の一種です。ジェネリクスの主な目的は、プログラマーにプログラミングの便利さを提供し、コードの冗長性を減らし、同時に言語自体の表現力を大幅に豊かにすることです。
- 列挙型
enum Option<T> {
Some(T),
None,
}
- 構造体
struct Point<T> {
x: T,
y: T,
}
- メソッド
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
ジェネリクスの性能#
Rust におけるジェネリクスはゼロコストの抽象であり、ジェネリクスを使用する際に性能の問題を心配する必要はありませんが、コンパイル速度が低下し、最終的に生成されるファイルのサイズが大きくなります。
Rust はコンパイル時にジェネリックコードの単相化(monomorphization) を行うことで効率を保証します。単相化は、コンパイル時に使用される具体的な型を埋め込むことによって、一般的なコードを特定のコードに変換するプロセスです。
コンパイラが行う作業は、私たちがジェネリック関数を作成するステップと正反対であり、コンパイラはすべてのジェネリックコードが呼び出される場所を探し、具体的な型に対してコードを生成します。
トレイトの多重制約#
pub fn notify(item: &(impl Summary + Display)) {} // シンタックスシュガー形式
pub fn notify<T: Summary + Display>(item: &T) {} // トレイト制約の形式
Where 制約
トレイト制約が多くなると、関数のシグネチャが複雑になります:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
where
を使って形式的に改善します:
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
動的配列#
https://doc.rust-lang.org/std/collections/
作成#
let mut v1 = Vec::with_capacity(10);
v1.extend([1, 2, 3]); // v にデータを追加
println!("Vector の長さは: {}, 容量は: {}", v.len(), v.capacity());
よく使うメソッド#
let mut v = vec![1, 2];
assert!(!v.is_empty()); // v が空でないことを確認
v.reserve(100); // v の容量を調整し、少なくとも 100 の容量を確保
v.insert(2, 3); // 指定インデックスにデータを挿入、インデックス値は v の長さを超えてはいけない、 v: [1, 2, 3]
assert_eq!(v.remove(1), 2); // 指定位置の要素を削除し、返す, v: [1, 3]
assert_eq!(v.pop(), Some(3)); // v の末尾の要素を削除し、返す、v: [1]
assert_eq!(v.pop(), Some(1)); // v: []
assert_eq!(v.pop(), None); // pop メソッドが返すのは Option 列挙値であることを忘れないでください
v.clear(); // v をクリア、v: []
let mut v1 = [11, 22].to_vec(); // append 操作は v1 のデータをクリアし、可変宣言を追加
v.append(&mut v1); // v1 のすべての要素を v に追加, v1: []
v.truncate(1); // 指定長さに切り詰め、多すぎる要素が削除される, v: [11]
v.retain(|x| *x > 10); // 条件を満たす要素を保持し、条件を満たさない要素を削除
let mut v = vec![11, 22, 33, 44, 55];
// 指定範囲の要素を削除し、削除された要素のイテレータを取得, v: [11, 55], m: [22, 33, 44]
let mut m: Vec<_> = v.drain(1..=3).collect();
let v2 = m.split_off(1); // 指定インデックスで二つの vec に分割, m: [22], v2: [33, 44]
let v2 = vec![11, 22, 33, 44, 55]; // スライスを使って配列を切り取る
let slice = &v2[1..=3];
assert_eq!(slice, &[22, 33, 44]);
ソート#
fn main() {
let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}
構造体のソート
// 年齢の降順でソートする比較関数をカスタマイズ
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));
ソートには Ord
トレイトを実装する必要があります。構造体がこのトレイトを実装している場合、カスタム比較関数を作成する必要はありません。ただし、Ord
を実装するには、Ord
、Eq
、PartialEq
、PartialOrd
などの属性を実装する必要があり、これらの属性は derive
できます:
#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)] // derive 属性
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("Al".to_string(), 30),
Person::new("John".to_string(), 1),
Person::new("John".to_string(), 25),
];
people.sort_unstable();
println!("{:?}", people);
}
// => [Person { name: "Al", age: 30 }, Person { name: "Al", age: 60 }, Person { name: "John", age: 1 }, Person { name: "John", age: 25 }, Person { name: "Zoe", age: 25 }]
derive
のデフォルト実装は、属性の順序に従って比較を行います。上記の例では、Person
の name
値が同じ場合、age
で比較されます。
HashMap#
作成#
fn main() {
use std::collections::HashMap;
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
println!("{:?}",teams_map)
}
ライフタイム🌟#
懸垂ポインタ#
ライフタイムの主な役割は、懸垂参照を避けることです。懸垂参照は、プログラムが本来参照すべきでないデータを参照する原因となります。
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
構文#
ライフタイムの構文は '
で始まり、名前は通常単独の小文字の文字です。一般的には 'a
をライフタイムの名前として使用します。参照型のパラメータの場合、ライフタイムは参照記号 &
の後に位置し、ライフタイムと参照パラメータを空白で区切ります:
&i32 // 参照
&'a i32 // 明示的なライフタイムを持つ参照
&'a mut i32 // 明示的なライフタイムを持つ可変参照
関数シグネチャ
ライフタイムパラメータを使用する場合、最初に <'a>
を宣言する必要があります。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
静的ライフタイム
Rust には非常に特別なライフタイム 'static
があります。このライフタイムを持つ参照は、プログラム全体と同じくらい長く生存します。
以前に学んだ文字列リテラルは、Rust のバイナリファイルにハードコーディングされているため、これらの文字列変数はすべて 'static
のライフタイムを持っています:
let s: &'static str = "私には特に優れた点はありませんが、長生きしています、へへ";
ライフタイム消去#
始める前に注意すべき点がいくつかあります:
-
消去規則は万能ではなく、コンパイラが何かが正しいと判断できない場合は、直接不正と見なされます。その場合、手動でライフタイムを注釈する必要があります。
-
関数やメソッド内で、パラメータのライフタイムは「入力ライフタイム」と呼ばれ、戻り値のライフタイムは「出力ライフタイム」と呼ばれます。
-
三つの消去規則
コンパイラは三つの消去規則を使用して、どのシナリオで明示的にライフタイムを注釈する必要がないかを判断します。最初の規則は入力ライフタイムに適用され、二つ目と三つ目は出力ライフタイムに適用されます。コンパイラが三つの規則のいずれも適用できないと判断した場合、エラーが発生し、手動でライフタイムを注釈する必要があることが通知されます。
-
各参照パラメータは独自のライフタイムを持ちます
例えば、参照パラメータの関数には一つのライフタイム注釈があります:
fn foo<'a>(x: &'a i32)
、二つの参照パラメータの関数には二つのライフタイム注釈があります:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
、このように続きます。 -
入力ライフタイムが一つだけの場合(関数パラメータに参照型が一つだけの場合)、そのライフタイムはすべての出力ライフタイムに割り当てられます。つまり、すべての戻り値のライフタイムはその入力ライフタイムと等しくなります。
例えば関数
fn foo(x: &i32) -> &i32
では、x
パラメータのライフタイムが自動的に戻り値&i32
に割り当てられるため、この関数はfn foo<'a>(x: &'a i32) -> &'a i32
と等しくなります。 -
複数の入力ライフタイムが存在し、その中に
&self
または&mut self
が含まれている場合、&self
のライフタイムがすべての出力ライフタイムに割り当てられます&self
形式のパラメータを持つ場合、その関数はメソッドであることを示し、この規則によりメソッドの使用が大幅に便利になります。
-
複雑な例:ジェネリクス、トレイト制約#
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>( // ライフタイムとジェネリクスを宣言
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann); // フォーマット {} を使用して ann を出力するため、Display トレイトを実装する必要があります。
if x.len() > y.len() {
x
} else {
y
}
}
Fn
トレイトでクロージャのライフタイムを解決#
fn main() {
let closure_slision = fun(|x: &i32| -> &i32 { x });
assert_eq!(*closure_slision(&45), 45);
// 合格!
}
fn fun<T, F: Fn(&T) -> &T>(f: F) -> F {
f
}
NLL (Non-Lexical Lifetime)#
以前に参照と借用の章でこの概念について説明しました。簡単に言えば、参照のライフタイムは通常、借用が始まってからスコープが終了するまで持続するべきですが、この規則は複数の参照が共存する場合を複雑にします:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新しいコンパイラでは、r1,r2 のスコープはここで終了します
let r3 = &mut s;
println!("{}", r3);
}
上記の規則に従うと、このコードはエラーになります。なぜなら、r1
と r2
の不変参照は main
関数が終了するまで持続し、その範囲内で r3
の可変参照を借用することは借用の規則に違反するからです:複数の不変借用か、1 つの可変借用のいずれかでなければなりません。
幸いなことに、この規則は 1.31 バージョンから NLL
が導入され、次のように変更されました:参照のライフタイムは借用が始まった時から、最後に使用された場所まで持続します。
最新の規則に従って、上記のコードを再分析します。r1
と r2
の不変借用は println!
の後で使用されなくなるため、そのライフタイムも終了します。したがって、r3
の可変借用は借用の規則に違反しなくなります。
再借用#
https://course.rs/advance/lifetime/advance.html#reborrow - 再借用
- todo
エラーハンドリング#
パニック#
https://course.rs/basic/result-error/panic.html#panic - 原理剖析
バックトレーススタック展開
プログラムが panic
をスローする前にどの呼び出しがあったかを知りたい場合、指示に従って RUST_BACKTRACE=1 cargo run
または $env:RUST_BACKTRACE=1 ; cargo run
を使用してプログラムを実行します。
panic 時の二つの終了方法
panic!
が発生したとき、プログラムは終了プロセスを処理するための二つの方法を提供します:スタック展開と直接終了。
デフォルトの方法は スタック展開
であり、これは Rust がスタック上のデータと関数呼び出しを遡ることを意味します。したがって、より多くの事後処理が必要になります。利点は、十分なエラーメッセージとスタック呼び出し情報を提供し、問題の振り返りに役立つことです。直接終了
は、データをクリーンアップせずにプログラムを直接終了し、事後処理をオペレーティングシステムに任せることを意味します。
ほとんどのユーザーにとって、デフォルトの選択を使用するのが最良ですが、最終的にコンパイルされたバイナリ実行ファイルのサイズが気になる場合は、直接終了の方法を使用することを検討できます。たとえば、次の設定を Cargo.toml
ファイルに追加することで、release
モードで panic
が発生したときに直接終了するようにします:
[profile.release]
panic = 'abort'
パッケージとモジュール#
アイテムの再エクスポート#
外部のモジュールアイテム A
が現在のモジュールにインポートされると、その可視性は自動的にプライベートに設定されます。もし他の外部コードが私たちのモジュールアイテム A
を参照できるようにしたい場合は、それを再エクスポートする必要があります:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
このように、pub use
を使用することで実現できます。ここで use
は hosting
モジュールを現在のスコープにインポートし、pub
はそのインポートされた内容を再度可視化します。
内部の実装詳細を隠したり、特定の目的でコードを整理したりしたい場合は、pub use
を使用して再エクスポートできます。たとえば、あるモジュールを使用して外部に対する API を提供する場合、そのモジュールは他のモジュールからの API をインポートし、再エクスポートすることができます。最終的に、ユーザーにとっては、すべての API が一つのモジュールによって統一的に提供されます。
コメントとドキュメント#
コードコメント#
// 行コメント
/**
ブロックコメント
*/
ドキュメントコメント#
/// `add_one` は指定された値に1を加えます
///
/// # 例
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
/** `add_two` は指定された値に2を加えます
let arg = 5;
let answer = my_crate::add_two(arg);
assert_eq!(7, answer);
*/
pub fn add_one(x: i32) -> i32 {
x + 1
}
一般的なドキュメントタイトル
ドキュメントコメントには # 例
というタイトルの他にもいくつかの一般的なものがあり、プロジェクトに応じて使用できます:
- パニック:関数が発生する可能性のある異常状況。これにより、関数を呼び出す人は事前に回避できます。
- エラー:発生する可能性のあるエラーと、それが発生する状況を説明します。これにより、呼び出し側は異なるエラーに対して異なる処理を行うことができます。
- 安全性:関数が
unsafe
コードを使用する場合、呼び出し側はunsafe
コードブロックが正常に動作するためのいくつかの使用条件に注意する必要があります。
パッケージとモジュールレベルのコメント#
これも行コメント //!
とブロックコメント /*! ... */
に分かれます。
ドキュメントの表示#
cargo doc
cargo doc --open
ドキュメントテスト#
https://course.rs/basic/comment.html# 文档测试 doc-test
テストを保持し、ドキュメントを隠す
ある時点で、ドキュメントテストの機能を保持したいが、特定のテストケースの内容をドキュメントから隠したい場合:
/// ```
/// # // # で始まる行はドキュメントに隠されますが、ドキュメントテストでは実行されます
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // try_main から戻る
/// # }
/// # fn main() {
/// # try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
上記のドキュメントコメントでは、ユーザーに見せたくない内容を隠すために #
を使用しましたが、テストケースの実行には影響しません。最終的にユーザーは、隠されていない行の let res = world_hello::compute::try_div(10, 0)?;
のみを見ることになります:
フォーマット出力#
https://course.rs/basic/formatted-output.html
{} と#
他の言語で一般的に使用される %d
、%s
とは異なり、Rust はフォーマットプレースホルダーとして {}
を選択しました。この選択は非常に正しかったことが証明され、ユーザーは特定の型に特定のプレースホルダーを選択する必要がなくなり、統一して {}
を使用することができ、残りの型推論などの詳細は Rust に任せることができます。
{}
に似た {:?}
もプレースホルダーです:
{}
はstd::fmt::Display
トレイトを実装した型に適用され、より優雅でユーザーフレンドリーな方法でテキストをフォーマットします。{:?}
はstd::fmt::Debug
トレイトを実装した型に適用され、デバッグシーンで使用されます。
両者の選択は簡単です。コードを書く際にデバッグが必要な場合は {:?}
を使用し、残りのシーンでは {}
を選択します。
**Display
トレイト **
ほとんどの型は Debug
を実装していますが、Display
トレイトを実装している Rust 型はそれほど多くなく、通常は自分でフォーマット方法をカスタマイズする必要があります:
let i = 3.1415926;
let s = String::from("hello");
let v = vec![1, 2, 3];
let p = Person {
name: "sunface".to_string(),
age: 18,
};
println!("{}, {}, {}, {}", i, s, v, p);
実行後、v
と p
はコンパイルできないことがわかります。なぜなら、Display
トレイトを実装していないからです。しかし、Debug
のように Display
を派生させることはできず、他の方法を探す必要があります:
{:?}
または{:#?}
を使用- カスタム型に
Display
トレイトを実装 newtype
を使用して外部型にDisplay
トレイトを実装
以下にそれぞれの方法を見ていきましょう。
{:#?}
は {:?}
とほぼ同じですが、唯一の違いは、より美しく内容を出力できることです:
// {:?}
[1, 2, 3], Person { name: "sunface", age: 18 }
// {:#?}
[
1,
2,
3,
], Person {
name: "sunface",
}
したがって、Display
をサポートしていない型に対しては、{:#?}
を使用してフォーマットすることを検討できます。理論的には、デバッグ出力により適しています。
カスタム型に Display
を実装する トレイト **
もしあなたの型が現在のスコープに定義されている場合、その型に Display
トレイトを実装することでフォーマット出力に使用できます:
struct Person {
name: String,
age: u8,
}
use std::fmt;
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"大佬在上,请受我一拜,小弟姓名{},年芳{},家里无田又无车,生活苦哈哈",
self.name, self.age
)
}
}
fn main() {
let p = Person {
name: "sunface".to_string(),
age: 18,
};
println!("{}", p);
}
このように、Display
トレイトの fmt
メソッドを実装することで、カスタム構造体 Person
にカスタム出力を追加できます。