Skip to content

Instantly share code, notes, and snippets.

@kotobukid
Created December 20, 2024 07:47
Show Gist options
  • Save kotobukid/25db7feead2cf586db2827cb6951c9ff to your computer and use it in GitHub Desktop.
Save kotobukid/25db7feead2cf586db2827cb6951c9ff to your computer and use it in GitHub Desktop.
StringはSend+Syncである...から中身が気になってくる

Chat 'ChatTitle(text=String型の内部実装と挙動, isCustom=true)' (8a4a9318-8258-4dbe-844e-390df6678c66)

User: 添付コードのwrite!の第1引数はmut Stringだと思いますが、Stringが内部的にはVecであることから書き込みが成立する、と考えて良いケースですか? String(実態はバイト列)を便利に使えるのは type String = Vec; impl String { fn as_ptr(&self) ... } こんな感じの実装になってるんでしょうか

Attachments: Attachment Name: CustomSystemPromptAttachment Attachments Kind: Visible Attachments Text:

Attachment Name: Information about open file in the editor Attachments Kind: Visible Attachments Text:

use std::sync::Arc; // Rcの代わりにArcを使用
use std::thread::spawn;
use std::fmt::Write;

fn main() {
    let a: String = "Hello, world!".to_string();
    dbg!(&a);
    println!("{:?}", &a.len());
    println!("{:?}", &a.as_ptr());
    println!("{:?}", a.capacity());
    println!();

    let mut b = String::with_capacity(64);
    write!(b, "Hello, world!").unwrap();
    dbg!(&b);
    println!("{:?}", &b.len());
    println!("{:?}", &b.as_ptr());
    println!("{:?}", b.capacity());

    // spawn({
    //     let a = Arc::clone(&a); // Arcの参照を安全にクローニング
    //     move || {
    //         dbg!(a);
    //     }
    // }).join().unwrap();  // スレッドを待機(エラー回避のため追加)
}

Attachment Name: Selection Attachments Kind: Visible Attachments Text:

    let mut b = String::with_capacity(64);
    write!(b, "Hello, world!").unwrap();
    dbg!(&b);
    println!("{:?}", &b.len());
    println!("{:?}", &b.as_ptr());
    println!("{:?}", b.capacity());

Assistant: String型について、その内部実装やwrite!マクロの動作の理解に役立つ情報を整理してお答えします。

String型の内部実装

String型の内部は実際にはVec<u8>に基づいています。このため、Stringは可変で容量を拡張可能なバイト列を操作しますが、UTF-8エンコードされた文字列のみを保持します。Rustの標準ライブラリでは、概ね以下のような関連があります:

pub struct String {
    vec: Vec<u8>,
}

ですので、Stringの操作(例: capacity, as_ptr, lenなど)はVec<u8>が持つ機能をラップして提供しています。

write!mut String

write!マクロはstd::fmt::Writeトレイトを利用して動作します。「write!の第一引数が&mut String(またはmut String)の場合に書き込みが成立するか」という質問についてですが、これが可能である理由は、StringWriteトレイトを実装しているためです。

以下に簡単なWriteトレイトの実装例を示します(Rust標準ライブラリの実際の方法ではありませんが、概念的にはこうなります):

impl Write for String {
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
        self.push_str(s);
        Ok(())
    }
}

基本的には、StringWriteトレイトを実装しており、そのwrite_strメソッドを通じて文字列を追加しているということです。write!(...)はこの機能を使って文字列を動的に構築しています。

write!(b, "Hello, world!")のコードでは、以下が起きています:

  1. bは容量を64に設定した空のStringです(String::with_capacity(64)を使用)。
  2. write!マクロを呼び出すことで、"Hello, world!"bに書き込みます。
  3. Writeトレイトのwrite_strメソッドが呼ばれ、内部的にpush_str関数を利用して文字列がbに追加されます。

そのため、書き込みが成立するのは、StringWriteトレイトを適切に実装しているからだといえます。

Stringが内部でVec<u8>を利用する仕組み

