LoginSignup
0
0

19章ー3節:高度な機能ー型についての発展的な事項

Posted at

目次

19.3.0 型についての発展的事項の概要

  • ニュータイプパターンが有用な理由

    • 型をつけることで異なる種類の値同士が混同されることを防ぐことができる
      • 例えば、Meters(u32)Millmeters(u32) を混同せずに済む
    • 型の実装の詳細を抽象化することもできる
      • 例えば、ID と人名を関連付けて格納する HashMap<i32, String>People 型でラップすることで以下のような恩恵が得られる
        • People コレクションにメソッドを適切に定義することで、HashMap の提供するすべてのAPI ではなく、自分たちの定義したパブリックな API とだけやり取りするようにカプセル化を施すことができる
  • 型エイリアス

    • 型に "あだ名" をつけることができる
    • たとえば type Kilometers = i32; とすると、i32 型を指す際に Kilometers とすることができる
    • これは、複雑な型をコード内で何度も使用する必要があるときに有用
      • たとえば type Thunk = Box<dyn Fn() + Send + 'static>; のようにすると便利
  • never 型 !

    • todo!()panic!() や ループ中の break;continue;、他には break; を含まない loop{...} などは never 型 ! を返す
    • この ! はどのような型にも型強制される
      • match 式の一部のアームで使用した際に、一見すると型が食い違う場合も問題なく型強制される
  • 動的サイズ決定型はコンパイル時にメモリサイズが既知でない値を扱うための型

    • 例は str 型とすべてのトレイト
    • これらの型は、適当なポインタと組み合わせて使用する必要がある
      • たとえば、&strBox<dyn Trait> のように
  • Rust ではコンパイル時にメモリサイズが既知の値をあらわすトレイト Sized が提供されている

    • このトレイトはデフォルトですべての要素にあてがわれる
    • このデフォルトの処理を上書きしてジェネリック関数で動的サイズ決定型を使いたい場合は、fn generic<T: ?Sized>(t: &T) { のよう ?Sized トレイト境界を指定すればよい

19.3.1 型安全性と抽象化のためのニュータイプパターンの使用

→ 概要で述べた以上の情報なし

19.3.2 型のエイリアスを使用して型の同義語を作成する

  • Rustには、型エイリアスを宣言して既存の型に別の名前を付ける機能がある(=同義語を作成する)

  • これには type キーワードを使う

  • ただし、型エイリアスを使用しても、ニュータイプパターンのように型の混同を防ぐことにはならないことに注意

    • あくまで、既存の型の同義語を作る機能であって、新しい個別の型を作る機能ではない
    • 新しい型を作りたいならニュータイプパターンなどを利用すること
  • 例:

    type Kilometers = i32;
    
    let x: i32 = 5;
    let y: Kilometers = 5;
    
    println!("x + y = {}", x + y);  // i32 と Kilometers は同じ型なので問題なくコンパイルされ、実行もされる
    
  • この機能を使えば複雑で長ったらしい型名を何度も読み書きする必要がなくなる

    • 例:

      type Thunk = Box<dyn Fn() + Send + 'static>;
      
      let f: Thunk = Box::new(|| println!("hi"));
      
      fn takes_long_type(f: Thunk) {
          // --snip--
      }
      
      fn returns_long_type() -> Thunk {
          // --snip--
      }
      
  • この機能の最たる例は 標準ライブラリの std::io モジュールの Result<T, std::io::Error> である

    • 標準ライブラリでは type Result<T> = std::result::Result<T, std::io::Error>; という型エイリアスが設定されている

    • そのため、Result<T> と書くだけで Result<T, std::io::Error> を表せる

    • 例:以下のコードは、さらにその下のコードのように書き換えられる

      use std::fmt;
      use std::io::Error;
      
      pub trait Write {
          fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
          fn flush(&mut self) -> Result<(), Error>;
      
          fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
          fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
      }
      
      use std::fmt;
      use std::io::Error;
      
      pub trait Write {
          fn write(&mut self, buf: &[u8]) -> Result<usize>;
          fn flush(&mut self) -> Result<()>;
      
          fn write_all(&mut self, buf: &[u8]) -> Result<()>;
          fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
      }
      

19.3.3 never 型は絶対に返らない

→ 概要で述べた以上の情報なし

19.3.4 動的サイズ型と Sized トレイト

  • 動的サイズ決定型(DST, unsized 型):
    • 実行時にしかサイズを知ることができない値を使ったコードを書けるようにしてくれている
    • これらの型は直接関数の引数に渡すことができない
      • &, Box<...>, Rc<...> などを使ってポインタの背後に置いて使用する必要がある
    • 最たる例は str 型やトレイト

str 型について

  • str 型は動的サイズ決定型

  • たとえば、以下のようなコードは許されない:

    • あきらかに、s1s2 の長さ(必要なメモリ数)が異なる
    • Rust では、ある型の値はすべて同じ量のメモリを使わなければならないので、これは許されない
    let s1: str = "Hello there!";
    let s2: str = "How's it going?";
    
  • これは str&str にすることによって解消される

    • &str は通常の参照と異なり、ある値のメモリアドレスを格納するのみならず、文字列の長さも格納する

      • これが動的サイズ決定型 str が通常の型と異なる点
    • ある文字列の先頭へのポインタとその文字列の長さのみを格納するだけなので &str 型はコンパイル時にメモリサイズがわかる

  • 実は str はあらゆるポインタと組み合わせることができる

    • たとえば、Box<str>Rc<str> などにすることもできる

トレイトについて

  • すべてのトレイトも動的サイズ決定型である
    • トレイトも str と同様に、ポインタと組み合わせることで、型を指定するのに利用できる
      • たとえば、&dyn Trait, Box<dyn Trait>, Rc<dyn Trait> のように
    • 実は、トレイトオブジェクトがポインタと組み合わせて生成する必要があったのも、トレイトが動的サイズ決定型だからである

Sized トレイトについて

  • Rust には、型のサイズがコンパイル時に既知かどうかを判断するための Sized トレイトが存在する

  • このトレイトはコンパイル時にサイズが既知のすべてに対して自動的に実装される

  • さらに、Rust はすべてのジェネリック関数に Sized の境界を暗黙的に追加する

    • つまり、以下のようなコードは、さらにその下のコードに置き換えられてコンパイルされると考えればよい

      fn generic<T>(t: T) {
          // --snip--
      }
      
      fn generic<T: Sized>(t: T) {
          // --snip--
      }
      
  • デフォルトでは、ジェネリック関数はコンパイル時に既知のサイズを持つ型に対してのみ機能する

  • しかし、以下の特別な構文を使用すればこの制限を緩和できる:

    fn generic<T: ?Sized>(t: &T) {
        // --snip--
    }
    
  • ?Sized というトレイト境界は、「TはSizedであってもなくてもよい」という意味を持つ

  • また、t の型を T から &T に変更したことにも注意してほしい

    • 型が Sized でない可能性があるため、何らかのポインターの後ろで使う必要があるためこのようにしている

参考文献

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