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

最近 Rust にハマりつつあるぷりんです。n 番煎じですが、Rust の文字列操作をまとめてみました。せっかくなので、個人的に一番親しみのある Python の構文と比較しながら見ていきます。

参考にしたサイト

やはりいろんな方が苦労されているようで、いろいろなところで記事があります。特に参考にした頻度の高いものを挙げておきます。

text.baldanders.info
↑個人的に一番まとまっているように感じました。最後の切り出しの苦労はなんか共感してしまいます。

doc.rust-jp.rs
↑公式です。いくつかの書き方は紹介されているものの、網羅的とは言えないような印象です。

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

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

他にもたくさん記事があるので調べてみてください。

文字/文字列の定義

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

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

一方 Rust では文字列を表す String 型, &str 型と文字を表す char 型があります。単純にダブルクォーテーションマークで挟んだものは &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 になるかはよくわかりません。。。
また 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"
}

文字列の結合

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 の所有権は移動せず、この後も参照できます。
他にも一旦 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(); // 左辺で型を書く必要あり   
}

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 では文字列そのものが文字のイテレーターになるので*1、それらをくっつけたイテレーターを作って 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:厳密には for 文などが呼ばれるタイミングなどで some_object.iter() というメソッドが呼ばれてイテレーターが返ってきますが、some_object が str 型の場合は構成要素の文字が順々に出てくるイテレータが返ってきます。