String型がVec<u8>に基づいているのはStringのメソッドや標準ライブラリの挙動とそのソースコードから確認できます。以下のような形式でメソッドが使われます:

  • as_ptr: 内部データの生ポインタを取得(Vec<u8>のもつ機能に委譲)。
  • capacity: メモリに割り当てられた現在の容量を返す(Vec<u8>capacityに委譲)。
  • len: 有効な文字列のバイト長を返す(Vec<u8>lenに委譲)。

具体的な実装イメージ:

impl String {
    pub fn as_ptr(&self) -> *const u8 {
        self.vec.as_ptr()
    }

    pub fn len(&self) -> usize {
        self.vec.len()
    }

    pub fn capacity(&self) -> usize {
        self.vec.capacity()
    }
}

型エイリアスについて

一見するとtype String = Vec<u8>のように考えるかもしれませんが、実際にはStringはこれとは異なり、新しい型です(型エイリアスではありません)。そのため、以下のような定義は正しくありません:

type String = Vec<u8>;

実際のString型は新しい構造体型として定義されています。その上で、Vec<u8>の操作を借りて方便的に動作を提供しています。

上記のように、「Stringが便利なのはVec<u8>に基づいているから」と考えていただいて問題ありません。


まとめ

  1. String型は内部的にVec<u8>をラップしている
  2. write!Stringに対して動作するのは、StringWriteトレイトを実装しているため
  3. RustのStringVec<u8>を借用しているものではなく、新しい型として実装されています(型エイリアスではありません)。
  4. String型が提供するメソッド(例:as_ptr, len, capacityなど)はVec<u8>の類似メソッドに大部分を委譲しています。

User: ありがとうございます。色々整理できそうです。ところで最後の項目、StringはVecの型エイリアスではない という話なのですが、私がこれまで読んできた書籍等のコードではこういった場合、 struct Hoge { items: Vec } みたいなところから始まる物が多かった気がするんですよね(初学者向け故かもしれません) このケースでitemsという名前のフィールドで一段階深く辿らなければデータそのものにアクセスできない、これを回避することが「Stringは型エイリアスである」と想像し質問するに至った理由なんですが、標準Stringの実装のように自作の構造体を定義する方法はありますか?

Assistant: ご質問ありがとうございます! Stringのように「Vec<u8>を内部に持った構造体で、そのまま便利にアクセスできるような方法を自作で実装するには?」という疑問ですね。以下に具体的な説明とサンプルコードを提供します!


Stringにおけるヒント

Rust標準ライブラリのStringが持つ便利な特性:

  • 内部的にはVec<u8>をラップしているが、直接データへアクセスできる(as_ptr, len, capacityなど)。
  • フィールド名を意識せずに、&String&mut Stringとして利用可能。
  • 必要なときには低レベルの操作を行うことができる。

これを実現するには、標準ライブラリが行うように独自の構造体を作成し、必要なトレイトやメソッドを実装すれば可能です。


自作構造体での方法

まず、自作の構造体をVec<u8>に基づいて設計し、DerefDerefMutのトレイトを利用することでフィールドを意識せずに操作可能な構造体を作る方法を紹介します。

サンプルコード: Vecを内部に持つ構造体

use std::ops::{Deref, DerefMut};

struct MyString {
    data: Vec<u8>, // Vec<u8>を内部フィールドとして保持
}

impl MyString {
    // コンストラクタ(初期化用)
    pub fn new() -> Self {
        MyString {
            data: Vec::new(),
        }
    }

    pub fn with_capacity(capacity: usize) -> Self {
        MyString {
            data: Vec::with_capacity(capacity),
        }
    }

    // 要素を追加する(UTF-8の文字列を追加する場合)
    pub fn push_str(&mut self, s: &str) {
        self.data.extend_from_slice(s.as_bytes());
    }

    // 長さを取得する
    pub fn len(&self) -> usize {
        self.data.len()
    }

    // キャパシティを取得する
    pub fn capacity(&self) -> usize {
        self.data.capacity()
    }

    // 生ポインタを取得する
    pub fn as_ptr(&self) -> *const u8 {
        self.data.as_ptr()
    }
}

// Derefトレイトを実装して、Vec<u8>として振る舞えるようにする
impl Deref for MyString {
    type Target = Vec<u8>;
    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

// DerefMutトレイトを実装して、&mut Vec<u8>に変換可能にする
impl DerefMut for MyString {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.data
    }
}

