目的別のRustプログラミングテクニック集
変数束縛とミュータビリティ
基本的な束縛
変数はデフォルトでイミュータブル(不変)です。
let x = 5; // イミュータブル
// x = 6; // エラー!再代入不可
ミュータブルな束縛
mut
キーワードでミュータブル(可変)にできます。
let mut y = 10; // ミュータブル
y = 11; // OK!
シャドーイング
同じ変数名で再宣言することで、以前の変数を隠蔽(シャドーイング)できます。型を変えることも可能です。
let z = "hello";
let z = z.len(); // 型が &str から usize に変わる
println!("z = {}", z); // z = 5
定数
常にイミュータブルで、型注釈が必須です。実行時ではなくコンパイル時に値が確定します。
const MAX_POINTS: u32 = 100_000;
データ型
スカラー型
単一の値を表現します。
- 整数型:
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,i128
,u128
,isize
,usize
(符号付きi
, 符号なしu
) - 浮動小数点型:
f32
,f64
(デフォルトはf64
) - 論理値型:
bool
(true
またはfalse
) - 文字型:
char
(Unicode スカラー値、''
で囲む)
let integer: i32 = -10;
let float: f64 = 3.14;
let is_rust_fun: bool = true;
let character: char = '🦀';
複合型
複数の値をグループ化します。
- タプル: 固定長の異種型リスト。
()
で定義。 - 配列: 固定長の同種型リスト。
[]
で定義。
// タプル
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 分解束縛
let five_hundred = tup.0; // インデックスでアクセス
// 配列
let a: [i32; 5] = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
// let error = a[10]; // コンパイル時または実行時エラー (境界外アクセス)
制御フロー
if式
条件に基づいてコードを実行します。if
は式であり、値を返すことができます。
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
// let式でのifの使用
let condition = true;
let value = if condition { 5 } else { 6 };
println!("The value is: {}", value); // The value is: 5
ループ
繰り返し処理を行います。
loop
: 無限ループ。break
で脱出、break value
で値を返すことも可能。while
: 条件がtrue
の間ループ。for
: イテレータを消費してループ。コレクションの反復処理に最適。
// loop
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // ループから値を返す
}
};
println!("The result is {}", result); // The result is 20
// while
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
// for (Range)
for i in 1..4 { // 1, 2, 3 (4は含まない)
println!("The value is: {}", i);
}
// for (Iterator)
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
match式
値が特定のパターンに一致するかどうかを調べ、一致した場合にコードを実行します。網羅的である必要があります。
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
let coin = Coin::Quarter;
println!("Value: {}", value_in_cents(coin)); // Value: 25
// Option<T>でのmatch
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five); // Some(6)
let none = plus_one(None); // None
// _ プレースホルダー (その他の全てのケース)
let dice_roll = 9;
match dice_roll {
3 => println!("Three!"),
7 => println!("Seven!"),
_ => println!("Other number: {}", dice_roll), // 他の全ての数字にマッチ
}
if let / while let
match
の糖衣構文で、特定のパターンにのみ関心がある場合に便利です。
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
// if let
if let Some(color) = favorite_color {
println!("Using your favorite color, {}, as the background", color);
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
// while let (Option)
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() { // popはOption<T>を返す
println!("{}", top); // 3, 2, 1 の順に出力
}
所有権、借用、ライフタイム
所有権 (Ownership)
Rustの最もユニークな機能。メモリ管理の安全性と効率性を保証します。
- 各値は「所有者」と呼ばれる変数を持つ。
- 所有者は常に1つだけ。
- 所有者がスコープから外れると、値は破棄される(メモリ解放)。
{
let s1 = String::from("hello"); // s1が"hello"を所有
let s2 = s1; // s1からs2へ所有権がムーブされる
// println!("{}, world!", s1); // エラー! s1はもはや有効ではない
println!("{}, world!", s2); // OK
} // s2がスコープを抜けるので、"hello"のメモリが解放される
// .clone()でディープコピー (所有権はムーブしない)
let s3 = String::from("world");
let s4 = s3.clone();
println!("s3 = {}, s4 = {}", s3, s4); // OK
スタックのみのデータ(Copy
トレイトを実装する型: 整数、bool、char、タプルなど)はムーブではなくコピーされます。
let x = 5;
let y = x; // xの値がyにコピーされる
println!("x = {}, y = {}", x, y); // OK、両方有効
借用 (Borrowing)
所有権を渡さずに値への参照を貸し出す仕組み。
- 不変参照 (
&T
): 複数同時に存在可能。参照中は元の所有者はデータを変更できない。 - 可変参照 (
&mut T
): 特定のスコープ内で1つだけ存在可能。可変参照がある間は、他のいかなる参照(不変も可変も)も存在できない。
fn calculate_length(s: &String) -> usize { // sはStringへの不変参照
s.len()
} // ここでsはスコープを抜けるが、参照している値は解放されない
fn change(some_string: &mut String) { // 可変参照
some_string.push_str(", world");
}
let mut s = String::from("hello");
let len = calculate_length(&s); // 不変参照を渡す
println!("The length of '{}' is {}.", s, len);
change(&mut s); // 可変参照を渡す
println!("After change: {}", s);
// 参照のルール例
let r1 = &s; // OK
let r2 = &s; // OK (複数の不変参照)
// let r3 = &mut s; // エラー! 不変参照(r1, r2)が存在する間は可変参照を作れない
// let r1 = &mut s; // OK
// let r2 = &mut s; // エラー! 同時に複数の可変参照は作れない
ダングリング参照の防止: Rustコンパイラは、参照が常に有効なデータを指すことを保証します。
// fn dangle() -> &String { // エラー: ダングリング参照を返そうとしている
// let s = String::from("hello");
// &s // sは関数終了時に破棄されるため、この参照は無効になる
// }
fn no_dangle() -> String { // 所有権を返すことで問題を解決
let s = String::from("hello");
s
}
ライフタイム (‘a)
参照が有効であるスコープ(生存期間)をコンパイラに示すための構文。多くの場合、コンパイラが推論するため明示不要(ライフタイム省略規則)。
主に関数や構造体で、参照を含む場合に明示的に指定します。
// 2つの文字列スライスのうち、長い方を返す関数
// xとy、そして戻り値の参照が同じライフタイム'aを持つことを示す
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
let string1 = String::from("abcd");
let string2 = "xyz"; // 文字列リテラルは'staticライフタイムを持つ
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result); // The longest string is abcd
// 構造体でのライフタイム注釈
struct ImportantExcerpt<'a> {
part: &'a str, // この構造体がpartフィールドの参照より長く生存しないことを示す
}
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
'static
ライフタイム: プログラムの実行期間全体で有効な参照。文字列リテラルなど。
構造体と列挙型
構造体 (Struct)
関連するデータをまとめるカスタムデータ型。
- 名前付きフィールド構造体: C言語の構造体に似ています。
- タプル構造体: フィールド名のないタプル風構造体。
- ユニット様構造体: フィールドを持たない構造体。トレイト実装などに利用。
// 名前付きフィールド構造体
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com"); // インスタンスがmutならフィールドも変更可能
// フィールド初期化省略記法 (変数名とフィールド名が同じ場合)
fn build_user(email: String, username: String) -> User {
User {
email, // email: email, と同じ
username, // username: username, と同じ
active: true,
sign_in_count: 1,
}
}
// 構造体更新記法 (他のインスタンスから一部フィールドをコピー)
let user2 = User {
email: String::from("user2@example.com"),
..user1 // user1の残りのフィールドをコピー (username, active, sign_in_count)
// 注意: user1.username はムーブされる (StringはCopyトレイトを実装しないため)
// user1.active, user1.sign_in_count はコピーされる (bool, u64はCopy)
};
// println!("User1 username: {}", user1.username); // エラー! ムーブされた後
// タプル構造体
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
println!("Black color: R={}, G={}, B={}", black.0, black.1, black.2);
// ユニット様構造体
struct AlwaysEqual;
let subject = AlwaysEqual;
// 用途: 特定の型にトレイトを実装したいが、データは不要な場合など
構造体メソッド (impl)
impl
ブロックを使って構造体に関連付けられた関数(メソッド)を定義します。
#[derive(Debug)] // Debugトレイトを自動導出 (println!での表示用)
struct Rectangle {
width: u32,
height: u32,
}
// Rectangle構造体用のimplブロック
impl Rectangle {
// メソッド (第一引数は self, &self, or &mut self)
fn area(&self) -> u32 { // &self: 不変の借用
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// 関連関数 (第一引数がselfでない関数、コンストラクタのように使われることが多い)
fn square(size: u32) -> Self { // Self は Rectangle のエイリアス
Self { width: size, height: size }
}
}
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
let sq = Rectangle::square(30); // 関連関数は :: で呼び出す
println!("rect1 is {:?}", rect1);
println!(
"The area of the rectangle is {} square pixels.",
rect1.area() // メソッド呼び出し
);
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
println!("Square: {:?}, Area: {}", sq, sq.area());
列挙型 (Enum)
ある値が、可能なヴァリアント(列挙子)のうちの1つであることを表現する型。
// IPアドレスの種類を表す列挙型
enum IpAddrKind {
V4,
V6,
}
// 列挙子のヴァリアントにデータを直接持たせることも可能
enum IpAddr {
V4(u8, u8, u8, u8), // 4つのu8値を持つヴァリアント
V6(String), // Stringを持つヴァリアント
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
// 様々な型のデータを持つことができる例
enum Message {
Quit, // データなし
Move { x: i32, y: i32 }, // 匿名構造体
Write(String), // Stringを一つ含む
ChangeColor(i32, i32, i32), // 3つのi32値を含む
}
// implブロックを使って列挙型にメソッドを定義することも可能
impl Message {
fn call(&self) {
match self {
Message::Quit => println!("Quit message"),
Message::Move{x, y} => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to R:{}, G:{}, B:{}", r, g, b),
}
}
}
let m = Message::Write(String::from("hello"));
m.call(); // Text message: hello
Option<T> 列挙型
標準ライブラリで定義されている非常に一般的な列挙型。値が存在しない可能性があることを表現します。
// 標準ライブラリでの定義 (概念)
// enum Option<T> {
// None, // 値が存在しない
// Some(T), // T型の値が存在する
// }
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None; // 型注釈が必要な場合がある
Option<T>
はmatch
やif let
と組み合わせて使うことが多いです。
コレクション
ヒープにデータを格納するコレクション型。
ベクタ (Vec<T>)
可変長の配列。同じ型の値を連続して格納します。
// 新しい空のベクタを作成
let mut v: Vec<i32> = Vec::new();
// vec! マクロで初期値を持つベクタを作成
let v2 = vec![1, 2, 3];
// 値を追加
v.push(5);
v.push(6);
v.push(7);
v.push(8);
// 要素へのアクセス (インデックス or getメソッド)
let third: &i32 = &v[2]; // インデックス (範囲外だとパニック)
println!("The third element is {}", third);
match v.get(2) { // getメソッド (範囲外だとNoneを返す)
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
// ベクタ内の値を反復処理
for i in &v { // 不変参照で反復
println!("{}", i);
}
for i in &mut v { // 可変参照で反復して値を変更
*i += 50; // * で参照先の値にアクセス
}
println!("Modified vector: {:?}", v); // Modified vector: [55, 56, 57, 58]
// ベクタに異なる型の列挙型ヴァリアントを格納する
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
ベクタはスコープを抜けると破棄され、その要素も全て破棄されます。
文字列 (String)
UTF-8エンコードされた、伸長可能なテキスト。&str
(文字列スライス)とは異なります。
let mut s = String::new(); // 空のString
let data = "initial contents";
let s = data.to_string(); // &strからStringへ変換
// let s = "initial contents".to_string(); // これも同じ
let s = String::from("initial contents"); // これも同じ
// 文字列の更新
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2); // s1に&strを追加 (s2の所有権は移動しない)
println!("s1 is {}, s2 is {}", s1, s2); // s1 is foobar, s2 is bar
let mut s = String::from("lo");
s.push('l'); // 1文字追加
println!("s is {}", s); // s is lol
// +演算子 (format!マクロの方が推奨されることが多い)
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1の所有権がムーブされ、s2の参照(&String)が使われる
// println!("{}", s1); // エラー! s1はムーブされた
// format! マクロ (所有権を奪わない、より効率的)
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s); // tic-tac-toe
println!("s1:{}, s2:{}, s3:{}", s1, s2, s3); // s1,s2,s3はまだ有効
// 文字列の内部表現 (UTF-8)
// Stringは Vec<u8> のラッパー。インデックスアクセスは不可。
let hello = String::from("Здравствуйте"); // キリル文字 (1文字が2バイト)
// let c = hello[0]; // エラー!
// 文字列スライス
let s = String::from("hello world");
let hello_slice = &s[0..5]; // "hello"
let world_slice = &s[6..11]; // "world"
// 文字(char)での反復
for c in "नमस्ते".chars() { // ヒンディー語
println!("{}", c);
}
// バイト(u8)での反復
for b in "नमस्ते".bytes() {
println!("{}", b);
}
ハッシュマップ (HashMap<K, V>)
キー(K)と値(V)のペアを格納するコレクション。キーを使って値にアクセスします。
use std::collections::HashMap; // 明示的にuseする必要がある
// 新しいハッシュマップを作成
let mut scores = HashMap::new();
// 値を挿入
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// チーム名(String)とスコア(i32)のベクタからハッシュマップを作成 (zipとcollect)
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let mut scores: HashMap<_, _> = // 型推論させる
teams.into_iter().zip(initial_scores.into_iter()).collect();
// 値へのアクセス (getメソッド)
let team_name = String::from("Blue");
let score = scores.get(&team_name); // Option<&i32> を返す
match score {
Some(s) => println!("Score for Blue: {}", s),
None => println!("Team Blue not found"),
}
// ハッシュマップ内の値を反復処理 (順序は保証されない)
for (key, value) in &scores {
println!("{}: {}", key, value);
}
// 所有権:
// Copyトレイトを実装する型 (i32など) は値がコピーされる。
// Stringのような所有権を持つ型は、値がハッシュマップにムーブされる。
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name と field_value はここで無効になる (ムーブされたため)
// println!("{}", field_name); // エラー
// 値の上書き
scores.insert(String::from("Blue"), 25); // Blueチームのスコアを25に更新
println!("Updated scores: {:?}", scores);
// キーが存在しない場合のみ値を挿入 (entry API)
scores.entry(String::from("Yellow")).or_insert(60); // Yellowは既に存在(50)するので変更なし
scores.entry(String::from("Red")).or_insert(30); // Redは存在しないので挿入される
println!("After entry: {:?}", scores);
// 古い値に基づいて値を更新
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0); // キーが存在すればその値への可変参照、なければ0を挿入してその参照
*count += 1; // 参照をデリファレンスしてカウントアップ
}
println!("Word counts: {:?}", map);
エラーハンドリング
Rustはエラーを2つのカテゴリに分類します: 回復可能なエラーと回復不可能なエラー。
回復不可能なエラー (panic!)
プログラムの実行を直ちに停止させます。バグなど、続行不可能な状態を示すのに使います。
fn main_panic() {
// panic!("crash and burn"); // プログラムがパニックし、メッセージを表示して終了
let v = vec![1, 2, 3];
// v[99]; // 境界外アクセスでパニックが発生
}
// RUST_BACKTRACE=1 環境変数を設定して実行すると、パニック時のバックトレースが表示される
// $ RUST_BACKTRACE=1 cargo run
回復可能なエラー (Result<T, E>)
エラーが発生する可能性がある操作の結果を表す列挙型。呼び出し元にエラー処理を強制します。
// 標準ライブラリでの定義 (概念)
// enum Result<T, E> {
// Ok(T), // 成功。T型の値を含む
// Err(E), // 失敗。E型のエラー値を含む
// }
use std::fs::File;
use std::io::{self, Read};
fn main_result() {
let f = File::open("hello.txt"); // File::openは Result<std::fs::File, std::io::Error> を返す
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
std::io::ErrorKind::NotFound => match File::create("hello.txt") { // ファイルが存在しない場合、作成を試みる
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error)
}
},
};
// unwrap() と expect() - エラー時にパニックさせるショートカット
// let f = File::open("hello.txt").unwrap(); // Okなら中の値を、Errならpanic!する
// let f = File::open("hello.txt").expect("Failed to open hello.txt"); // unwrapと同じだが、パニックメッセージを指定できる
}
// エラーの伝播 ( ? 演算子)
// Resultを返す関数内で、別のResultを返す関数の結果を簡単に扱う
// Errの場合は、自動的に現在の関数からErrを返す。Okの場合は、中の値を返す。
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("username.txt")?; // エラーならここで io::Error を返す
let mut s = String::new();
f.read_to_string(&mut s)?; // エラーならここで io::Error を返す
Ok(s) // 成功したら String を Ok で包んで返す
// より短く書くことも可能
// let mut s = String::new();
// File::open("username.txt")?.read_to_string(&mut s)?;
// Ok(s)
// さらに短く (std::fs::read_to_string を使う)
// std::fs::read_to_string("username.txt")
}
// 注意: ? 演算子は Result<T, E> を返す関数内でのみ使用可能
// main関数で ? を使うには、mainが Result を返すように変更する必要がある
fn main() -> Result<(), Box<dyn std::error::Error>> { // Box<dyn Error> は様々な種類のエラーを許容するトレイトオブジェクト
let f = File::open("hello.txt")?; // mainからエラーを返せる
Ok(()) // mainが成功したことを示す
}
エラー処理戦略を選択する際のガイドライン:
- プロトタイプコード、テスト、回復不能な状況では
panic!
が適切。 - ライブラリコードや、呼び出し元がエラーに対処すべき場合は
Result
を使用する。 unwrap
やexpect
は、プログラムのロジック上Err
にならないことが確実な場合や、簡単な例では便利だが、堅牢なコードでは避けるべきことが多い。
ジェネリクス、トレイト、ライフタイム(再訪)
ジェネリクス (Generics)
具体的な型を指定せずに、コードの重複を減らし、抽象的な概念を表現する機能。
// ジェネリックな関数 (任意の型 T のスライスから最大の要素を見つける)
// T に PartialOrd(比較可能) と Copy(コピー可能) トレイト境界を設定
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result); // 100
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result); // y
// ジェネリックな構造体
struct Point<T, U> { // 異なる型パラメータ T と U を持つ
x: T,
y: U,
}
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
// ジェネリックなメソッド定義
impl<T, U> Point<T, U> {
fn x(&self) -> &T { // T 型の x フィールドへの参照を返す
&self.x
}
}
// 特定の型の場合のみメソッドを定義することも可能
impl Point<f32, f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
println!("integer_and_float.x = {}", integer_and_float.x()); // 5
// println!("Distance: {}", integer_and_float.distance_from_origin()); // エラー! T,Uがf32でないため
ジェネリクスを使用しても、コンパイル時に具体的な型に置き換えられる(単相化)ため、実行時コストは発生しません。
トレイト (Traits)
特定の型が持つべき共通の振る舞い(メソッドシグネチャの集まり)を定義する仕組み。インターフェースに似ています。
// Summaryトレイトの定義
pub trait Summary {
fn summarize_author(&self) -> String; // 著者情報を返すメソッドシグネチャ
// デフォルト実装を提供することも可能
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
// NewsArticle構造体
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// NewsArticle に Summary トレイトを実装
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
format!("@{}", self.author)
}
// summarizeメソッドはデフォルト実装を使用
}
// Tweet構造体
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
// Tweet に Summary トレイトを実装
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
// summarizeメソッドをオーバーライド
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best hockey team in the NHL."),
};
println!("Tweet summary: {}", tweet.summarize()); // horse_ebooks: of course, as you probably already know, people
println!("Article summary: {}", article.summarize()); // (Read more from @Iceburgh...)
// トレイト境界 (Trait Bounds)
// ジェネリック型パラメータが特定のトレイトを実装していることを要求する
// item パラメータは Summary トレイトを実装する任意の型を受け入れる
pub fn notify(item: &impl Summary) { // `impl Trait` 構文
println!("Breaking news! {}", item.summarize());
}
// 上記は糖衣構文で、実際は以下と同じ (トレイト境界構文)
// pub fn notify<T: Summary>(item: &T) {
// println!("Breaking news! {}", item.summarize());
// }
notify(&article); // Breaking news! (Read more from @Iceburgh...)
notify(&tweet); // Breaking news! horse_ebooks: of course, as you probably already know, people
// 複数のトレイト境界 ( + 構文 )
// pub fn notify<T: Summary + std::fmt::Display>(item: &T) { ... }
// where節によるトレイト境界の指定 (複雑な場合に読みやすい)
// fn some_function<T, U>(t: &T, u: &U) -> i32
// where T: std::fmt::Display + Clone,
// U: Clone + std::fmt::Debug
// { ... }
// トレイトを実装する型を返す関数
// 注意: impl Trait は単一の具体的な型を返す場合にのみ使用可能
fn returns_summarizable() -> impl Summary {
Tweet { // ここでは常に Tweet を返す
username: String::from("stable_rust"),
content: String::from("New Rust version released!"),
reply: false,
retweet: false,
}
// もし条件によって NewsArticle を返したり Tweet を返したりしたい場合は、
// impl Traitではなく Box<dyn Summary> (トレイトオブジェクト) を使う必要がある
}
let summarizable = returns_summarizable();
println!("Returned summary: {}", summarizable.summarize());
// 標準ライブラリの有用なトレイト例:
// - Clone: .clone()でディープコピー可能にする
// - Copy: 型がスタック上でコピー可能であることを示す (Cloneも必要)
// - Debug: {:?} フォーマッタでデバッグ出力可能にする
// - Default: ::default()でデフォルト値を生成可能にする
// - Eq, PartialEq: 等価性比較 (==) を可能にする
// - Ord, PartialOrd: 順序比較 (<, >, etc.) を可能にする
// - Hash: ハッシュ可能にする (HashMapのキーなど)
// - Iterator: forループで反復可能にする
// - Drop: スコープを抜ける際のクリーンアップ処理を定義する
// - AsRef, AsMut: ある型から別の型への参照を安価に取得する
// - Borrow, BorrowMut: 借用操作を一般化する
// - From, Into: 型変換
// - ToString, Display: 文字列化
// - Read, Write: I/O操作
// - Error: エラー型
ライフタイムとトレイト境界の組み合わせ
参照を含むジェネリック型やトレイトを使用する場合、ライフタイムとトレイト境界の両方を指定する必要があることがあります。
use std::fmt::Display;
// 'a ライフタイムを持つ文字列スライス x, y を受け取り、
// Display トレイトを実装する ann (アナウンスメント) も受け取る関数
// 戻り値の参照は、 x と y のうち短い方のライフタイムに束縛される
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display, // ann は Display トレイトを実装している必要がある
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
let announcement = "Comparing two strings";
result = longest_with_an_announcement(string1.as_str(), string2.as_str(), announcement);
// result のライフタイムは string2 のライフタイム ('a) に束縛される
println!("The longest string is {}", result);
}
// ここでは string2 はスコープ外だが、result は string1 の部分を参照しているので有効
// しかし、もし result が string2 を参照していた場合、ここではダングリング参照になるためコンパイルエラー
// println!("Result outside scope: {}", result); // これはコンパイルエラーにならない (resultはstring1を指しているため)
モジュールとクレート
クレート (Crate)
Rustのコンパイル単位。ライブラリクレートまたはバイナリクレートのいずれかです。
- バイナリクレート: 実行可能なプログラムを生成 (
src/main.rs
がクレートルート)。 - ライブラリクレート: 他のプログラムで使用できるコードを提供 (
src/lib.rs
がクレートルート)。 Cargo.toml
でクレートの種類や依存関係を定義します。
パッケージ (Package)
1つ以上のクレートを含み、それらをビルド、テスト、共有するための情報 (Cargo.toml
) を持つ単位。通常、1つのライブラリクレートと複数のバイナリクレートを含むことができます。
モジュール (Module)
クレート内のコードを整理し、名前空間を制御するための仕組み。
// --- src/lib.rs (クレートルート) ---
mod front_of_house { // front_of_house モジュールを定義
pub mod hosting { // hosting サブモジュールを定義 (pubで公開)
pub fn add_to_waitlist() {} // 公開関数
fn seat_at_table() {} // プライベート関数 (モジュール内からのみアクセス可)
}
mod serving { // serving サブモジュール (プライベート)
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
// 絶対パスでアクセス
// crate::front_of_house::hosting::add_to_waitlist();
// useキーワードでパスをスコープに導入
use crate::front_of_house::hosting; // hostingモジュールを導入
// use self::front_of_house::hosting; // self は現在のモジュールを指す (lib.rsではcrateと同じ)
pub fn eat_at_restaurant() {
// use を使った相対パスでのアクセス
hosting::add_to_waitlist();
// hosting::seat_at_table(); // エラー!プライベート関数
// 絶対パスでのアクセスも可能
crate::front_of_house::hosting::add_to_waitlist();
}
// --- モジュールを別ファイルに分割 ---
// src/lib.rs
mod front_of_house; // front_of_house.rs または front_of_house/mod.rs を探す
pub use crate::front_of_house::hosting; // hostingモジュールを再エクスポート
pub fn eat_at_restaurant_v2() {
hosting::add_to_waitlist();
}
// --- src/front_of_house.rs ---
pub mod hosting; // hosting.rs または hosting/mod.rs を探す
// mod serving { ... }
// --- src/front_of_house/hosting.rs ---
pub fn add_to_waitlist() {
println!("Added to waitlist!");
}
// --- pub use による再エクスポート ---
// ライブラリの利用者が hosting::add_to_waitlist() のように直接使えるようにする
// (内部構造 front_of_house を隠蔽できる)
// 上の src/lib.rs 内の `pub use crate::front_of_house::hosting;` がこれに該当
// --- use の慣習 ---
// - 関数: 親モジュールまでを use する (例: `use std::collections::HashMap;` → `HashMap::new()`)
// - 構造体、列挙型、その他: フルパスを use する (例: `use std::io::Result;`)
// - 同じ名前のアイテムを導入する場合: 親モジュールを use するか、`as` キーワードで別名を付ける
// use std::fmt::Result;
// use std::io::Result as IoResult;
// --- glob 演算子 (`*`) ---
// モジュール内の全ての公開アイテムを導入 (非推奨: 何がスコープに入るか不明確になる)
// use std::collections::*;
マクロ
コードを生成するコード(メタプログラミング)の仕組み。
宣言的マクロ (macro_rules!)
パターンマッチングに基づいてコードを生成します。println!
, vec!
などがこれにあたります。
// 簡単なマクロ定義例 (vec!マクロの簡易版)
#[macro_export] // クレート外で使えるようにエクスポート
macro_rules! my_vec {
// マッチングアーム: `( $( $x:expr ),* )`
// $() は繰り返しパターン
// $x はキャプチャ変数名
// :expr は式(expression)であることを示すフラグメント指定子
// , は区切り文字
// * は0回以上の繰り返しを示す
( $( $x:expr ),* ) => { // マッチした場合に展開されるコード
{ // ブロックで囲むのが一般的
let mut temp_vec = Vec::new();
$( // キャプチャした各 $x に対して繰り返す
temp_vec.push($x);
)*
temp_vec // ベクタを返す
}
};
// 別のパターンも定義可能
// ( $elem:expr ; $n:expr ) => { ... }; // my_vec![0; 5] のような形式
}
// マクロの使用 (定義後に呼び出す)
fn use_my_vec() {
let v = my_vec![1, 2, 3]; // 上記のパターンにマッチ
println!("My vec: {:?}", v); // My vec: [1, 2, 3]
let empty_v = my_vec![];
println!("Empty vec: {:?}", empty_v); // Empty vec: []
}
// よく使われるフラグメント指定子:
// item: アイテム (関数、構造体、モジュールなど)
// block: ブロック ({ ... })
// stmt: 文 (statement)
// pat: パターン
// expr: 式 (expression)
// ty: 型
// ident: 識別子 (変数名、関数名など)
// path: パス (foo::bar)
// tt: トークンツリー (柔軟だが複雑)
// literal: リテラル
// meta: メタアイテム (#[derive(...)] の中身など)
// lifetime: ライフタイム ('a)
手続きマクロ (Procedural Macros)
より強力なマクロで、コンパイル時にRustコードを受け取り、操作し、新しいコードを生成します。3種類あります。
- カスタム
derive
マクロ: 構造体や列挙型に#[derive(YourTrait)]
構文でトレイト実装を自動生成。 - 属性様マクロ: 任意のアイテム(関数、構造体など)に付与するカスタム属性。
#[route(GET, "/")]
のような形で使われる。アイテムを変更したり、新しいアイテムを生成したりできる。 - 関数様マクロ: 関数呼び出しのように見えるマクロ。
sql!(SELECT * FROM users)
のような形で使われ、引数のトークンを操作してコードを生成する。
手続きマクロの作成は複雑で、通常は別の専用クレート(proc-macro = true
を Cargo.toml
に設定)として実装し、syn
や quote
といったクレートを利用します。
// カスタムderiveマクロの使用例 (概念)
// use hello_macro::HelloMacro; // 別のクレートで定義されたトレイト
// use hello_macro_derive::HelloMacro; // 別のクレートで定義された手続きマクロ
// #[derive(HelloMacro)] // ここで手続きマクロが呼び出され、HelloMacroトレイトの実装が生成される
// struct Pancakes;
// fn main() {
// Pancakes::hello_macro(); // 生成されたメソッドを呼び出す
// }
// --- hello_macro (トレイト定義クレート) ---
// src/lib.rs
// pub trait HelloMacro {
// fn hello_macro();
// }
// --- hello_macro_derive (手続きマクロ実装クレート) ---
// Cargo.toml
// [lib]
// proc-macro = true
// [dependencies]
// syn = "1.0"
// quote = "1.0"
// src/lib.rs
// extern crate proc_macro;
// use proc_macro::TokenStream;
// use quote::quote;
// use syn;
// #[proc_macro_derive(HelloMacro)]
// pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// // 入力コードを解析可能な構文木に変換
// let ast = syn::parse(input).unwrap();
// // トレイト実装を生成するためのヘルパー関数を呼び出す
// impl_hello_macro(*)
// }
// fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
// let name = &ast.ident // 構造体/列挙型の名前を取得
// let gen = quote! { // quote!マクロでRustコードを生成
// impl HelloMacro for #name {
// fn hello_macro() {
// // stringify!マクロで識別子を文字列リテラルに変換
// println!("Hello, Macro! My name is {}!", stringify!(#name));
// }
// }
// };
// gen.into() // TokenStreamに変換して返す
// }
非同期プログラミング (Async/Await)
I/Oバウンドな操作(ネットワークリクエスト、ファイル読み書きなど)を効率的に扱うための機能。スレッドをブロックせずに待機できます。
基本的な使い方
async
キーワードで非同期関数を定義し、.await
で非同期処理の完了を待ちます。非同期処理を実行するには、非同期ランタイム(例: tokio
, async-std
)が必要です。
// Cargo.toml に依存関係を追加
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// reqwest = "0.11" // 非同期HTTPクライアント (例)
use tokio; // 非同期ランタイム
// 非同期関数 (Future<Output = T> を返す)
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
println!("Fetching {}...", url);
let body = reqwest::get(url)
.await? // HTTP GETリクエストを非同期に実行し、完了を待つ (?でエラー伝播)
.text()
.await?; // レスポンスボディの読み込みを非同期に実行し、完了を待つ
println!("Fetched {}", url);
Ok(body)
}
async fn async_main() {
let url1 = "https://www.rust-lang.org";
let url2 = "https://tokio.rs";
// 逐次実行
// let body1 = fetch_url(url1).await;
// let body2 = fetch_url(url2).await;
// println!("Body 1 length: {:?}", body1.map(|b| b.len()));
// println!("Body 2 length: {:?}", body2.map(|b| b.len()));
// 並行実行 (tokio::join!)
let (result1, result2) = tokio::join!(
fetch_url(url1),
fetch_url(url2)
);
println!("Result 1 length: {:?}", result1.map(|b| b.len()));
println!("Result 2 length: {:?}", result2.map(|b| b.len()));
// tokio::spawn でタスクをバックグラウンド実行
let task_handle = tokio::spawn(async {
fetch_url("https://crates.io").await
});
// 他の処理 ...
match task_handle.await { // spawnされたタスクの結果を待つ
Ok(result) => println!("Crates.io fetch result: {:?}", result.map(|b| b.len())),
Err(e) => println!("Task failed: {}", e),
}
}
// 非同期ランタイム上で async_main を実行
fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async_main());
// または、main関数に #[tokio::main] 属性を付与する (最も簡単)
// #[tokio::main]
// async fn main() {
// async_main().await;
// }
}
Future トレイト
async fn
は、Future
トレイトを実装する型を返します。Future
は、まだ完了していない可能性のある計算を表します。
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// Futureトレイトの定義 (概念)
// pub trait Future {
// type Output; // Futureが完了したときに生成される値の型
// // pollメソッド: Futureの状態を確認し、進行を試みる
// // Pin<&mut Self>: 自己参照構造体を安全に扱うためのラッパー
// // Context<'_>: Futureを駆動する環境(Wakerなど)へのアクセスを提供
// fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
// }
// Poll<T> 列挙型
// enum Poll<T> {
// Ready(T), // Futureが完了し、値Tを生成した
// Pending, // Futureはまだ完了していない (Wakerが登録され、再度pollされるのを待つ)
// }
通常、Future
トレイトを直接実装することは少なく、async/await
構文を使用します。コンパイラがasync fn
から自動的にFuture
を実装するステートマシンを生成します。
非同期ランタイムの役割
- Executor:
Future
を受け取り、poll
メソッドを呼び出して実行を進める。Pending
が返された場合、Waker
を使って将来的に再度poll
できるようにスケジュールする。 - Reactor: OSからのI/Oイベント(ネットワーク、タイマーなど)を監視し、関連するタスクの
Waker
を呼び出してExecutorにタスクの再開を通知する。
tokio
, async-std
などがこれらの機能を提供します。
テスト
Rustには組み込みのテストフレームワークがあります。
ユニットテスト
通常、テスト対象のコードと同じファイル(またはサブモジュール)に記述します。
// src/lib.rs (例)
pub fn add_two(a: i32) -> i32 {
a + 2
}
// テストモジュール (cfg(test) 属性)
#[cfg(test)] // このモジュールは cargo test 実行時のみコンパイルされる
mod tests {
use super::*; // 外側のモジュール(この場合はlib.rs)のアイテムをインポート
#[test] // この関数がテスト関数であることを示す
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4); // 左辺と右辺が等しいことを表明 (等しくないとパニック)
}
#[test]
fn test_add_two() {
assert_eq!(add_two(3), 5);
}
#[test]
#[should_panic] // このテストはパニックすることを期待する
fn test_panic() {
panic!("This test should panic");
}
#[test]
#[should_panic(expected = "less than or equal to 100")] // パニックメッセージの一部を指定
fn test_panic_with_message() {
struct Guess { value: i32 }
impl Guess {
fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be less than or equal to 100, got {}.", value);
}
Guess { value }
}
}
Guess::new(200);
}
#[test]
fn use_result_in_tests() -> Result<(), String> { // Resultを返すテスト関数
if 2 + 2 == 4 {
Ok(()) // 成功
} else {
Err(String::from("two plus two does not equal four")) // 失敗
}
}
// プライベート関数のテスト
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[test]
fn internal() {
assert_eq!(internal_adder(2, 2), 4);
}
}
実行: cargo test
特定のテストを実行: cargo test test_add_two
モジュール内のテストを実行: cargo test tests::
テスト実行中に標準出力を表示: cargo test -- --show-output
デフォルトではテストは並列実行されます。--test-threads=1
で逐次実行。
#[ignore]
属性で特定のテストを無視できます (cargo test -- --ignored
で実行可能)。
統合テスト
ライブラリクレートの公開APIをテストします。tests
ディレクトリを作成し、その中に.rs
ファイルを作成します。
// tests/integration_test.rs
// このファイルは独立したクレートとしてコンパイルされるため、
// テスト対象のライブラリクレートを `use` する必要がある
use my_crate; // ライブラリクレート名を指定 (Cargo.tomlの[package].name)
#[test]
fn it_adds_two_integration() {
assert_eq!(my_crate::add_two(10), 12);
}
// 共通のセットアップコードが必要な場合、 tests/common/mod.rs などを作成し、
// 各テストファイルから use tests::common; のように利用できる
// (commonディレクトリ内のファイルは統合テストとして扱われない)
実行: cargo test
(ユニットテストと一緒に実行されます)
統合テストのみ実行: cargo test --test integration_test
(ファイル名を指定)
バイナリクレートの場合、src/main.rs
にユニットテストは書けますが、公開関数が少ないため統合テストは一般的ではありません。代わりに、ライブラリとしてロジックをsrc/lib.rs
に抽出し、main.rs
からはそれを呼び出すようにして、lib.rs
に対して統合テストを書くのが良い設計です。
その他便利な機能・パターン
型エイリアス (Type Aliases)
既存の型に別名を付けます。コードの可読性向上や、長い型名を短縮するのに役立ちます。
type Kilometers = i32; // i32 に Kilometers という別名を付ける
type Thunk = Box<dyn Fn() + Send + 'static>; // 長い型に別名
type Result<T> = std::result::Result<T, std::io::Error>; // 特定のエラー型を持つResultに別名
fn takes_long_type(f: Thunk) {
// ...
}
fn returns_long_type() -> Thunk {
Box::new(|| println!("hi"))
}
fn process(res: Result<i32>) { // std::result::Result<i32, std::io::Error> と同じ
match res {
Ok(n) => println!("Success: {}", n),
Err(e) => println!("IO Error: {}", e),
}
}
let x: Kilometers = 5;
let y: i32 = 8;
println!("x + y = {}", x + y); // 型エイリアスは元の型と互換性がある
Never 型 (!)
値を持つことがない型。発散する関数(決して戻らない関数、例: panic!
, std::process::exit
, 無限ループ)の戻り値型として使われます。
fn bar() -> ! {
panic!("This function never returns!");
// または loop {} など
}
fn example() {
let guess = "abc";
loop {
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
// continue は値を返さないが、型チェックを通す必要がある
// continue の型は ! であり、任意の型に変換できるため、u32が期待される場所で使える
Err(_) => continue,
};
println!("Parsed guess: {}", guess);
break; // ループを抜ける
}
}
動的サイズ型 (DST) と Sized トレイト
コンパイル時にサイズが不明な型(例: str
, [T]
, トレイトオブジェクト dyn Trait
)。DSTは直接変数に束縛できず、通常は参照(&str
, &[T]
, &dyn Trait
)やポインタ型(Box<str>
, Box<[T]>
, Box<dyn Trait>
)を介して使用します。
Sized
トレイトは、コンパイル時にサイズがわかる型に自動的に実装されます。ジェネリック関数はデフォルトでT: Sized
の境界を持つため、DSTを扱うには?Sized
境界を使用します。
// デフォルトで T は Sized であることが要求される
// fn generic<T>(t: T) { ... }
// 上記は以下と同じ意味
// fn generic<T: Sized>(t: T) { ... }
// DST を許容するには ?Sized を使う
// T は Sized でなくても良い (例: &str や Box<dyn Trait> を受け取れるようにする)
// fn generic_dst<T: ?Sized>(t: &T) { ... }
let s1: &str = "Hello there!"; // str は DST だが、&str は Sized
let s2: Box<str> = "General Kenobi".into(); // Box<str> も Sized
// generic_dst(s1);
// generic_dst(&*s2); // Boxから参照を取り出す
トレイトオブジェクト (dyn Trait)
実行時に異なる型(ただし同じトレイトを実装している)のオブジェクトを扱うための機能。動的ディスパッチを使用します。
trait Draw {
fn draw(&self);
}
struct Button {
width: u32, height: u32, label: String,
}
impl Draw for Button {
fn draw(&self) { println!("Drawing a button: {}", self.label); }
}
struct SelectBox {
width: u32, height: u32, options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) { println!("Drawing a select box with options: {:?}", self.options); }
}
// Drawトレイトを実装する異なる型のオブジェクトを保持できるベクタ
// Box<dyn Draw> はトレイトオブジェクトへのポインタ
let screen_components: Vec<Box<dyn Draw>> = vec![
Box::new(SelectBox {
width: 75, height: 10,
options: vec![String::from("Yes"), String::from("No"), String::from("Maybe")],
}),
Box::new(Button {
width: 50, height: 10, label: String::from("OK"),
}),
];
// 各コンポーネントに対して draw メソッドを呼び出す (動的ディスパッチ)
for component in screen_components.iter() {
component.draw(); // 実行時にどの具体的な draw 実装を呼び出すか決定される
}
// トレイトオブジェクトの制約:
// - オブジェクト安全性: メソッドが Self を返したり、ジェネリック型パラメータを持たないなどの条件を満たす必要がある
コメント