Open10

100 Days of Rust シーズン 2

shinyayshinyay

100 Days of Rust シーズン 1 といいますか、前回 Rust をまとめて学習したときの記録については Zenn に記事としてまとめています。

そして、新たにまた Rust を学習というか復習しようと思い、6 月からまた 100DaysOfCode を再開させていたのでした。
今回は、Zenn に記事としてまとめることはしないで、自分自身の理解の確認のような形で少しずつ ( 1日 30 - 60 分くらい) Rust についていろいろなトピックを確認しています。

何を実施したかは Twitter の方に記載をしていました。

折返しの 50 回も過ぎたので、時々こちらのスクラップも使って学んでいることをまとめてみようかなと思います。

shinyayshinyay

ライフタイムについてあれこれ復習をしました。

'static のライフタイムを考えるときに思い出すこととしては、定数かなと思います。定数は任意のスコープ(グローバルスコープも可能)な宣言です。
その定数の宣言の仕方たに 2 種類あります。

  • const: 不変の値
  • 'static: Static なライフタイムをもつ ミュータブル (mut) な値

https://twitter.com/yanashin18618/status/1685182585750855680

https://twitter.com/yanashin18618/status/1685182908196442112

shinyayshinyay

Day60
https://twitter.com/yanashin18618/status/1685634657520631808

Rust言語におけるCellは、内部可変性を提供するための単純なデータ構造の1つです。Cellは、std::cellモジュールに含まれています。内部可変性を持つデータを、安全に不変な参照の中で変更したい場合に使用します。

Cellは以下のように定義されます:

use std::cell::Cell;

pub struct Cell<T: Copy> {
    // データをカプセル化するためのプライベートフィールド
    value: UnsafeCell<T>,
}
  • Cellはジェネリクスで、Tはコピー可能(Copyトレイトを実装する)な型である必要があります。これは、Cellが値の所有権を持たず、値をコピーして保持するためです。

Cellの主なメソッドは以下の通りです:

  1. Cell::new(value: T) -> Cell<T>: 新しいCellを生成し、初期値valueで初期化します。
  2. get(&self) -> T: Cellに保持されている値を取得します。
  3. set(&self, value: T)replace(&self, value: T) -> T: Cellに新しい値を設定します。setメソッドは単に値を更新し、以前の値を返します。replaceメソッドは以前の値を返すと同時に新しい値を設定します。
  4. into_inner(self) -> T: Cellを解体して、内部に保持されていた値を取得します。

以下は、Cellの使用例です:

use std::cell::Cell;

fn main() {
    let x = Cell::new(42);
    let value = x.get();
    x.set(value + 1);
    println!("x: {}", x.get()); // x: 43

    let old_value = x.replace(100);
    println!("old value: {}", old_value); // old value: 43
    println!("new value: {}", x.get());   // new value: 100
}

注意点:

  • Cellは基本的なデータ型に対してのみ動作するため、複雑なデータ構造や参照を含むデータには適用できません。
  • Cellは、内部のデータがスレッドセーフでないため、マルチスレッド環境での使用には適していません。スレッドセーフな内部可変性が必要な場合は、std::sync::Mutexstd::sync::RwLockを検討してください。

Cellは、不変な参照の中で内部の値を変更する際に有用ですが、その使用は慎重に行う必要があります。一般的には、所有権と借用による安全性を保つ方法を優先して設計することが推奨されます。

shinyayshinyay

Day 61
https://twitter.com/yanashin18618/status/1686204774449127424

Rust言語におけるRefCellは、std::cellモジュールに含まれるデータ構造で、内部可変性を提供します。RefCellは、所有権のルールを実行時にチェックすることで、不変な参照の中でデータを可変に変更することを可能にします。ただし、不変な参照が同時に存在する場合には、RefCellによるチェックによってパニック(panic)が発生する可能性があります。そのため、内部可変性を持つデータに対しては慎重に使用する必要があります。

RefCellは以下のように定義されます:

use std::cell::RefCell;

pub struct RefCell<T: ?Sized> {
    // データをカプセル化するためのプライベートフィールド
    value: UnsafeCell<T>,
}
  • RefCellはジェネリクスで、Tsizedである必要があります。また、Tは不変(immutable)な参照と可変(mutable)な参照を持つことができる型であることを示す?Sizedトレイトバウンドを持っています。これは、RefCellが不変な参照を持つ場合でも、内部のデータを可変に変更できるようにするためです。

