Rustの文字列内の入れ替え
きっかけ
Rustを使えるようになりたいなと思ってちょこちょこ触ってます。
最近とあるところで入力された文字列の中で、その中のm番目とn番目(m,nは文字列のサイズ以下)を入れ替えるという問題が出ておりました。
C言語だと文字列は普通に配列のため、特に難しく考えずに行えるのですが、Rustだと地味に難しかったりします。
// このコードはエラーになります
fn main() {
let mut str = "fuga".to_string();
let c1 = str[0];
let c2 = str[2];
println!("{} {}", c1, c2); // "f g"を期待
}
あれ? 文字列ってスライス的に取れないのか? と思ったのですが許されないようです。
なおvscodeのコード補完などで見ると…
という感じです。ワタシムツカシイノワカラナイヨ。
文字列に関するメソッドの中では置き換えるためのメソッドで、 replace
が存在するのでこれはと思いましたが、ちょっと違いました。
文字列はバイト単位じゃないんだよね
Rustでは文字列に対してUTF-8(Unicodeというべきか)を基準としているようです。
そのため、文字単位で取り出す操作と言う考え方がメインになっているようで、バイト単位で考えたいのであれば、 as_bytes()
を使って強制的にバイト単位にぶった切るとかしないといけないみたいです。
なお日本語はUTF-8では基本3バイトのようです。
いろいろ悩んで結局こうなりました
- 文字列をベクタに変換する → ベクタならn番目がざっくり取れる
- 書き換え可能なものにする → 入れ替えてOK
- 最後にベクタを文字列に変換する
とりあえずベクタに変換して取り出すところまでやってみましょう。
fn main() {
let mut str = "fuga".to_string();
let mut chars: Vec<char> = str.chars().collect();
let c1 = chars[0];
let c2 = chars[2];
println!("{} {}", c1, c2); // "f g"を期待
}
これならf g
無事得られました。
ということで、これをベースに入れ替える操作をしてみます。
fn main() {
let mut str = "fuga".to_string();
let mut chars: Vec<char> = str.chars().collect();
let c1 = chars[0];
let c2 = chars[2];
chars[0] = c2; // 逆に入れる
chars[2] = c1;
// イテレータにして集めて結合(charで得られた文字の集合を結合してる
let result: String = chars.into_iter().collect();
println!("{} {}", c1, c2); // "f g"を期待
println!("{}", result);
}
これで望んだ結果 gufa
が得られました。
ま、Rustではタプルを用いてこういう書き方ができるのでもう少しスッキリできます。
fn main() {
let mut str = "fuga".to_string();
let mut chars: Vec<char> = str.chars().collect();
(chars[0], chars[2]) = (chars[2], chars[0]); // タプルで直接入れ替える
let result: String = chars.into_iter().collect();
println!("{}", result);
}
なお、Vec::swap()
もあるのでその方がわかりやすいかもしれません。
もう少し直感的な方法あるはずよね…
上記のコードをコンパイルすると、str
はmutでなくていいということでサジェストの警告が出ます。ということはメモリ上では別の場所にベクタを作っており、元のStringから別のメモリを用意しての動きとなっています。これってオーバーヘッドが少なからず発生してるかと思います。
何かもう少し直感的な方法がありそうに思うのですが、Rustの強者様達のご意見が得られれば幸いです。
Discussion
夜通し考えて気付いたら朝8時になってしまいました (以下、
m < n
を仮定してます)思いついた中で一番直感的なのは
で、一番速いのは
でした (雑なベンチマークなのであれですが参考までに : この記事の方法に対して、前者は3倍くらい、後者は5倍くらい速かったです)
こんなことに時間を浪費させてしまい申し訳ない…
ベンチマークはしていませんが、もしバイト単位でswapしたいなら(例えばASCII文字列だとわかっているのであれば)次のように
String::into_bytes
とString::from_utf8
で行うのが一番速いと思います。この方法だと所有権を移行しているので追加のメモリアロケーションは行いません。ただし、 Rust の文字列は有効な utf-8 である必要があるので、バイト列を編集した結果有効な utf-8 にならない場合、String::from_utf8
は失敗します(ここではunwrap()
しています)。で、もし多バイト文字を含む utf-8 なら、最後の例でも動きますが、次のようにすると unsafe を使わなくても済みます。
補足ありがとうございます!
swap
すればいいのはわかっています)unsafe
を使うことに抵抗がないので気にしてなかったのですが、指摘ありがとうございます (この場合mem::take
して*string = 〜
だとas_mut_vec
でやるよりパフォーマンスは落ちるので一長一短ですかね 。。。)パフォーマンスに関しては mem::take よりも効きそうなのは最後の
String::from_utf8
ですかね。これは文字列全体をスキャンして有効な utf8 であることを確認するということですから、特に長い文字列では効いてきそうです。 mem::take は実質的にポインタの交換ですから定数時間です。最後の代入も所有権を移行していますからポインタの代入です。どのような方法を取るにしろ、 utf-8 のチェックを回避するには unsafe が必要なので、性能的には safe と unsafe の壁がここに現れる気がします。
unsafe を許すかどうかは状況次第だと思いますので、使うべきではないとは思いませんが、 safe バージョンで書くならこうなるよというのは示しておいてもいいかなと思った次第です。
ナンデコンナテキトウナオハナシナノニツワモノタチカラこめんとイタダケテルンデスカ
という失礼なツッコミをさせていただきましたが、本当にありがとうございます。
個人的には短くてコードを読むストレスの少なさで、 「かなる」さんの1番目のやつを(遅いと言われても)使うのがストレス低めで良いかと思いました(やはりunsafeは最後の最後にしておきたい)。
ASCII文字列限定(バイト単位で処理できる)のであれば、msakutaさんのinto_bytes()案も捨てがたい所です。なにより所有権が移動してるのであれば安心感高まりますね。
unsafeを回避したいという気持ちはイケナイ?
返信ありがとうございます!(やっぱりUTF-8なんですね)
unsafeを回避したいという気持ちは僕の中ではもう擦り切れて消えてしまっていますが、言われてみればその方がおかしいんでしょうね 。。
パフォーマンス的にも「1番目では足りなくて2番目ならいける」という状況はかなり (競プロの難問くらいしか) 考えにくいので、普通は1番目でいいと思います (もしそんな状況になったら2番目のやつをコピペしていってください)
なお文字列に関してはUTF-8を基準にしてました。なので「あいうえお」の0,2文字目とかも想定しておく必要があるのでなにげに辛いところです。