21

この記事は最終更新日から3年以上が経過しています。

投稿日

更新日

Rustで文字列の先頭文字や部分文字列を取得する

Rustで文字列の先頭文字を取得する

Rustの文字列には様々な型があるので初心者には操作がなかなか難しいです。
「文字列の先頭文字を取得する」という操作も、大きく分けて2通りあることに気づきました。

let text = "Hello World!";

このtextという文字列の先頭文字Hを取得することを考えましょう。
Rustでは文字列リテラルを変数に代入した時、&'static str型という&str型の一種を持ちます。なので、text&str型です。

&str型には.chars()という、文字列を文字(char型)のイテレータ(Chars型)に変換するメソッドがあります。イテレータにすれば.next()で順に要素を前から一つずつ見ていくことができます。なので、

let ch1 = text.chars().next().unwrap();
println!("{}", ch1); // H

このように先頭文字を取得できます。イテレータは次の要素がないかもしれないのでOption型を返すことから、unwrap()する必要があります。

追記(2019/06/04)

@pseudo_foxkeh さんに教えていただいた情報を載せておきます。

イテレータは.next()で順に要素を前から一つずつ見ていくことができますが、.nth()で直接 n 番目の要素を見ることもできます。というか、このメソッドの方が便利ですね。なので、.nth()を使うと

let ch1 = text.chars().nth(0).unwrap();
println!("{}", ch1); // H

このように書けます。なお、ここで得られるch1char型です。


もう一つの方法としてはスライスを使うことができます。感覚としてはPythonなどに近くて、

let ch2 = &text[..1];
println!("{}", ch2); // H

といけます。ここで得られるch2&str型です。


このように見るとスライスの方が便利のように見えますが、そうとも限りません。世の中の文字はASCII文字のような1バイトに収まるものばかりではないからです。

let text = "令和";

今度は漢字でやってみましょう。一つ目の方法は

let ch1 = text.chars().next().unwrap();
println!("{}", ch1); // 令
let ch1 = text.chars().nth(0).unwrap();
println!("{}", ch1); // 令

やはり上手くいきます。ところが二つ目の方法だと

let ch2 = &text[..1];
println!("{}", ch2); 
// thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside '令' (bytes 0..3) of `令和`', src/libcore/str/mod.rs:2027:5

とエラーを起こします。
エラーメッセージにある通り「令」という漢字は3バイトなのでこれではダメです。let ch2 = &text[..3];とすると上手くいきます。

追記(2019/05/31)

@termoshtt さんに教えていただいた情報を載せておきます。

今回「令」は3バイトなので、スライスをする際に3を指定してやる必要がありましたが、このようなバイト数の数値は毎回確認して決め打ちしておかねばならないのでしょうか?
実は、.char_indices()というメソッドを使えば、文字列の中の各文字の開始位置(スライスすべき位置)を得ることができます。
.char_indices()CharIndicesという型のイテレータを返します。この型は、Charsの拡張で、文字だけでなく文字の位置も含めてタプルとして保持します。
今回の例の場合、二文字目の開始位置を得てそれをスライスの終端とすればよいので、

let (i, _) = text.char_indices().nth(1).unwrap();
let ch2 = &text[..i];
println!("{}", ch2); // 令

または

let i = text.char_indices().nth(1).unwrap().0;
let ch2 = &text[..i];
println!("{}", ch2); // 令

他の例でも確認しておきましょう。

let text = "🍣🍺";
let (i, _) = text.char_indices().nth(1).unwrap();
let ch2 = &text[..i];
println!("{}", ch2); // 🍣
let text = "🍣🍺";
let i = text.char_indices().nth(1).unwrap().0;
let ch2 = &text[..i];
println!("{}", ch2); // 🍣

寿司の絵文字🍣は4バイトですが、やはり上手くいきます。


ここで問題になっているのは文字列のバイト数の取り方です。
&strという型自体は内部的には&[u8]、すなわち1バイト(=8ビット)の数が延々と続いているだけでそれを文字のかたまりごとに上手くスライスできる保証はありません。
一方、&strに備わっている.chars()を使えば、文字列に含まれるそれぞれの文字が何バイトであろうと、うまくバイトを区切って一文字ずつ取得し、イテレータにすることができます。また、.char_indices()を使えば文字列に含まれる各文字の開始位置がわかるので、それを使ってスライスをすることができます。

というわけで、アルファベットや単純な記号のみを使うのであればスライスで簡潔に済みますが、より複雑な文字を扱いたい場合、スライスだと逆に記述量が多くなってしまい、.chars()を使う方がよい、というのが実情のようです。さらなる別の方法があればぜひ教えてくださいm(_ _)m

Rustで文字列の部分文字列を取得する

@pseudo_foxkeh さんの情報をもとに追記しました。ありがとうございます。(2019/06/04)


先頭文字だけでなく、今度は部分文字列を取得することを考えてみましょう。
単純なASCII文字だけから成る文字列であれば、上記の通り直接スライスで範囲を指定して簡単にできるので、そのような簡単なケースは飛ばして最初から複雑な文字列を考えてみましょう。

let text = "🍣🍺大好き💖💖";

この文字列の真ん中の大好きの部分を取り出していきたいと思います。

Rustのイテレータには.enumerate()というインデックス付きのイテレータを返すメソッドがあります(他の言語にもこのような「インデックス付きのものを返す」機能はよくありますね)。
なので、このメソッドを用いて得られるインデックスを利用してfilterして、残った文字を畳み込みでくっつけてあげることを考えると、

let str1 = text.chars().enumerate().filter(|&(i,_)| i >= 2 && i < 5).fold("".to_string(), |s, (_, c)| format!("{}{}", s, c));
println!("{}", str1); // 大好き

こんな感じで頑張ってワンライナーで書くことができます。
もちろんワンライナーにこだわらなければ

let mut str1 = "".to_string();
for (i, c) in text.chars().enumerate() {
  if i >= 2 && i < 5 {
    str1.push(c);
  }
}
println!("{}", str1); // 大好き

でもいいですね。
なお、ここで得られるstr1String型です。


一方で、あくまでスライスにこだわる場合、

let begin = text.char_indices().nth(2).unwrap().0;
let end = text.char_indices().nth(5).unwrap().0;
let str2 = &text[begin..end];
println!("{}", str2); // 大好き

と書けます。なお、ここで得られるstr2&str型です。

先ほどの先頭文字の型(&strcharか)の場合と異なり、String&strは簡単に行き来できるので、返り値の型に神経質になる必要はそんなになさそうです。


こちらも、さらなる別の方法があればぜひ教えてくださいm(_ _)m

ちなみに、「Rust 部分文字列」でググると、古い記事の中には.slice_chars()メソッドを紹介しているものがありますが、すでに Deprecated になっているようです(参照: https://github.com/rust-lang/rust/pull/26914/files#diff-2544e5e5ce71797344d5ec6acde08b87R555 )。

理由としては, can be implemented with char_indices and hasn't seen enough use to justify inclusion と書かれているので、やはり.char_indices()を使ってね、ということみたいです。

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について
21