LoginSignup
12
8

More than 1 year has passed since last update.

[Rust] 配列等から文字列のベクタ Vec<String> への変換とその仕組み

Posted at

文字列の変換自体は別記事にしました。

参考「[Rust] 文字列スライス str から文字列 String への変換とその仕組み - Qiita

0. まとめ

  • 文字列スライス &str
  • 文字列 String
要素の型が文字列スライス &str の場合
let array = ["foo", "bar", "baz"]; // 配列やベクタ、スライス等

let vec: Vec<String> = array.iter().map(|&s| s.to_string()).collect(); // |s| でなく |&s| 推奨 (※理由は後述)
// or
let vec: Vec<String> = array.iter().map(|&s| String::from(s)).collect();
// or
let vec: Vec<String> = array.iter().map(|&s| s.into()).collect();
// or
let vec: Vec<String> = array.iter().map(|&s| s.to_owned()).collect();

// 確認
assert_eq!(
    vec,
    vec!["foo".to_string(), "bar".to_string(), "baz".to_string()]
);
要素の型が文字列以外の場合
let array = [10, 20, 30]; // 配列やベクタ、スライス等

let vec: Vec<String> = array.iter().map(|x| x.to_string()).collect(); // メモ: i8, u8 等では |x| でなく |&x| 推奨 (※理由は後述)

// 確認
assert_eq!(vec, vec![10.to_string(), 20.to_string(), 30.to_string()]);

1. データ変換の流れ

配列等の要素の型が文字列スライス &str の場合
["foo", "bar", "baz"]                        : [&str; 3];
["foo", "bar", "baz"].iter()                 : Iter<&str>;
["foo", "bar", "baz"].iter().map(f)          : Map<Iter<&str>, {{closure}}>;
["foo", "bar", "baz"].iter().map(f).collect(): Vec<String>;
配列等の要素の型が文字列以外の場合
[10, 20, 30]                        : [i32; 3];
[10, 20, 30].iter()                 : Iter<i32>;
[10, 20, 30].iter().map(f)          : Map<Iter<i32>, {{closure}}>;
[10, 20, 30].iter().map(f).collect(): Vec<String>;
  1. 型強制によって、入力の型が配列等の場合もスライスのメソッドを呼べる
  2. スライスから Iter 型のイテレータに変換
  3. Iterator::map メソッドで Iter 型のイテレータから Map 型のイテレータに変換
  4. Iterator::collect メソッドで Map 型のイテレータから Vec<String> に変換

1.1. 型強制によるスライスへの自動型変換

配列からスライスへの Unsized 型強制や、メソッド呼び出し時の型強制等については別記事にしました。

参考「[Rust] 配列やベクタが自動でスライスに変換される仕組み - Qiita
参考「[Rust] メソッド呼び出し時におけるメソッド探索の仕組み: 自動参照 & 自動参照外し及び Unsized 型強制 - Qiita

let 文や関数引数等で行われる型強制とメソッド呼び出しによる型強制は仕組みが異なりますが、「Unsized 型強制」や「Deref 型強制」自体は同じで、配列やベクタ等はスライスに型強制されます。

  • [&str; N] -> [&str]: Unsized 型強制
  • Vec<&str> -> [&str]: Deref 型強制

※ Unsized 型強制はメソッド呼び出しではそのまま行われますが、let 文や関数引数等では参照などの「ポインタのような」型 (&[&str; N] 等) であ必要があります。

1.2. スライスからイテレータへ変換

スライスの iter メソッドを使用して、スライス用のイテレータの実装である (Iterator トレイトを実装する) Iter 型に変換します。

  1. [&str] -> &[&str]: メソッド呼び出し時の自動参照
  2. &[&str] -> Iter<&str>: iter メソッドによる変換
iter メソッドの定義
impl<T> [T] {
    // ...
    pub fn iter(&self) -> Iter<'_, T> {
        // ...
    }
    // ...
}

参考「iter - slice - Rust

let array = ["foo", "bar", "baz"];

use std::slice::Iter;
let iter: Iter<&str> = array.iter();

1.3. Iter 型は Iterator トレイトを実装

std::slice::Iter 型は core::slice::iter::iterator マクロを用いて Iterator トレイトを実装します。

iterator マクロの定義
macro_rules! iterator {
    (
        struct $name:ident -> $ptr:ty,
        $elem:ty,
        $raw_mut:tt,
        {$( $mut_:tt )?},
        {$($extra:tt)*}
    ) => {
        // ...
        impl<'a, T> Iterator for $name<'a, T> {
            type Item = $elem;
            // ...
        }
        // ...
    }
}
Iter 型の実装の一部
iterator! {struct Iter -> *const T, &'a T, const, {/* no mut */}, {
    // ...
}}

マクロの引数の値は以下のようになります。

  • $name = Iter
  • $elem = &'a T
Iter 型は Iterator トレイトを実装 (※理論上のソースコード)
impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;
    // ...
}

type T = &str; とすると、<Iter as Iterator>::Item&&str 型になります (本記事ではライフタイムの説明略) 。

参考「Iterator - Iter in std::slice - Rust