impl std::fmt::Write for MyString {
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
        self.push_str(s);
        Ok(())
    }
}

fn main() {
    let mut my_str = MyString::with_capacity(64); // 容量64で初期化
    my_str.push_str("Hello");
    my_str.push_str(", world!");

    println!("String: {:?}", String::from_utf8(my_str.data.clone()).unwrap());
    println!("Length: {}", my_str.len());
    println!("Capacity: {}", my_str.capacity());
    println!("Pointer: {:?}", my_str.as_ptr());

    // write!マクロも動作
    write!(my_str, " How are you?").unwrap();
    println!(
        "After write!: {:?}",
        String::from_utf8(my_str.data.clone()).unwrap()
    );
}

コード解説

  1. 構造体の作成

    • MyString構造体を作り、内部にVec<u8>を保持。
    • 初期化方法やストレージ操作が行いやすいようにメソッドを作成(newwith_capacityなど)。
  2. 標準ライブラリのような便利なメソッドを再現

    • push_str: UTF-8エンコードの文字列を追加。
    • len, capacity, as_ptrなど、Vec<u8>の持つ機能をラップ。
  3. Derefトレイトを実装

    • DerefDerefMutトレイトを実装することで、MyStringが暗黙のうちにVec<u8>のように使える。
    • これにより、MyStringのインスタンスを直接Vec<u8>として操作可能。
  4. トレイトの実装でさらに柔軟に

    • std::fmt::Writeトレイトを実装することで、write!マクロも利用可能に。

結果として得られるメリット

  • フィールドを意識せずに使える: Derefトレイトのおかげで、MyStringをまるでVec<u8>そのもののように操作可能です。例えば、len()capacity()をフィールド名を介さずに呼び出せます。

  • 標準Stringに似た操作感:

    • write!(my_str, ...)のように、標準Stringで使えるメソッドを再現できます。
    • 生ポインタや文字列長などを提供するインターフェースは、完全に標準Stringと一致させることも可能です。

重要な注意点

自作型の場合、UTF-8エンコードを保証するのは開発者の責任です。Vec<u8>を直接操作した結果、UTF-8でないデータが含まれる可能性があるため、標準のStringのような安全性を完全に達成したい場合は、さらなるチェックが必要です。たとえば、文字列を操作するメソッド内で常にUTF-8検証を行う必要があります。


まとめ

標準Stringの実装に似た仕組みを自作の構造体で実現することは可能です。この回答で示した方法ではMyStringVec<u8>の便利さを活かしつつ、フィールド名を意識せずに操作できる構造を作る方法を説明しています。また、write!などの既存のマクロも活用できる設計になっています。ぜひ試してみてください!

User: Deref, DerefMutを実装することで「自動参照外しのされ方」を定義可能になり、それにより今回はmy_string.data.len()のように本来書かねばならないところをmy_string.len()のように記述できるようにした って感じなんですかね? これによって、MyStringに実装した関連関数と、Vecがもともと持っているメソッド群を、インスタンスから直接生えているかのように呼び出し可能になる(Vecのものと重複する名前の関連関数をMyStringに実装することはできない?)?

Assistant: はい、その認識は非常に的確です!DerefDerefMutを実装することで、Rustの「自動参照外し」が形づくられ、結果として以下のような振る舞いを可能にしています:


1. DerefとDerefMutの効果

Derefトレイトは、構造体のインスタンスが特定の型として「振る舞う」ように見せる仕組みを提供します。今回の場合、MyStringがあたかもVec<u8>であるかのように動作できます。

  • 具体例: my_string.len()を呼び出すと、本来はMyString型のメソッドlenが呼ばれるべきですが、DerefによってMyStringVec<u8>として解釈されるため、Vec<u8>len()が呼び出されます。このプロセスを「自動参照外し」と呼びます。

DerefMutの場合、これを&mut参照に対しても同様に適用できます。


2. MyStringとVecを一体化させる方法

Derefを使うことで、以下のように両方の世界を活用可能になります:

  1. MyString独自のメソッドを直接呼び出し可能:

    • 例: my_string.push_str("hello!"); は、MyStringの独自メソッドを直接利用しています。
  2. Vec<u8>のメソッドを呼び出し可能:

    • 例: my_string.len() は、Vec<u8>len()が呼ばれます。

