Charaken 技術系ブログ

技術系に関して学んだことや、気になったことを記事にしていく。

【Rust】がばがばRust独学 - 4. Ownership - 4 Slice Type

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回も、OwnershipのSlice Typeについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


SliceType

所有権を持たない別のデータ型をsliceと呼びます。

例えば、スペースが区切りで文字列の始めの1単語を持ってくるような関数を考えた場合、 下記のような関数が想定されます。

fn first_word(s: &String) -> ?

まずは、ドキュメント(The Slice Type - The Rust Programming Language)の demo codeと同様に末尾のindexを返すパターンを実行すると正常に動作します。

fn main() {
    let s = String::from("Hello Rust!!");
    let fs = first_word(&s);
    println!("fs -> {}", fs);
}

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}
fs -> 5

実装としては、文字列slet bytes = s.as_bytes(); によってbyte配列に変換後、 bytes.iter().enumerate() によりバイト配列をiteratorとして反復させて、 スペースのバイト(b' ')との比較でスペースが出てきたらその位置を返すような処理になっています。

下記は println!("item -> {}, space byte -> {}", item, b' '); により、比較を確認するようにしたコードになります。

fn main() {
    let s = String::from("Hello Rust!!");
    let fs = first_word(&s);
    println!("fs -> {}", fs);
}

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        println!("item -> {}, space byte -> {}", item, b' ');
        if item == b' ' {
            return i;
        }
    }

    s.len()
}
item -> 72, space byte -> 32
item -> 101, space byte -> 32
item -> 108, space byte -> 32
item -> 108, space byte -> 32
item -> 111, space byte -> 32
item -> 32, space byte -> 32
fs -> 5

しかし、demoコードでも記載されているように、対象文字列をclearした場合であっても、 文字列 s と、初めのword位置 fs までの位置は連動していないため、 エラーの原因となりやすく、脆弱になってしまいます。

fn main() {
    let mut s = String::from("Hello Rust!!");
    let fs = first_word(&s);
    s.clear();
    println!("fs -> {}", fs);
}

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}
fs -> 5

上記の解決策として、 Stringslice があります。

Stringslice

文字列に対して &s[0..5] や、0始まりのsliceの場合は &s[..5]; 、文末までの場合は &s[5..]; のように記述してsliceすることが可能です。

Hello をsliceによって切り出したものが下記になります。

fn main() {
    let s = String::from("Hello Rust!!");
    let slice = &s[..5];
    println!("slice -> {}", slice);
}
slice -> Hello

上記の場合、ドキュメントにNOTEとして記載されておりますが、 マルチバイト文字の場合、途中で文字列sliceを作成しようとするとプログラムがエラーを起こして終了するとのことです。 そのため、ASCII以外のUTF-8による詳細な処理はドキュメントのChapter 8を参照するとのことでした。

Note: String slice range indices must occur at valid UTF-8 character boundaries. If you attempt to create a string slice in the middle of a multibyte character, your program will exit with an error. For the purposes of introducing string slices, we are assuming ASCII only in this section; a more thorough discussion of UTF-8 handling is in the “Storing UTF-8 Encoded Text with Strings” section of Chapter 8.

sliceを利用してfirst wordを取得する場合は下記のようになります。

fn main() {
    let s = String::from("Hello Rust!!");
    let slice = first_word(&s);
    println!("s -> {}, slice -> {}", s, slice);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
s -> Hello Rust!!, slice -> Hello

&str で参照している s を消した場合(s.clear();)は、 参照先が無くなるためエラーとなります。

fn main() {
    let mut s = String::from("Hello Rust!!");
    let word = first_word(&s);
    s.clear();
    println!("the first word is: {}", word);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> demo.rs:4:2
  |
3 |     let word = first_word(&s);
  |                           -- immutable borrow occurs here
4 |     s.clear();
  |     ^^^^^^^^^ mutable borrow occurs here
5 |     println!("the first word is: {}", word);
  |                                       ---- immutable borrow later used here

上記のコードで、気になる点が2点あります。

  • str とは何者?
  • &str のスコープが生きている
str は何者?

str の公式ドキュメント(std::str - Rust, str - Rust)を見ると、

let s = String::from("hello world");

とした場合の "hello world"&str であり、Primitive Typeであるとのこと。

公式ドキュメント(The Slice Type - The Rust Programming Language)のFigure 4-6がわかりやすく、図の右側の表が str であり、String である sstr を参照することにより String として利用でき、また、first_word として返している &str は図の world 部分にであり、参照を返している。

関数の戻り値で &str が利用できる理由

上記の通り、 str の実体が存在するため、ポインタとして &str は返されるため利用ができる。 しかしながら、その場合、実体である str のownershipはどのようの制御されるのだろうか・・・? この疑問はまた別で確認していきます。

その他のslice

配列についても同様にsliceが可能です。

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];

    print!("a -> [");
    for (_i, &item) in a.iter().enumerate() {
        print!("{}, ", item);
    }
    println!("]");

    print!("slice -> [");
    for (_i, &item) in slice.iter().enumerate() {
        print!("{}, ", item);
    }
    println!("]");
}
a -> [1, 2, 3, 4, 5, ]
slice -> [2, 3, ]

終わりに

sliceによるポインタ周りの理解が深められたが、 str の実体のownership周りが難しい・・・ 教えてください、偉い人!