1.4. map メソッドによるイテレータから別のイテレータへ変換

map は他のプログラミング言語でも取り入れられることが多い機能で、イテレータの各要素を別の値に変換し、同じ長さで変換された要素を持つ別のイテレータに変換します。

map メソッドの定義
pub trait Iterator {
    // ...
    type Item;
    // ...
    fn map<B, F>(self, f: F) -> Map<Self, F>
    where
        Self: Sized,
        F: FnMut(Self::Item) -> B,
    {
        // ...
    }
    // ...
}

参考「map - Iterator in std::iter - Rust

Rust の場合は一度 Iterator トレイトを実装する Map 型に変換し、要素を変換するクロージャをラップし、要素の値を得ようとした段階で変換が行われます (遅延評価) 。

Map 型の next メソッドの定義
impl<B, I: Iterator, F> Iterator for Map<I, F>
where
    F: FnMut(I::Item) -> B,
{
    type Item = B;
    // ...
    fn next(&mut self) -> Option<B> {
        self.iter.next().map(&mut self.f)
    }
    // ...
}

self.iter.next().mapmapOption<I::Item> 型が持つメソッドで、イテレータの map メソッドとは別物です (本記事では不明略) 。

参考「Iterator - Map in std::iter - Rust

let array = ["foo", "bar", "baz"];

use std::slice::Iter;
let iter: Iter<&str> = array.iter();

let f = |&s: &&str| s.to_string(); // ※文字列の変換については後述

use std::iter::Map;
let map: Map<Iter<&str>, _> = iter.map(f);

ここでは文字列スライスから文字列に変換することが目的なので、関数 f&&str から String へ変換する関数です。

Map<Iter<&str>, _> のジェネリクスは以下のように解決されます。

  • type I = Iter<&str>;
  • type I::Item = &&str;
  • type B = String;
  • type Item = String;

※関数がクロージャの場合、一意で匿名 (ユニークでアノニマス) な型になるため、型名を表現することはできません。

参考「Closure types - The Rust Reference

1.5. イテレータは IntoIterator トレイトをブランケット実装

IntoIterator トレイトは「イテレータへ変換できる特性」で、イテレータ自身もブランケット実装します。

IntoIterator トレイトをブランケット実装
impl<I: Iterator> IntoIterator for I {
    type Item = I::Item;
    type IntoIter = I;

    #[inline]
    fn into_iter(self) -> I {
        self
    }
}

参考「IntoIterator - Map in std::iter - Rust

参考「Using Trait Bounds to Conditionally Implement Methods - Traits: Defining Shared Behavior - The Rust Programming Language」(「トレイト境界を使用して、メソッド実装を条件分けする」「ブランケット実装」)

I: Iterator (I 型は Iterator トレイトを実装) かつ iter: I (変数 iterI 型) のとき iter.into_iter() の戻り値の型も I 型になります。

use std::slice::Iter;

let foo: Iter<i32> = [10, 20, 30].iter();

let bar: Iter<i32> = foo.into_iter();

Iterator トレイトを実装する Map 型も IntoIterator トレイトをブランケット実装します。

1.6. ベクタ VecFromIterator トレイトを実装

FromIterator トレイトは「イテレータから変換できる特性」です。

IntoIterator トレイトを実装する型、すなわち「イテレータ自身を含むイテレータに変換可能な型」から Self 型に変換する機能を持ちます。

FromIterator トレイトの定義
pub trait FromIterator<A>: Sized {
    // ...
    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self;
}

参考「FromIterator in std::iter - Rust

ベクタ VecFromIterator トレイトを実装します。

impl<T> FromIterator<T> for Vec<T> {
    // ...
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Vec<T> {
        // ...
    }
}

参考「FromIterator<T> - Vec in std::vec - Rust

use std::slice::Iter;
let foo: Iter<i32> = [10, 20, 30].iter();

use std::iter::FromIterator;
let bar: Vec<&i32> = Vec::<&i32>::from_iter(foo);

1.7. イテレータからベクタへ変換

イテレータの collect メソッドは、自身のイテレータを FromIterator トレイトを実装する型へ変換します。

変換の際に FromIterator::from_iter メソッドを呼んでいます。

collect メソッドのデフォルト実装
pub trait Iterator {
    // ...
    type Item;
    // ...
    fn collect<B: FromIterator<Self::Item>>(self) -> B
    where
        Self: Sized,
    {
        FromIterator::from_iter(self)
    }
    // ...
}

参考「collect - Iterator in std::iter - Rust

let array = ["foo", "bar", "baz"];

use std::slice::Iter;
let iter: Iter<&str> = array.iter();

let f = |&s: &&str| s.to_string(); // ※文字列の変換については後述

use std::iter::Map;
let map: Map<Iter<&str>, _> = iter.map(f);

let vec: Vec<String> = map.collect();

ここで、map.collect() のジェネリクスは以下のように解決されます。

  • type Item = String;
  • type B = Vec<String>;

メソッド呼び出しを繋げると、以下のようになります。

let array = ["foo", "bar", "baz"];

// ※ map メソッドに渡すクロージャの中身については後述
let vec: Vec<String> = array.iter().map(|&s: &&str| s.to_string()).collect();

2. 文字列 String に変換する方法

2.1. 文字列スライス str から文字列 String の変換

Rust における文字列スライス str から文字列 String の変換は主に以下の 4 つです。

(※他にも、大文字や小文字に変換するメソッド等もあります。)

s: &str のとき:

  • s.to_string()
  • String::from(s)
  • s.into()
  • s.to_owned()

詳細は別記事にしました。

参考「[Rust] 文字列スライス str から文字列 String への変換とその仕組み - Qiita

2.2. 文字列以外から文字列 String の変換

フォーマットを指定せずに単に文字列に変換したい場合には to_string メソッドを使用します。

2.3. to_string メソッドの注意

to_string メソッドでは自動参照外しされません。

参考「[Rust] to_string メソッド等は呼び出し時に自動参照外しされない - Qiita

これにより、&str&&str で呼ばれる to_string が異なる (文字列に変換する具体的な処理が異なる) 等が起きます (バージョン 1.55.0 現在) 。

参考「[Rust] &str と &&str で呼ばれる to_string メソッドが異なる - Qiita

str 以外でも同様の現象が起きることがあります。

  • char, str, String
  • Cow<str>
  • i8, u8

参考「Implementors - ToString in std::string - Rust」(ToString トレイトを独自に実装する型等)

処理速度を気にするのであれば、明示的に参照外しをするか、型を修飾してメソッドを呼ぶ方が良いです。

3. 文字列 String に変換する関数またはクロージャを定義

Iter ではイテレータの要素の型に参照が付きます。

例:

  • type <Iter<&str> as Iterator>::Item = &&str;
  • type <Iter<i32> as Iterator>::Item = &i32;

3.1. to_string メソッドを使用する場合

to_string メソッドは明示的な参照外しをしなくてもコンパイルエラーになりませんが、前述の通り、型が独自に ToString トレイトを実装している場合を考えて明示的に参照外しするか型を修飾します。

関数やメソッドの引数において「パターン」を用いて参照外しすることができます。

参考「[Rust] 「パターン」を用いた非構造化変数束縛 - Qiita

// &str の場合
let f = |&s: &&str| s.to_string(); // 参照パターンによる参照外し
// or
let f = |s: &&str| (*s).to_string(); // 参照外し演算子 * による参照外し
// or
let f = |s: &&str| str::to_string(s); // 型を修飾して自動参照外し
// or
let f = |s: &&str| <str as ToString>::to_string(s); // 完全修飾構文で自動参照外し
// etc.

// i32 の場合
let f = |x: &i32| x.to_string();

Iterator::map メソッドの引数に直接クロージャを書く場合は、クロージャの引数の型を省略できます。

3.2. String::from メソッドや <&str>::into メソッドを使用する場合

frominto は対応する機能です。

どちらもジェネリック型を使用していて、その型を解決できる必要があります。
&&str から変換しようとするとジェネリクスを解決できず、コンパイルエラーになります。

明示的に参照外しするか型を修飾して対応します。

from を使用する場合
let f = |&s: &&str| String::from(s); // 参照パターンによる参照外し
// or
let f = |s: &&str| String::from(*s); // 参照外し演算子 * による参照外し
// or
let f = |s: &&str| -> String { From::<&str>::from(s) }; // 型を修飾して自動参照外し
// or
let f = |s: &&str| <String as From<&str>>::from(s); // 完全修飾構文で自動参照外し
// etc.
into を使用する場合
let f = |&s: &&str| -> String { s.into() }; // 参照パターンによる参照外し
// or
let f = |s: &&str| -> String { (*s).into() }; // 参照外し演算子 * による参照外し
// or
let f = |s: &&str| -> String { <&str>::into(s) }; // 型を修飾して自動参照外し
// or
let f = |s: &&str| -> String { <&str as Into<_>>::into(s) }; // 型を修飾して自動参照外し
// or
let f = |s: &&str| <&str as Into<String>>::into(s); // 完全修飾構文で自動参照外し
// etc.

Iterator::map メソッドの引数に直接クロージャを書く場合は、クロージャの引数の型や戻り値の型を省略できます。

3.3. str::to_owned メソッドを使用する場合

ToOwned トレイトは参照に対して再帰的にブランケット実装され、実装している型によって戻り値の型が異なるため、明示的に参照を外す型を修飾しないとコンパイルエラーになります。

ToOwned::to_owned メソッドの戻り値の型は Self でなく Self::Owned のため、戻り値の型から self の型を決定できません。

参考「[Rust] to_string メソッド等は呼び出し時に自動参照外しされない - Qiita

let f = |&s: &&str| s.to_owned(); // 参照パターンによる参照外し
// or
let f = |s: &&str| (*s).to_owned(); // 参照外し演算子 * による参照外し
// or
let f = |s: &&str| str::to_owned(s); // 型を修飾して自動参照外し
// or
let f = |s: &&str| <str as ToOwned>::to_owned(s); // 完全修飾構文で自動参照外し
// etc.

Iterator::map メソッドの引数に直接クロージャを書く場合は、クロージャの引数の型を省略できます。

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8