3. Vecと同名のメソッドを持つ場合

Rustでは、構造体(または型)固有のメソッドが、Derefで参照外しされた型のメソッドよりも優先されます。

たとえば、Vec<u8>が元々持つlenと同名のメソッドをMyString側に実装した場合、my_string.len()MyString側のlenが呼び出され、Vec<u8>lenは呼び出されません。

具体例: 同名のメソッドを実装する

struct MyString {
    data: Vec<u8>,
}

impl MyString {
    pub fn len(&self) -> usize {
        println!("Calling MyString's len()");
        self.data.len() // 内部のVec<u8>のlenを利用
    }
}

impl std::ops::Deref for MyString {
    type Target = Vec<u8>;
    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

fn main() {
    let my_string = MyString {
        data: vec![1, 2, 3],
    };

    // MyStringのlenが呼ばれる
    println!("Length: {}", my_string.len()); // "Calling MyString's len()"
}

4. Vecのメソッドを呼びたい場合

もしMyStringに同名のメソッドがある場合で、それを避けて「元のVec<u8>のメソッドを使いたい」ときには、以下のように参照を明示的に使うことができます:

println!("Vec<u8> length: {}", MyString::deref(&my_string).len());

このように、deref()メソッドを用いれば元の型(Vec<u8>)にアクセスできます。


5. 自動参照外しの要点まとめ

自動参照外しメカニズムにより:

  • MyStringに実装されたメソッドと、
  • Vec<u8>に由来するメソッドを、 インスタンスに対して直接呼び出せるようになります。

ただし、以下の点に注意してください:

  1. MyString固有のメソッドが優先される
  2. 両方のメソッド名が重複した場合、MyString側が勝つ
  3. もしVec側のメソッドを使いたければ、deref()を明示的に用いる

全体のまとめ

  • DerefDerefMutトレイトを実装することで、自作の構造体があたかも内部の型そのもののように振る舞える。
  • 自動参照外しを利用すると、内部型のフィールド名を記述せずに、関連メソッド(Vec<u8>のもの)を呼び出せる。
  • メソッドの重複が起きる場合、Rustでは自作型(今回で言えばMyString)のメソッドが優先されるが、内部型のメソッドも明示的な参照で使うことができる。

あなたのコード設計において、Vec<u8>の機能を活かしつつ追加機能を提供したい場合、このように使い分けができますので、ぜひ活用してみてください!

use std::fmt::Write;
use std::ops::{Deref, DerefMut};
struct MyString {
data: Vec<u8>, // Vec<u8>を内部フィールドとして保持
}
#[allow(dead_code)]
impl MyString {
// コンストラクタ(初期化用)
pub fn new() -> Self {
MyString { data: Vec::new() }
}
pub fn with_capacity(capacity: usize) -> Self {
MyString {
data: Vec::with_capacity(capacity),
}
}
// 要素を追加する(UTF-8の文字列を追加する場合)
pub fn push_str(&mut self, s: &str) {
self.data.extend_from_slice(s.as_bytes());
}
// 長さを取得する
pub fn len(&self) -> usize {
self.data.len()
}
// キャパシティを取得する
pub fn capacity(&self) -> usize {
self.data.capacity()
}
// 生ポインタを取得する
pub fn as_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
}
// Derefトレイトを実装して、Vec<u8>として振る舞えるようにする
impl Deref for MyString {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.data
}
}
// DerefMutトレイトを実装して、&mut Vec<u8>に変換可能にする
impl DerefMut for MyString {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
impl std::fmt::Write for MyString {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.push_str(s);
Ok(())
}
}
fn main() {
let mut my_str = MyString::with_capacity(64); // 容量64で初期化
my_str.push_str("Hello");
my_str.push_str(", world!");
println!(
"String: {:?}",
String::from_utf8(my_str.data.clone()).unwrap()
);
println!("Length: {}", my_str.len());
println!("Capacity: {}", my_str.capacity());
println!("Pointer: {:?}", my_str.as_ptr());
// write!マクロも動作
write!(my_str, " How are you?").unwrap();
println!(
"After write!: {:?}",
String::from_utf8(my_str.data.clone()).unwrap()
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment