以下の内容はhttps://smooth-pudding.hatenablog.com/entry/2024/05/11/095721より取得しました。


Rust の文字列操作がややこしいので Python の記法と比べながらまとめてみた Ver II

こんにちは。以前、以下の記事を書きました:
smooth-pudding.hatenablog.com

ハトのボドゲ解析などで Rust の経験値を積んだ今、この記事を読むと、いろいろと気になる点が見つかってしまいました。せっかくなので、現時点の知識で書き直してみたいと思います。前回の記事をベースに、気になるところを加筆修正していくスタイルで行こうと思います。
前回は n 番煎じだったので、今回は (n + n') 番煎じです。

参考にしたサイト

前回「特に参考にした頻度の高いもの」として引用しているサイトをそのまま並べておきます。


text.baldanders.info
↑一番よくまとまっていると感じたそうです。

doc.rust-jp.rs
↑公式です。公式だけど網羅的ではないようです。

note.com
↑ユーザー視点でのつまづきポイントも拾いながら紹介されています。

qiita.com
qiita.com
↑いずれも文字・文字列型の間の変換がまとまっています。

なんで前回、Rust by Example じゃない方の公式の String のドキュメントを引用していないんだろう・・・と思ったので、以下に置いておきます、

doc.rust-lang.org
doc.rust-lang.org

Rust の文字列操作を理解するには、まず String と str (の参照 &str) を理解することがスタートです。

文字/文字列の定義

Python では str 型のみがあり、シングルクォーテーションマークまたはダブルクォーテーションマークで挟んで定義できます。Python に文字型はありません(たぶん)。

# Python
s = "Hello"
t = 'World' # "World" と等価
u = 'c' # "c" と等価

一方 Rust では文字列を表す String 型, &str 型と文字を表す char 型があります*1。単純にダブルクォーテーションマークで挟んだものは &str 型となり、String 型を作るには適切に変換する必要があります。

// Rust
fn main() {
    let s = "Hello"; // &str 型
    let t1 = "World".to_string(); // String 型
    let t2 = String::from("Nice"); // String 型
    let t3 = "Good".to_owned(); // String 型
    let t4: String = "Happy".into(); // String 型
}

イメージとしては &str は長さが決まっていて「固い」文字列、String は長さが可変の「柔らかい」文字列という感じです(もちろん変更を加えるには mutable にする必要はあります)。←言いたいことは分からなくもないですが、若干不安になる表現なので保留で。文字列の編集については後ほど紹介するので、自身の言葉で言語化してみてください。
一方 char 型はシングルクォーテーションマークで挟んで定義します。複数の文字を含めることはできません。

// Rust
fn main() {
    let c = 'c'; // char 型
}

それぞれの型の間の変換

Python はそもそも全部同じ str 型なので当然変換自体がありません。
Rust はいろいろな変換方法があります。冒頭でも紹介した以下のサイトが詳しいです。
qiita.com
qiita.com
上記のサイトを含めて String → &str は & をつければいい、としか紹介されていないところが多いので、他の方法もいくつか紹介しておきます。

// Rust
fn main() {
    let s = "abc".to_string();
    let t1 = &s; // よく紹介されている方法
    let t2 = s.as_str(); // .as_str() で変換
    let t3 = &s[..]; // 全体のスライスの参照
    let t4 = &*s; // deref の ref (まだ理解できません...)
}

&String と &str は別物な気がするのですが、なぜ & を付けるだけで &str になるかはよくわかりません。。。←これは Vec<T> と &[T] の関係と同じです。&[T] は気分的には「各要素は T という型を持っていて長さが決まっていて云々の性質を持つ、コレクションへの不変参照」という意味です。&str も似たような感じで種々の性質を持つものと思えば、「&String は &str の一種」と考えるのがより事実に近そうです。そのため、例えば関数の引数が &str なときに &String を渡すことは可能ですが、ある変数に String の参照 &String を代入しても、それはあくまで &String 型です。
(補足) 上の例について補足しておきます。

  • t1 は &String を定義しています。上述のとおり、&str 型の引数に渡すこともできます。
  • t2 と t3 は同じ内容で、正真正銘の &str を手に入れています。コピーをしているわけではないので、s を mutable にしても、t1, t2 が生きている間は編集できません。
  • t4 は String の参照外し (Deref) で str に変換されたものを参照しています。Deref はまだあんまり理解していません。以下に記述があります。

doc.rust-lang.org


また char 型から &str に変換する方法は一旦 .to_string() で String を経由してから &str にする方法がよく紹介されています。ただ String に変換するときに動的メモリ確保が動いてオーバーヘッドが発生するらしいので、気になる場合は以下の記事のコメント欄にある方法を利用するとよさげです。
qiita.com

// Rust
fn main() {
    let c = 'a'; // char 型
    let mut buffer = [0u8; 4];
    let s: &mut str = c.encode_utf8(&mut buffer); // "a"
}

(補足) 上の記述では「発生するらしい」とどこか自信なさげですが、これは String がヒープ領域のメモリ確保を行うことを指していると思われます。たしかに↑で紹介している方法はヒープメモリの確保が走らないので、めちゃくちゃパフォーマンスを意識しているときには有効だと思います。

文字列の結合

Python では足し算記号のほか、format を使ってつなげることができます。

# Python
s = "abc"
t = "def"
u1 = s + t
u2 = "%s%s" % (s, t)
u3 = "{}{}".format(s, t)
u4 = f"{s}{t}" # 個人的に一番好き

一方 Rust でも足し算記号で結合できますが、左辺は String 右辺は &str である必要があります。結合後は String になります。

// Rust
fn main() {
    let s = "abc".to_string(); // String 型
    let t = "def"; // &str 型
    let u = s + t; // String 型
}

なおこのタイミングで s の値の所有権は u に移るため、このあと s は参照できなくなります。これを避けるためには他の手法を使うか、.clone() を挟めばOKです。

// Rust
fn main() {
    let s = "abc".to_string(); // String 型
    let t = "def"; // &str 型
    let u = s.clone() + t; // String 型
    println!("{}", s); // 参照可能
}

Rust でも format を利用することができ、format! マクロで実現できます。これは String, &str, char のいずれからも実行できます。結合後は String になります。また所有権の移動もありません。

// Rust
fn main() {
    let s = "abc".to_string(); // String 型
    let t = "def"; // &str 型
    let c = 'g'; // char 型
    let u1 = format!("{}{}", s, t); // "abcdef" の String 型
    let u2 = format!("{}{}", s, c); // "abcg" の String 型
    let u3 = format!("{}{}", t, c); // "defg" の String 型
    let u4 = format!("{}{}{}", s, t, c); // "abcdefg" の String 型
}

Rust の String は Vec に似ていて、push (char を繋げる用) や push_str (&str を繋げる用) を使って後ろに延長することが可能です。当然もとの String は mutable である必要があります。

// Rust
fn main() {
    let mut s = "abc".to_string(); // mutable な String 型
    let t = "def"; // &str 型
    let c = 'g'; // char 型
    s.push_str(t); // "abcdef" の String になる
    s.push(c); // "abcdefg" の String になる
}

なお (見た目に反して?) t や c の所有権は移動せず、この後も参照できます。(補足) そもそも &str や char 型は Copy を実装している (所有権を移動せずコピーが走る) ので、所有権云々という話は出てきません。

他にも一旦 char のイテレーターにしてから結合するという技もあるようです。.chars() は String, &str のいずれでも使えて、構成要素となる文字 (char 型) が順番に出てくるイテレーターを生成します。イテレーター同士は .chain() で結合できて、そのあと .collect() で String にまとめています。なお所有権は移動しません(なんで???)

// Rust
fn main() {
    let s = "abc".to_string(); // String 型 (&str 型でもOK)
    let t = "def"; // &str 型 (String 型でもOK)
    let u: String = s.chars().chain(t.chars()).collect(); // 左辺で型を書く必要あり   
}

(補足) 所有権が移動しない理由を説明します。まず s.chars() は s が持っている各文字への参照を順番に出してくるイテレーターを作る命令です。参照を取り出すだけなので、所有権を移動しないように実装されています*2。↑のコード例ではそのあと collect を使って String に詰めています。このタイミングで新しいヒープ領域のメモリが確保されて、s や t が持っている文字への参照を詰め込んだ新しい String が作られます。中身は元の s, t と同じですが (より正確には参照先は同じですが)、String そのものとしては異なるものができているようなイメージでよいと思います。

collect は「左辺で指定した型に応じて値を集めてまとめる」というメソッドなので、型指定は必須です。あるいは collect の型テンプレートを手で指定することも可能です(が、今回は左辺で型指定するほうがスマートでしょう)。

// Rust
fn main() {
    let s = "abc".to_string(); // String 型 (&str 型でもOK)
    let t = "def"; // &str 型 (String 型でもOK)
    let u = s.chars().chain(t.chars()).collect::<String>();
}

ちなみに Python でも itertools.chain を使えば同じやり方が可能です。Python では文字列そのものが文字のイテレーターになるので*3、それらをくっつけたイテレーターを作って join でつなげます。

# Python
from itertools import chain

s = "abc"
t = "def"
u = "".join(chain(s, t))

文字列の繰り返し

Python では掛け算記号で実現できます。

# Python
s = "a" * 5 # "aaaaa"

一方 Rust では掛け算記号は使えませんが、.repeat() というメソッドがあります。繰り返す元は String か &str が使えて、結果は String になります。所有権は移動しません。

// Rust
fn main() {
    let s = "a".repeat(5); // "aaaaa" という String
    let t = "a".to_string(); // String 型
    let u = t.repeat(5); // "aaaaa" という String
}

char を繰り返したい場合は一旦 String または &str に変換してから上記の方法を使うか、何らかの方法で同じ要素を持った配列なり Vec なりを作った後に .iter() でイテレーターにして .collect() するとよいかもしれません。

// Rust
fn main() {
    let c = 'a'; // char 型
    let s: String = [c; 5].iter().collect(); // "aaaaa" という String
}

なお変数の値で繰り返し回数を決めたい場合は上記の配列を使った書き方ではうまくいかないので、諦めてさっさと String なり &str なりに変換してしまうほうが手短だと思います。

文字列の切り出し

追記: 以下の方法は全角文字などが現れる場合はうまくいかないようです。以下の記事を参照してください。
qiita.com

Python では文字の入った配列のごとく扱うことができます。

# Python
s = "abcdefg"
t1 = s[0] # "a"
t2 = s[-2] # "f"
t3 = s[:3] # "abc"
t4 = s[1:] # "bcdefg"
t5 = s[2:-1] # "cdef"

Rust でもほぼ同様の書き方ができますが、切り出した後に & を付ける必要がある点が要注意です。結果は &str 型です。

// Rust
fn main() {
    let s = "abcdefg"; // String 型でもOK
    let t1 = &s[0..=0]; // "a" (Python でいう s[0:1])
    let t2 = &s[s.len()-2..=s.len()-2]; // "f" (Python でいう s[-2:-1])
    let t3 = &s[..3]; // "abc"
    let t4 = &s[1..]; // "bcdefg"
    let t5 = &s[2..s.len()-1]; // "cdef"
}

範囲指定の上限は Python だと外側にあっても OK でしたが、Rust だと (正しく?) panic するみたいなので要注意です。
特定のインデックスの位置にある char を取り出したい場合は .char() で文字のイテレーターを作ったあと .nth() で取り出せばOKです。インデックスの外側を参照しても即座には panic しないように、返ってくる値は Option<char> (つまり Some(文字) か None)になっています。Some の中身を取り出すには .unwrap() を使います。

// Rust
fn main() {
    let s = "abcdefg"; // String 型でもOK
    let t1 = s.chars().nth(0).unwrap(); // 'a'
    let t2 = s.chars().nth(s.len()-2).unwrap(); // 'f'
}

インデックスの外側を参照する恐れのある場所であれば、単に .unwrap() するのではなく .unwrap_or_else() したり、もっと真面目にパターンマッチしたりしてエラーハンドリングするようにしましょう。(Python でいうと try ~ except IndexError で挟むのに対応します)
Python では増大幅を指定して歯抜けに取り出すのも簡単です。特に増大幅を負の数に設定することで逆順も可能です。

# Python
s = "abcdefg"
t1 = s[::2]  # "aceg"
t2 = s[2::3] # "cf"
t3 = s[::-1] # "gfedcba"

Rust では、イテレータの操作を頑張ればなんとか実現できます。

// Rust
fn main() {
    let s = "abcdefg"; // String 型でも可
    // ↓"aceg" という String
    let t1: String = s.chars().step_by(2).collect();
    // ↓"cf" という String
    let t2: String = s[2..].chars().step_by(3).collect();
    // ↓"gfedcba" という String
    let t3: String = s.chars().rev().collect();
}

その他細々とした文字列操作

その他文字列操作でよく使う気がする Python 関数たちを列挙しておきます。

# Python
"aa,b,ccc".split(",") # ["aa", "b", "ccc"]
" abc  ".strip() # "abc"
"abcde".replace("a", "A") # "Abcde"
"abCdE".to_upper() # "ABCDE"
"abCdE".to_lower() # "abcde"
"hello".starts_with("he") # True
"hello".ends_with("llo") # True
"ll" in "hello" # True

Rust だとこんな感じです。

// Rust
fn main() {
    let s = "aa,b,ccc"; // String でも可
    let v: Vec<_> = s.split(",").collect(); // ["aa", "b", "ccc"]: Vec<&str>
    let s = " abc  "; // String でも可
    let t = s.trim(); // "abc": &str 型
    let s = "abcde"; // String でも可
    let t = s.replace("a", "A"); // "Abcde": String 型
    let s = "abCdE"; // String でも可
    let t = s.to_uppercase(); // "ABCDE": String 型
    let t = s.to_lowercase(); // "abcde": String 型
    let s = "hello"; // String でも可
    let b = s.starts_with("he"); // true
    let b = s.ends_with("llo"); // true
    let b = s.contains("ll"); // true
}

最後に

いろいろツッコミを入れてみましたが、将来の自分から見るとまだまだ甘い記述になっているかもしれません。まだまだ勉強が足りず不十分な内容もあると思いますので、ご指摘歓迎です。

*1:もっと言えば OS ごとのシステム文字列を扱う OsString と &OsStr, また path を扱う PathBuf と &Path などがありますが、今回はスコープ外とします。

*2:より細かいことをいえば、String 自体は chars というメソッドは持っておらず、参照外し (Deref) によって String → str の型変換が行われて、&str のメソッド chars が呼ばれていそうです。

*3:厳密には for 文などが呼ばれるタイミングなどで some_object.iter() というメソッドが呼ばれてイテレーターが返ってきますが、some_object が str 型の場合は構成要素の文字が順々に出てくるイテレータが返ってきます。




以上の内容はhttps://smooth-pudding.hatenablog.com/entry/2024/05/11/095721より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14