1
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【21/37】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第21回になります!

今回の関連ページ

[06_ticket_management/05_iter] イテレータ 2 ( Iterator トレイト? )

問題はこちらです。

lib.rs
use ticket_fields::{TicketDescription, TicketTitle};

// TODO: Provide an `iter` method that returns an iterator over `&Ticket` items.
#[derive(Clone)]
pub struct TicketStore {
    tickets: Vec<Ticket>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct Ticket {
    title: TicketTitle,
    description: TicketDescription,
    status: Status,
}

#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Status {
    ToDo,
    InProgress,
    Done,
}

impl TicketStore {
    pub fn new() -> Self {
        Self {
            tickets: Vec::new(),
        }
    }

    pub fn add_ticket(&mut self, ticket: Ticket) {
        self.tickets.push(ticket);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ticket_fields::test_helpers::{ticket_description, ticket_title};

    #[test]
    fn add_ticket() {
        let mut store = TicketStore::new();

        let ticket = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::ToDo,
        };
        store.add_ticket(ticket);

        let ticket = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::InProgress,
        };
        store.add_ticket(ticket);

        let tickets: Vec<&Ticket> = store.iter().collect();
        let tickets2: Vec<&Ticket> = store.iter().collect();
        assert_eq!(tickets, tickets2);
    }
}

解説

いつもなら「〇〇トレイトを実装しよう!」という流れになるところですが、今回は珍しく「トレイトではなく、本体が所有する iter メソッドを定義しましょう!」という問題になっています。

まぁ今回もトレイトが全く関わっていないわけではなく、「返り値の型が Iterator トレイトを実装している」という特徴があり、特に次回以降解決する部分のようです(コメントアウトしている関数のシグネチャがその伏線)。

lib.rs
impl TicketStore {
    // ...省略...

    // pub fn iter(&self) -> impl Iterator<Item = &Ticket> {
    pub fn iter(&self) -> std::slice::Iter<'_, Ticket> {
        self.tickets.iter()
    }
}

Book の方に書かれている内容ですが、要は今回の目的は「所有権を奪わず、アイテムの 参照 をイテレートしたい」というものです。そのための手段が2つ紹介されています。

  • TicketStore ではなくその参照 &TicketStore の方に IntoIterator を実装する
  • 参照を要素とするイテレータを返す

今回は2つ目のアプローチというわけで、そしてイテレータであることを表す Iterator トレイトを実装するのではなく、「そういう(参照の)イテレータを返す」だけですから、別にこの目的専用のトレイトはないのでしょう。慣例的に .iter() メソッドとしています。

今回の問題では、内部で所有している動的配列である tickets にある スライスのメソッド .iter() の返り値をそのまま返しています。 VecDeref 先がスライスなのでこの呼び出しが可能です。以上より、スライスの .iter() の返り値型 std::slice::Iter<'a, T> がそのまま TicketStore::iter の返り値型になっています。

'_'a という表記方法がこの章まで来て(ようやく)出てきました。そう、これがライフタイム注釈というもので、次のエクササイズの話題のようです。

[06_ticket_management/06_lifetimes] ライフタイム注釈

問題はこちらです。

lib.rs
use ticket_fields::{TicketDescription, TicketTitle};

// TODO: Implement the `IntoIterator` trait for `&TicketStore` so that the test compiles and passes.
#[derive(Clone)]
pub struct TicketStore {
    tickets: Vec<Ticket>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Ticket {
    pub title: TicketTitle,
    pub description: TicketDescription,
    pub status: Status,
}

#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Status {
    ToDo,
    InProgress,
    Done,
}

impl TicketStore {
    // ...省略...

    pub fn iter(&self) -> std::slice::Iter<Ticket> {
        self.tickets.iter()
    }
}
テストを含めた全体
lib.rs
use ticket_fields::{TicketDescription, TicketTitle};

// TODO: Implement the `IntoIterator` trait for `&TicketStore` so that the test compiles and passes.
#[derive(Clone)]
pub struct TicketStore {
    tickets: Vec<Ticket>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Ticket {
    pub title: TicketTitle,
    pub description: TicketDescription,
    pub status: Status,
}

#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Status {
    ToDo,
    InProgress,
    Done,
}

impl TicketStore {
    pub fn new() -> Self {
        Self {
            tickets: Vec::new(),
        }
    }

    pub fn add_ticket(&mut self, ticket: Ticket) {
        self.tickets.push(ticket);
    }

    pub fn iter(&self) -> std::slice::Iter<Ticket> {
        self.tickets.iter()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ticket_fields::test_helpers::{ticket_description, ticket_title};

    #[test]
    fn add_ticket() {
        let mut store = TicketStore::new();

        let ticket = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::ToDo,
        };
        store.add_ticket(ticket);

        let ticket = Ticket {
            title: ticket_title(),
            description: ticket_description(),
            status: Status::InProgress,
        };
        store.add_ticket(ticket);

        let tickets: Vec<&Ticket> = store.iter().collect();
        let tickets2: Vec<&Ticket> = (&store).into_iter().collect();
        assert_eq!(tickets, tickets2);
    }
}

