🎉

RustのString,&strなどの大体の図解と使用例

2024/03/27に公開


備忘を兼ねてある程度理解したつもりのRustのStringと&str(&'static str)の関係図を描いてみました。Stringは構造体であって直接Vecを指しているわけではないはず や、静的領域のデータは本当に配列なのか など完全に正確ではないと思いますが、この程度理解していれば初心者の実用には足る図になっている気がします。蛇足としてよく使いそうな例もメモとして載せています。

大体の図解

relevant of String

対応したコード

fn main() {
    let str1: &str = "a©あ😀";      // #1
    let str2: &str = &str1[3..=5];   // #2
    let mut str3: String = String::with_capacity(12);
    str3.push_str(str1);     // #3
    // let str3 = str1.to_string();         // こう書くことが多い1(普段capacity気にしない時)
    // let str3 = String::from("a©あ😀");  // こう書くことが多い2(普段capacity気にしない時)
    let str4: &str = &str3[..];     // #4
    let str5: &str = &str3[3..=5];  // #5
    let str6: &String = &str3;      // #6

    println!("{}", str1); // a©あ😀
    println!("{}", str2); // あ
    println!("{}", str3); // a©あ😀
    println!("{}", str4); // a©あ😀
    println!("{}", str5); // あ
    println!("{}", str6); // a©あ😀
    assert_eq!(str2, str5);
}

雑な解説

  • #1はいわゆる文字列リテラル
    • 静的領域に確保された文字列"a©あ😀"を指すため厳密に書くと&'static str
    • 静的領域に確保 = 実行中存在保証 = ライフタイムが'static ... だから&'static str
  • #2は文字列リテラルのスライス
    • 静的領域に確保された文字列を指すスライスのため&'static str
  • #3はいわゆる文字列
    • ヒープ領域に確保されるため、文字列リテラルをto_string()すると別途メモリ確保される
    • 実体はVec<u8>
  • #4は文字列のスライス
    • 全体を表現するスライスのためstr3とstr4は実質同じようなもの
  • #5は文字列のスライス
    • ヒープ領域に確保された文字列を指すスライス
  • #6はいわゆる文字列への参照(ポインタ)
    • 参照の参照のようなもの
    • 自動で型強制Derefなどが働き、あまり意識せずStringや全体を表現する&strとして扱えることが多い
  • assert_eq!(str2, str5)なのは中身が同じだから
    • 確保されている領域が違っても内容の[0xE3,0x81,0x82]を比較してくれていると思われる
  • 各アドレスの確認用コード

補足

mut String

対応したコード

fn main() {
    let mut str7: &'static str = "a"; // #7
    str7 = "b"; // #8
    println!("{}", str7); // b
}

雑な解説

  • mutableとして宣言しても静的領域に確保された文字列は書き換わらない
    • 指している先が変化するだけ

よく使いそうな例

fn main() {
    let str1: &str = "a©あ😀";
    let string1: String = String::from("str-ing ");
    let mut string2: String = String::from("1234");
    let vec: Vec<String> = Vec::from(["abc".to_owned(), "789".into(), "@#?".to_string()]);

    let _: String = str1.to_string(); // &strからStringにキャスト
    let _: &str = &string1[..];       // Stringから&strにキャスト &string1は&strか&Stringか型推論が必要
    // -----&strでもStringでも使用可能-----
    if let Ok(int) = string2.parse::<i32>() {        // 文字列から数値キャスト
        string2 = int.to_string();                   // 数値から文字列キャスト
    }
    assert!(String::new().is_empty());               // 空判定
    assert_eq!(string1.chars().count(), 8);          // 文字数カウント .len()はバイト数のため使えない
    assert_eq!(string1.replace("-", ""), "string "); // 置換 (返り値String)
    assert!(string1.starts_with("str"));             // 指定文字列から始まる判定 (ends_withもある)
    assert_eq!(string1.split("-").collect::<Vec<_>>(), ["str","ing "]); // 文字列を分割
    assert_eq!(string1.trim(), "str-ing");           // 前後の空白文字除去 (返り値&str)
    assert!(string1.contains("r-i"));                // 指定文字列を含む判定
    assert!(string1.chars().all(|x| x.is_ascii_alphanumeric())); // [A-Z,a-z,0-9]のみで文字列構成しているか判定
    assert!(!string1.chars().all(|x| x.is_ascii_alphabetic()));  // [A-Z,a-z]のみで文字列構成しているか判定
    assert!(string2.chars().all(|x| x.is_ascii_digit()));        // [0-9]のみで文字列構成しているか判定
    // 数字だけ取得
    assert_eq!("12ab48".chars().filter(|&x| x.is_ascii_digit()).collect::<String>(), "1248");
    // ascii文字だけ取得
    assert_eq!("あ1aい@😀 ".chars().filter(|&x| x.is_ascii()).collect::<String>(), "1a@ ");
    // 部分文字列取得 (返り値String)
    assert_eq!(str1.chars().enumerate().filter(|&(x,_)| x>=1 && x<3).map(|(_,x)| x).collect::<String>(), "©あ");
    // 部分文字列取得 (返り値&str)
    assert_eq!(&str1[str1.char_indices().map(|(x,_)| x).nth(1).unwrap_or(0)..
                    str1.char_indices().map(|(x,_)| x).nth(3).unwrap_or(0)], "©あ");
    // -----------------------------------

    string2.push_str(str1);  // 文字列結合 Stringのみ使用可能
    string2 += &string1[..]; // 文字列結合 内部でpush_strが使用されている
    assert_eq!(string2, "1234a©あ😀str-ing ");
    assert_eq!(vec.concat(), "abc789@#?");    // 文字列要素を結合 (Vec<&str>でも使用可能)
    assert_eq!(vec.join(","), "abc,789,@#?"); // 文字列要素を結合 (Vec<&str>でも使用可能)

    let bar = Foo::new(str1);
    let baz = Foo::new(string1);
    assert_eq!(format!("{}{}",bar.f1,baz.f1), "a©あ😀str-ing "); // String生成
}

struct Foo { f1: String }
impl Foo {
    fn new(str1: impl Into<String>) -> Self {
        Self{ f1: str1.into() } // &str,String両対応可能な定義
    }
}

使えるかもしれない追記

fn main() {
    let foo = "あstrいう";
    let bar = foo.to_string();
    let baz = "";

    assert_eq!(foo.get_slice(0, 0), "あ");
    assert_eq!(foo.get_slice(2, 4), "trい");
    assert_eq!(bar.get_slice(3, 7), "rいう");
    assert_eq!(bar.get_slice(5, 3), "");
    assert_eq!(baz.get_slice(0, 1), "");

    assert_eq!(foo.get_substring(0, 0), "あ");
    assert_eq!(foo.get_substring(2, 4), "trい");
    assert_eq!(bar.get_substring(3, 7), "rいう");
    assert_eq!(bar.get_substring(5, 3), "");
    assert_eq!(baz.get_substring(0, 1), "");
}

trait CustomMethods {
    fn get_slice(&self, start: usize, end: usize) -> &str;
    fn get_substring(&self, start: usize, end: usize) -> String;
}
impl CustomMethods for str {
    fn get_slice(&self, start: usize, end: usize) -> &str {
        if self.is_empty() { return self; }
        let cnt = self.chars().count()-1;
        let min_end = std::cmp::min(cnt, end);
        if start > min_end { return &self[..0]; }
        let (a, b) = self.char_indices()
            .map(|(x,_)| x)
            .enumerate()
            .filter(|&(i, _)| i==start || i==min_end+1)
            .fold((usize::MAX, usize::MAX), |(acc_bs,_acc_be),(_, x)| if acc_bs==usize::MAX {(x,x)} else {(acc_bs, x)});
        if end >= cnt { &self[a..] } else { &self[a..b] }
    }
    fn get_substring(&self, start: usize, end: usize) -> String {
        self.chars().enumerate().filter(|&(x,_)| x>=start && x<=end).map(|(_,x)| x).collect::<String>()
    }
}

参考文献

Discussion