RefCellの主なメソッドは以下の通りです:

  1. RefCell::new(value: T) -> RefCell<T>: 新しいRefCellを生成し、初期値valueで初期化します。
  2. borrow(&self) -> Ref<T>: 内部のデータを不変な参照として借用します。既に可変な参照が存在する場合は、ランタイムによるチェックでパニックが発生します。
  3. borrow_mut(&self) -> RefMut<T>: 内部のデータを可変な参照として借用します。不変な参照が既に存在する場合は、ランタイムによるチェックでパニックが発生します。
  4. into_inner(self) -> T: RefCellを解体して、内部に保持されていた値を取得します。

以下は、RefCellの使用例です:

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(42);

    // 不変な参照を取得
    let value = x.borrow();
    // 可変な参照を取得しようとすると、実行時にパニックが発生する
    // let mut mutable_value = x.borrow_mut(); // パニック

    println!("x: {}", *value); // x: 42

    // 内部の値を変更するためには可変な参照を取得する必要がある
    {
        let mut mutable_value = x.borrow_mut();
        *mutable_value += 1;
    }

    // 不変な参照として値を取得できる
    let new_value = x.borrow();
    println!("new x: {}", *new_value); // new x: 43
}

注意点:

  • RefCellは、ランタイムにおける借用規則のチェックを行うため、パフォーマンスが影響を受ける可能性があります。必要な場合のみ使用するようにしてください。
  • RefCellは、スレッド間で共有されるスレッドセーフな内部可変性を提供するものではありません。マルチスレッド環境での使用には注意してください。スレッドセーフな内部可変性が必要な場合は、std::sync::Mutexstd::sync::RwLockを検討してください。

RefCellは、Rustの強力な借用規則を回避して内部のデータを可変に変更するための手段として使用されますが、その使用は慎重に行う必要があります。必要な場面でのみ利用し、データ競合を防止するために注意深く設計することが重要です。

shinyayshinyay

Day 62
https://twitter.com/yanashin18618/status/1686583941095145472

RustのMutexは、マルチスレッドプログラムにおいて共有データのアクセスを安全に管理するための仕組みです。Mutexstd::syncモジュールに含まれていますが、単一スレッド環境でもデータの同期を行いたい場合に使用することができます。

単一スレッド環境でのMutexは、マルチスレッド環境でのような競合状態の問題を解決する必要はありませんが、例えばメインスレッドと別のスレッド間で共有されるデータを同期する必要がある場合に有用です。

以下は、単一スレッド環境でのMutexの使用例です:

use std::sync::Mutex;

fn main() {
    // データを保持するためのMutexを作成
    let counter = Mutex::new(0);

    // Mutexをロックしてデータにアクセス
    let mut num = counter.lock().unwrap();
    *num += 1;
    println!("Value: {}", *num); // Value: 1

    // ロックは自動的に解除されるため、ここで再度ロックしなくてもよい

    // 別のスコープで再度ロックしてデータにアクセス
    {
        let mut num = counter.lock().unwrap();
        *num += 10;
        println!("Value: {}", *num); // Value: 11
    }

    // ここでもロックは自動的に解除される
}

この例では、単一スレッド環境でMutexを使ってデータの同期を行っています。Mutexを使ってデータにアクセスするためには、lockメソッドを呼び出してロックを取得します。MutexGuard型がスコープを抜けるときに自動的にロックが解除されます。

単一スレッド環境では、マルチスレッド環境と同じようなデータ競合の心配はありませんが、特定のリソースへのアクセスを同期化する必要がある場合には、Mutexを使用してデータの同期を行うことができます。

shinyayshinyay

Day 63

https://twitter.com/yanashin18618/status/1687023451561947136

RustのRwLockは、複数のスレッドで共有されるデータの読み取り(読み込み)と書き込み(更新)を安全に管理するための同期プリミティブです。RwLockは、std::syncモジュールに含まれています。

RwLockは、複数の読み取りアクセスは同時に行うことができるが、書き込みアクセスは独占的に行われる特性を持っています。つまり、複数のスレッドが同時に読み取りを行うことは可能ですが、書き込みが行われる間には他のスレッドが読み取りや書き込みを行えないように制御されます。

RwLockの主なメソッドは以下の通りです:

  1. RwLock::new(data: T) -> RwLock<T>: 新しいRwLockを生成し、共有データdataで初期化します。
  2. read(&self) -> RwLockReadGuard<T>: 読み取りロックを取得し、共有データに対して不変な参照を取得します。複数のスレッドで同時に読み取りが行われるため、ロックの取得はブロックされません。
  3. write(&self) -> RwLockWriteGuard<T>: 書き込みロックを取得し、共有データに対して可変な参照を取得します。書き込みロックが取得されている間、他のスレッドからの読み取りや書き込みがブロックされます。

以下は、RwLockの使用例です:

use std::sync::{RwLock, Arc};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(0));
    let mut handles = vec![];

    // 読み取りロックを複数のスレッドで同時に取得
    for _ in 0..5 {
        let data = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let read_guard = data.read().unwrap();
            println!("Read: {}", *read_guard);
        });
        handles.push(handle);
    }

    // 書き込みロックを1つのスレッドが独占的に取得
    let write_guard = data.write().unwrap();
    *write_guard = 42;
    println!("Write: {}", *write_guard);

    for handle in handles {
        handle.join().unwrap();
    }
}

この例では、複数のスレッドが読み取りロックを同時に取得してデータにアクセスし、1つのスレッドが書き込みロックを独占的に取得してデータを更新しています。RwLockにより、複数のスレッドで同時に読み取りを行うことが可能であり、書き込みが行われる間には他のスレッドからの読み取りや書き込みがブロックされることが確認できます。

注意点:

  • RwLockは、読み取りロックを持つスレッドと書き込みロックを持つスレッドの間に適切なバランスを保つことが重要です。書き込みロックが頻繁に取得されると、読み取りの同時実行性が低下する可能性があります。
  • RwLockはマルチスレッド環境で共有データの同期に有用ですが、スレッドセーフな内部可変性が必要な場合にはstd::cell::RefCellを検討する必要があります。
shinyayshinyay

Day 64
https://twitter.com/yanashin18618/status/1687415212570525696

Rustのuseキーワードは、他のモジュール内で定義されたアイテム(関数、構造体、トレイトなど)を自分のスコープに持ち込むために使用されます。また、asキーワードを使用して、インポートしたアイテムに別名をつけることもできます。これは、名前の衝突を回避したり、より簡潔な名前を使用したりする際に便利です。

以下に具体的な使用方法を説明します。

  1. 基本的なインポート:
// モジュール内のアイテムを直接使用する
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("key", "value");
    println!("{:?}", map);
}
  1. 別名をつける(リネーム):
// モジュール内のアイテムに別名をつけて使用する
use std::collections::HashMap as MyHashMap;

fn main() {
    let mut map = MyHashMap::new();
    map.insert("key", "value");
    println!("{:?}", map);
}
  1. 複数のアイテムをインポート:
// 複数のモジュール内のアイテムをインポート
use std::collections::{HashMap, HashSet};

fn main() {
    let mut map = HashMap::new();
    let mut set = HashSet::new();
    map.insert("key", "value");
    set.insert("item");
    println!("{:?}", map);
    println!("{:?}", set);
}
  1. 別名をつけて複数のアイテムをインポート:
// 複数のモジュール内のアイテムに別名をつけてインポート
use std::collections::{HashMap as MyHashMap, HashSet as MyHashSet};

fn main() {
    let mut map = MyHashMap::new();
    let mut set = MyHashSet::new();
    map.insert("key", "value");
    set.insert("item");
    println!("{:?}", map);
    println!("{:?}", set);
}

use文は、コードの可読性や簡潔さを向上させるために活用される一方で、過剰なインポートを行うことでコードが混乱する可能性もあります。適切なモジュールとアイテムの選択、リネームの使用、そして必要なものだけをインポートすることが大切です。

shinyayshinyay

Day 67
https://twitter.com/yanashin18618/status/1688551214181650432

Cow(Clone On Write)を簡単に説明します。

Rustでは、データを変数に入れると、そのデータの「所有権」を持つことになります。データを所有権を持つ変数から別の変数にコピーすると、そのデータのコピーが作成されます。しかし、大きなデータをコピーすることは避けたいですよね。そこで登場するのがCowです。

Cowは、データを「所有」するのではなく、「借用」する方法を提供します。具体的には、元のデータへの参照を持つか、新しいコピーを持つかを柔軟に切り替えられます。

