Open2

Rust学習

RyoRyo

Rust 教材

The Rust Programming Language

  • 03. 一般的なプログラミングの概念
    • 3.1. 変数と可変性
    • 3.2. データ型
    • 3.3. 関数
    • 3.4. コメント
    • 3.5. 制御フロー
  • 04. 所有権を理解する
    • 4.1. 所有権とは?
    • 4.2. 参照と借用
    • 4.3. スライス型
  • 05. 構造体を使用して関係のあるデータを構造化する
    • 5.1. 構造体を定義し、インスタンス化する
    • 5.2. 構造体を使ったプログラム例
    • 5.3. メソッド記法
  • 06. Enumとパターンマッチング
    • 6.1. Enumを定義する
    • 6.2. match制御フロー演算子
    • 6.3. if letで簡潔な制御フロー
  • 07. 肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
    • 7.1. パッケージとクレート
    • 7.2. モジュールを定義して、スコープとプライバシーを制御する
    • 7.3. モジュールツリーの要素を示すためのパス
    • 7.4. useキーワードでパスをスコープに持ち込む
    • 7.5. モジュールを複数のファイルに分割する
  • 08. 一般的なコレクション
    • 8.1. ベクタで値のリストを保持する
    • 8.2. 文字列でUTF-8でエンコードされたテキストを保持する
    • 8.3. キーとそれに紐づいた値をハッシュマップに格納する
  • 09. エラー処理
    • 9.1. panic!で回復不能なエラー
    • 9.2. Resultで回復可能なエラー
    • 9.3. panic!すべきかするまいか
  • 10. ジェネリック型、トレイト、ライフタイム
    • 10.1. ジェネリックなデータ型
    • 10.2. トレイト: 共通の振る舞いを定義する
    • 10.3. ライフタイムで参照を検証する
  • 11. 自動テストを書く
  • 12. 入出力プロジェクト: コマンドラインプログラムを構築する
  • 13. 関数型言語の機能: イテレータとクロージャ
  • 14. CargoとCrates.ioについてより詳しく
  • 15. スマートポインタ
  • 16. 恐れるな!並行性
  • 17. Rustのオブジェクト指向プログラミング機能
  • 18. パターンとマッチング
  • 19. 高度な機能

その他

Rust by Example

Rust APIガイドライン (非公式日本語訳)

Rust裏本

RyoRyo

メモ

基本データ型

  • 符号付き整数: i8, i16, i32, i64, i128, isize
  • 符号無し整数: u8, u16, u32, u64, u128, usize
  • 浮動小数点数: f32, f64
  • 論理値型: bool
  • char: 'a', 'あ', '∞', '🐾' シングルクォート指定
  • タプル型: (i32, f64, i32) = (10, 2.5, 20)
  • 配列型: [1, 2, 3] 配列の全要素は同じ型, サイズ固定長

整数型の基準はi32型
浮動小数点数の基準はf64型

文字列

Rustは文字をUnicode/UTF-8として扱います。つまり、日本語や絵文字などの文字も標準で扱うことが可能です。

文字列リテラル

プログラム上(ソースコード上)に直接的に"Hello"などと文字や数字を記述するもの。
文字列リテラルの型は&'static str
'staticはその変数のライフタイム(生存期間)プログラムの終了まで有効であるということを意味しています。

char(文字型)

単一の文字を表す。
シングルクォートで指定する。
ダブルクォーテーションで囲んで定義したものは&str型に推論される。
Unicodeコードポイントを保持する。

str(文字列スライス)

文字列を扱う。
文字列スライス(&[u8]型のスライス)と呼ばれ、String型の一部への参照。

String(Collection型)

標準ライブラリで提供される型。ベクタ(Vec<u8>)として保持される。
文字列を追加したり変更したりできる、可変(mutable)のサイズである。
(ヒープ上にデータを確保し、所有権を持つ)

文字列のn文字目を取得する場合、for文で走査することになる。

// chars 4バイト
for c in "hello".chars() {
    println!("{}", c);
}

// bytes 各バイト(`u8`型) 
for b in "नमस्ते".bytes() {
    println!("{}", b);
}

式と文

  • 式(Expression): 結果値に評価される
  • 文(Statement): なんらかの動作をして値を返さない命令

セミコロン

文末の式にセミコロンをつけないことで値を返すことができる(returnを省略できる)
この場合ブロック{ ... }も式である

{
    let x = 3;
    x + 1
}

ブレース閉じ}の後のセミコロンについては基本的には不要
変数に何かを代入した後、何かを処理する時などには必要となる

所有権について

所有権規則

  • Rustの各値は、所有者と呼ばれる変数と対応している。
  • いかなる時も所有者は一つである。
  • 所有者がスコープから外れたら、値は破棄される。

ムーブ(所有権の移動)

所有権が移動することをムーブと呼ぶ。所有権が移動すると、移動元の変数は無効化される。

let s1 = String::from("hello");
let s2 = s1; // s1からs2に所有権が移動

// s1は無効化されているため、以降s1を利用しようとするとコンパイルエラー
println!("{}, world!", s1);

所有権の複製(Copy Trait)

スカラー値における所有権は移動せず、値がコピーされる。

  • あらゆる整数型。u32など。
  • 論理値型であるbool
  • あらゆる浮動小数点型、f64など。
  • 文字型であるchar。
  • タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。

参照と借用

借用