解説

前エクササイズで取り組まなかった &TicketStore の方に Iterator を実装する問題です。先のエクササイズでは '_ としてとりあえず考慮しなくて?良かったのですが、今回は Lifetime elision 、つまりライフタイム省略が効かないパターンらしく、考慮する必要があるみたいです。

lib.rs
impl<'a> IntoIterator for &'a TicketStore {
    type Item = &'a Ticket;
    type IntoIter = std::slice::Iter<'a, Ticket>;

    fn into_iter(self) -> Self::IntoIter {
        self.tickets.iter()
    }
}

ライフタイム自体は 第8回第12回 にて軽く関連した話が出てきていましたが、表記という形でちゃんと向き合うのは今回が初めてですね。

本問題での 'a の使われ方はおおよそ次のような感じです(うーん間違ってるかも...)。

  • impl<'a>'a: 'a というライフタイムパラメータを使いますよという宣言。ジェネリクスの型パラメータと同じと考えるとわかりやすい
  • &'a TicketStore, &'a Ticket'a: ライフタイム 'a の間は生きていることが保証された参照、の意味
  • std::slice::Iter<'a, Ticket>'a: ライフタイム 'a の間だけ生きている構造体

こんな後半になってようやくライフタイムが登場していることについて、筆者の直感と100 Exercisesが一致しているからかなぁとか思ったりします。

というのも、正直Rustの習得において ライフタイムと常に向き合う必要はありません 。Rust言語自体の仕様策定に関わりたいとかならともかく、日々のコーディングではコンパイラに怒られた時だけ考えれば良いです。ですから筆者もライフタイムは曖昧にしか理解していません。

ライフタイムで何かつまずいたら、次の順番で解決を試みるのがお勧めです。

  1. Clone トレイトを実装しちゃダメなのか検討する
  2. もし効率面の問題ではなく借用等の問題から Clone トレイトを使えない、みたいな場合は、おそらく所有権の所在が不明である等が原因として考えられるので、 RcArc が使えないかを検討する
  3. もし効率面からなんとしても参照のまま複雑な構造体を扱いたい、という段になったら、ライフタイム注釈をつける

ライフタイム注釈を真正面から向き合わなくてもなんとかなるための考えや道具がRustにはいくつかあるわけです。今回は珍しく必要なシーン(実体ではなく参照を返したい)だったからようやく登場したという感じなのだと思います。普段は忘れていて大丈夫です!

ライフタイムに依存した構造体

おそらく Iter<'a, T> は要素実体への参照を保持していると考えられます。しかし名前には & が含まれず、「ライフタイム 'a だけ存在していられる」以上の情報は含まれていないです。どうやったらこんな感じの構造体が書けるのでしょうか...?フィールドに参照があれば良い...?難しくはないですが、軽く確認してみたいと思います。こんな感じでしょうかね。

Rust
#[derive(Debug)]
struct Hoge<'a, T> {
    my_slice: &'a [T],
}

fn main() {
    let h: Hoge<'_, u32>;

    {
        let v = vec![1, 2, 3, 4, 5];
        
        h = Hoge {
            my_slice: v.as_slice(),
        };
        
        println!("{:?}", h);
    }
    
    // println!("{:?}", h); // ここで参照するとコンパイルエラー!
}

ついでに PhantomData (幽霊型)という構造体を使って、ちょっと遊んでみました。表記上は参照を含みますが、この構造体はなんと実質 なんのデータも持たない のに、 ライフタイムパラメータだけはバッチリ有効 という存在になっています!

Rust
use std::marker::PhantomData;

struct Hoge;

#[derive(Debug)]
struct ResourceRelatedToHoge<'a>(PhantomData<&'a Hoge>);

impl<'a> ResourceRelatedToHoge<'a> {
    fn new(_: &'a Hoge) -> Self {
        Self(PhantomData)
    }
}

fn main() {
    let r: ResourceRelatedToHoge<'_>;
    {
        let hoge = Hoge;
        r = ResourceRelatedToHoge::new(&hoge);
        
        println!("{:?}", r);
    }
    // println!("{:?}", r); // ここで参照するとコンパイルエラー!
}

Rustで変わったことをしたい時(例えば筆者が実際に経験した例だと、RAII的に管理しなければならないWin32APIの某を扱う時など)に、「参照は要らないけど他の構造体に依存したい」という時は上記のように PhantomData 型を使うといい感じにできます!小ネタでした。

では次の問題に行きましょう!

次の記事: 【22】 コンビネータ・RPIT ~ \U0001F409 「 Iterator トレイトを実装してるやつ」~

1
0
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
1
0