例えば、ある文字列があるとします。この文字列を使って何かをするとき、そのデータを所有権を持つか、または参照を持つかを選べます。データが読み取り専用で変更されることがない場合、参照を使うことでメモリを節約できます。変更が必要な場合は、新しいコピーを作成することができます。

以下は、Cowの使い方の簡単な例です:

use std::borrow::Cow;

fn main() {
    let data1: Cow<str> = "hello".into(); // 文字列の参照を保持
    let data2: Cow<str> = String::from("world").into(); // 新しいコピーを保持

    print_data(data1.clone()); // 参照を使った出力
    print_data(data2.clone()); // コピーを使った出力
}

fn print_data(data: Cow<str>) {
    println!("{}", data);
}

ここでは、data1は元の文字列への参照を、data2は新しいコピーを保持します。print_data関数に渡すことで、どちらの方法を使うかを選べます。これにより、必要なときだけデータをコピーすることができ、メモリの無駄を防げます。

要するに、Cowは「データを所有するか、借用するか」の柔軟な選択を提供してくれるものです。大きなデータを効率的に扱いたい場合や、データのコピーを避けたい場合に役立ちます。

shinyayshinyay

Day 68
https://twitter.com/yanashin18618/status/1688849725473345537

Rc(Reference Counting)は、Rustの標準ライブラリに含まれるスマートポインタの一種です。Rcは「共有所有権」を提供し、複数の所有者が同じデータを共有するために使用されます。単一スレッド環境で使用され、データの共有を容易にするための強力なツールです。

以下に、Rcの主な特徴と使い方を説明します:

  1. 共有所有権: Rcは複数の所有者が同じデータを共有するためのスマートポインタです。データの共有により、メモリ使用量が削減されます。

  2. 不変性: Rcはデータの共有をサポートするため、不変性(immutable)のデータを扱うのに適しています。データが変更されない限り、複数の所有者が同じデータを安全に参照できます。

  3. クローンで所有権を増やす: Rcインスタンスをクローンすることで、新たな所有者を作成します。この際、データ自体はコピーされず、参照カウントが増加します。

  4. 循環参照の注意: Rcは循環参照を回避するためにWeakと組み合わせて使用する必要があります。循環参照が発生すると、メモリリークの原因になる可能性があります。

以下は、Rcの基本的な使い方の例です:

use std::rc::Rc;

fn main() {
    let data = Rc::new(42); // Rcでデータを共有

    let clone1 = Rc::clone(&data); // 共有データへの新たな参照を作成
    let clone2 = Rc::clone(&data); // 同様に共有データへの新たな参照を作成

    println!("Value: {}", data); // 共有データを使用

    // data, clone1, clone2がスコープを抜けると、データも自動的に解放
}
shinyayshinyay

Day 69
https://twitter.com/yanashin18618/status/1689478670799527936

std::thread::spawnは、Rustの標準ライブラリに含まれる関数で、新しいスレッドを生成するために使用されます。この関数を使うと、指定したクロージャ(関数)を新しいスレッド内で実行できます。ここでは、std::thread::spawnの基本的な使い方について説明します。

use std::thread;

fn main() {
    // 新しいスレッドでクロージャを実行
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("Thread: {}", i);
        }
    });

    // メインスレッドの処理
    for i in 1..=3 {
        println!("Main: {}", i);
    }

    // 新しいスレッドの終了を待つ
    handle.join().unwrap();

    println!("All threads are done!");
}

この例では、std::thread::spawnを使用して新しいスレッド内でクロージャを実行しています。新しいスレッドは、メインスレッドと同時に実行され、互いに並行して処理を行います。handle.join()メソッドを呼び出すことで、新しいスレッドの終了を待ちます。

注意点:

  • spawn関数に渡すクロージャは、スレッド内で実行されるため、クロージャ内で使用される変数は移動(move)されます。クロージャ内で外部の変数を使用する場合は、moveキーワードを使用して明示的に変数を移動させる必要があります。
  • スレッドの実行順序は保証されません。メインスレッドと新しいスレッドが交互に実行されるかもしれません。

std::thread::spawnを使用することで、Rustのスレッド生成と並行処理をシンプルに行うことができます。しかし、スレッド間通信や競合状態の回避には注意が必要です。安全で並行性のあるコードを書くためには、同期プリミティブ(Mutexやチャンネルなど)を使用することが重要です。