アンド記号&を使用することで参照を取ることができる。
他の言語同様に他のデータへのアクセスを提供する。
所有権は移動せず、値にアクセスすることができる。
この参照を取ることを借用と呼ぶ。

参照の規則

  • 任意のタイミングで、一つの可変参照 or 不変な参照いくつでものどちらかを行える。
  • 参照は常に有効でなければならない。

制約

  • 2つ以上のポインタが同じデータに同時にアクセスする。
  • 少なくとも一つのポインタがデータに書き込みを行っている。
  • データへのアクセスを同期する機構が使用されていない。

構造体

  • 構造体
  • タプル構造体
    名前フィールドなし、タプルと同様に扱う(タプルに名前がついたようなもの)
    インデックスで参照する or 各フィールドの値を別々の変数に展開する
  • ユニット様構造体
    フィールドを何も持たない構造体
    トレイトだけ実装するような時

制御フロー

パターンマッチ

matchではパターンに値が一致した際に、紐付けられた処理コードが実行される。
matchパターン => 式をアームと呼ぶ。
各アームに紐付けられるコードは式であり、マッチしたアームの式の結果がmatch式全体の戻り値になる。

matchは包括的で全てのあらゆる可能性を網羅する必要がある。
記述したアーム以外に一致する_(ワイルドカード)プレイスホルダーを使うことができる。

if let記法

if letという記法は等号記号で区切られたパターンと式を取り、式がmatchに与えられ、
パターンが最初のアームになったmatchと、 同じ動作をします。

言い換えると、if letは値が一つのパターンにマッチした時にコードを走らせ、 他は無視するmatchへの糖衣構文と考えることができます。

let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
    println!("three");
}

// 下記と同義
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

ジェネリクス

構造体のメソッドにはimplの直後にTを宣言しなければならない。

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

トレイト

trait宣言を用意て定義する。
impl トレイト名 for 実装対象インスタンス型という文法で実装する。

trait Summary {
    fn summarize_author(&self) -> String;

    // デフォルト実装
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

引数としてのトレイト

引数でトレイトを指定する場合は、impl Trait構文を使う。

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// トレイト境界 (trait bound)
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}
// トレイト境界構文の方が便利な場合がある
pub fn notify<T: Summary>(item1: &T, item2: &T) {}

// 複数トレイトの指定 itemは両方実装していなくてはいけない
pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}

// `where`句による指定も可能
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{
    ...
}

ライフタイム注釈

アポストロフィー(')で'aといった記述をする。
基本的に借用チェッカーがライフタイムを推論を行うので、コンパイラが解決できない場合に指定が必要。
不用意な指定はしない方が良さそう。

パッケージ、クレート、モジュール

パッケージ: Cargoで管理する1つ以上のクレートを含んだもの
クレート: パッケージに含まれれる実行バイナリおよびライブラリ
モジュール: 関数などの要素の論理的な階層

パッケージ

Cargoパッケージマネージャによって作成され、アプリケーションを構成する最上位階層。

クレート

ソースコード、ソースファイルのこと。
次の2種類が存在する。

  • バイナリクレート
    エントリポイント (main 関数) を持つクレート
    src/main.rsがクレートルートとなり、モジュール構造の起点となる
     コマンド:cargo new (cargo new --bin)
  • ライブラリクレート
    エントリポイントを持たないクレート
    src/lib.rsがクレートルート
    コマンド:cargo new --lib

モジュール

クレート内の関連する機能やデータ構造のをまとめて管理し、名前空間で構造する。
実際にはクレート(ファイル)にモジュールを切り離すケースが多い。(ファイル名=モジュール名)

標準ライブラリ

https://doc.rust-lang.org/std/index.html

Slice型

配列、ベクタ、文字列などの一部分を参照する。
スライスで指定するのはバイトの位置なので、
マルチバイト文字を含む文字列では文字の位置と異なる。
文字の境目ではないバイト位置を指定すると実行時にパニックが発生する。

Range型

start..endの形式で範囲を指定する。
先頭と末尾を省略できる。

// for文で利用
for i in 1..4 {
    println!("{i}");
}

Option<T>型

値が存在しない可能性がある場合に利用する。

enum Option<T> {
    Some(T),
    None,
}

Result型

処理が失敗する可能性がある場合に利用する、エラーハンドリングの型

enum Result<T, E> {
    Ok(T),
    Err(E),
}

?演算子を利用することでResultの値がErrの場合、早期returnを行うことができる。

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

Vector型(Collection型)

可変長な配列を扱う。
基本的に同じ型しか利用できない。(Enum型を利用することで異なる型を保持可能)

// 空のベクタ生成
let v: Vec<i32> = Vec::new();
// 初期値を持つベクタ生成
let v = vec![1, 2, 3];

// 操作
let mut v = Vec::new();
v.push(5); // 追加
vector.pop(); // 末尾削除

// 値へのアクセス
let v = vec![1, 2, 3, 4, 5];
let third = &v[2];
let third = v.get(2); // Option<&T>

HashMap型(Collection型)

キー&バリューのデータ構造。
ハッシュマップの仕様にはuse文による宣言が必要。(使用頻度が低いとの判断の模様)
キーは全て同じ型でなければならず、 値も全て同じ型でなければならない。
安全性に重きを置いているため
HashDos(ハッシュテーブル衝突攻撃)に対して安全性を得るために、安全なハッシュ関数を利用している。
これは最速なアルゴリズムではなく、パフォーマンスを優先する場合異なるhasherを利用することになる。