はじめに
Rust、素敵です。ネイティブコードを吐けるコンパイル言語なのに、こんなに書きやすい。これまでC/C++での悩みだった「コンパイルは通るけど、まともに動かない。つか落ちる」ということがほとんどなく、日常遣いの書き捨てツールでも、ガンガン使えます。うれし。
でも、いくつかハードルがあります。ご存じの通り、借用の概念は身につくまでに多少苦労しますし、Option
とかResult
とか、付随してmatch
とかtry!
とかif let
とかにも、慣れるまでちょっとした違和感の谷があります。
ともあれ、「普段使いできるコンパイル言語」って、長い間待ち焦がれてきたものだったので、Rustには感謝しかありません。……でも昨日、ちょっといやーんな場面に出くわしました。
byte index 4 is not a char boundary; it is inside '請' (bytes 3..6) of `申請日時`
落ちた! 落ちたよ! Panicだよ。unwrap()
で暫定的に手抜いていたところじゃないよ! とひとしきりワンワン吠えました。
何が起きたか
どういうことが起きたかというと、スライスの境界が文字と文字の間ではなく、ひとつの文字をぶった切るようになってしまった、ということです。ご存じとは思いますが、ASCII文字以外のUTF8の文字って、人間にとっての1文字が、バイト列としては3×nバイトになります。今回の例で言うと、文字とメモリイメージの対応は下図の肌色部分のようになっていまして、それに対して、赤で示した範囲をスライスで取ろうとしたのでPanicしたのです。
これはガード不能攻撃で、予測不能な入力文字列をStringに入れて、as_str()で中身を覗いてスライスに対して判定をかます、という大変当たり前なコードが、チェックする暇もなく落とされてしまうのです。スライスがResult
を返してくれるわけでもないので、そうすると、&str
のスライス自体が……詰んでね?
聖典を見る
では、とRust Bookを見ます。章のタイトルに「UTF」が入っているものを検索すれば、すぐに該当の記述は見つけられます。
ここに言及されてはいますが、あまり助けになる説明はありません。軽く絶望します。事前に[u8]
にして自力で妥当な切れ目を調べれば回避可能ですが、それも頭が悪そうな気がしますので、もう少しなんとかならないものでしょうか。まあcrate.ioのリンクがあったので、使えそうなcrateを探すことにしました。一応こんなのがあって、書記素に分割してくれるようですが、別に書記素に分けたいわけじゃないんですよね。
暫定解決策と展望もしくは教えてエロい人
単にこの際、&str
のスライスは禁じ手にして、chars()
イテレータを走査するのがいいのかもしれません。そういうもんなの? 教えてエロい人! ボスケテ……。コンパイラスイッチで&str
のスライスをエラーにさせてほしい気分です。当面はchars().collect()
で作ったVec<char>
に対して、各種文字列処理を行う道具を整えようと思います。割と使うけど自明でない(=作っておく必要がある)のは、cmp, instr, replace, atoi系, toupper系, 全角半角変換, かなカナ変換
とか、その辺ですかね。正規表現マッチは自分で書きたくありませんので、そのときはStringに戻しますかね。抽象度の高い正規表現があって、そのままVec<char>
にも使えたらいいけど、期待できない気がします。自分で書くのは……車輪の再発明という意味ではDr.STONEなのですが、そそらないです、これは……。
何か、盛大に勘違いしていますか、私は?
太字でうっすら思っていた通り、盛大に勘違いしていました
コメントで教えていただいたので、追記です。上記の通り、太字でうっすら思っていた通り、盛大に勘違いしていました。
fn main() {
let s = "申請日付";
println!("{}", s.get(0..3).unwrap_or("ouch"));// 申
println!("{}", s.get(0..4).unwrap_or("ouch"));// ouch
()
}
そんなわけで、getなら、スライスと同じ結果を、Resultに包んで得られるのです。不用意にスライスを使うと危ない、というのは、それはそれとして何か対策が必要な気はしますが、まずは正しいやり方が分かって良かったです。ありがとう、そして、ありがとう。
なお、ハコヅメは第1話は楽しめましたが、第1話なのにテンポというか間合いがもっさりしていて、若干不安が残りました。