2025/05/27
はじめに
RustでString
を特定のセパレータで分割する際、楽に実現する場合に s.split(sep).collect::<Vec<_>>()
のような使い方で、Vecに変換することが可能。
しかし、Vecで保持する場合、その長さのチェックが必要な場合は、そのチェックが通ったコードなのかどうかの境界を設ける必要が出てしまう。
そこで、Arrayに変換することで、
- 要素数チェックの完了を型で表現・保証する
- コンパイラによる静的解析の向上
- 実行コードでの利用自由度向上
の恩恵が得られるので、その方法を紹介する。
本記事内では、一旦は記述の簡素さを重視したサンプルを書いていく。Result
の取り扱いも視認性のために省略するものとする。
素朴な分割の場合
以下のcommand
がある時、これをスペース区切りにしたければ、以下のように書くのが最もシンプルと思われる。
そして、即座に要素を変数に割り当てて使う場合は、これで十分目的を果たすことができる。
Split
のままでも良いが、イテレータのままだと取り扱いがしにくいので、Vec
に変換する。
let command = "cargo run app";
let inputs = command.split(' ').collect::<Vec<&str>>();
assert_eq!(inputs, vec!["cargo", "run", "app"]);
このsplitは十分目的を果たすが、
assert_eq!(inputs[3], "app");
このindexを間違えたコードはコンパイルを通過してしまう。
通常、このようなアクセスをする際は、パース(split)したあとに、長さチェックを行い、プログラムが求める入力の条件と合わない場合はエラーを返す必要がある。 長さチェックを通過したコードかどうかの管理は、プログラムの記述時に工夫が必要になる。
固定長配列にする場合
これまでのコードに手を加えて以下のようにすると、inputs
を取得した時点でサイズが確定する。
let command = "cargo run app";
let inputs: [&str; 3] = command.split(' ').collect::<Vec<_>>().try_into().unwrap();
assert_eq!(inputs, ["cargo", "run", "app"]);
この実装においては、
assert_eq!(inputs[3], "app");
この先程のようなindexを間違えたコードは、
this operation will panic at runtime
`#[deny(unconditional_panic)]` on by defaultrustcClick for full compiler diagnostic
というメッセージとともにコンパイラの静的解析によってビルド前(実行前)に検出される。
そしてこれは、サイズが確定していて、すべての要素が初期化済みとして安全に利用できるだけではなく、
let [arg0, arg1, arg2]: [&str; 3] = command.split(' ').collect::<Vec<_>>().try_into().unwrap();
assert_eq!(arg0, "cargo");
assert_eq!(arg1, "run");
assert_eq!(arg2, "app");
このように、要素に直接名前をつけて変数にすることもできる。
let [_, arg1, arg2]: [&str; 3] = command.split(' ').collect::<Vec<_>>().try_into().unwrap();
このように、使わない部分の無視もコードとして表現でき、コードの可読性を向上させることに大きく寄与する。
固定長配列への変換をメソッドとして使えるようにする
上の実装は分割の利便性向上のために書いてきたが、このままだと、splitからのtry_intoの組み合わせが並ぶことで、記述が冗長となってしまう。
そこで、上述したArrayへの変換をtraitに定義することで、&str
のメソッドとして使えるようにしておく。
ここで、ジェネリクスとして配列のサイズをコンパイル時に指定できるようにすることで、任意のサイズの配列に変換できるようにする。
以下が、動作する完全なコード例である。
pub trait SizedSplit {
type Error;
fn sized_split<const N: usize>(&self, sep: &'static str) -> Result<[&str; N], Self::Error>;
}
impl SizedSplit for str {
type Error = ();
fn sized_split<const N: usize>(&self, sep: &'static str) -> Result<[&str; N], Self::Error> {
let strs = self.split(sep).collect::<Vec<&str>>();
match strs.try_into() {
Ok(arr) => Ok(arr),
Err(_) => Err(()),
}
}
}
fn main() {
let command = "cargo run app";
let inputs = command.sized_split::<3>(" ").unwrap();
assert_eq!(inputs, ["cargo", "run", "app"]);
let [_, subcommand, arg] = command.sized_split::<3>(" ").unwrap();
assert_eq!(subcommand, "run");
assert_eq!(arg, "app");
assert!(command.sized_split::<2>(" ").is_err());
assert!(command.sized_split::<3>(" ").is_ok());
assert!(command.sized_split::<4>(" ").is_err());
}
おわりに
SizedSplit::sized_split
の引数について、2025/05/27時点でnightlyであるstd::str::pattern::Pattern
を使うとsep
の表現がsplitの引数と同じで使えるようになる。