所有権と借用

所有権 Ownership

  • 所有権とは
  • 所有権と値のライフスコープ
  • 所有権の移動
    • コピーセマンティクス
      • Copyトレイト実装型
        • 基本型 (bool, i32, ...)
        • 参照型
    • ムーブセマンティクス

所有権とは

値を保持できる変数は、必ず1つであるという制約のこと。

#[derive(Debug)]
struct V(i32);

// 変数aが値V(0)の所有者となる
let a = V(0);

// 変数bにaを代入すると、値V(0)の所有者が変数bに移る
let b = a;

// コンパイルエラー
dbg!(a);

所有権と値のライフスコープ

「値の所有者は1つである」という制約から所有する変数のスコープが値のライフタイムとなる。

struct V; 

let a = V;
{
    let b = a; // 値Vのライフタイム = 変数bのスコープ
}

所有権の移動

代入によって所有権が移動するかどうかは、Copyトレイトを実装しているかどうかによって決まる。

Copyトレイト

Cloneのサブトレイトであり、マーカートレイト。

#[derive(Debug)]
#[derive(Clone, Copy)]
struct V; 

let a = V;
{
    // 値がCopyされるので、aは所有権を失わない
    let b = a;
}
// aは有効
dbg!(a);

主なCopyトレイト実装型

  • 基本型
  • 参照型 &T

参照型&Tはコピーセマンティクス

fn get_str(str: &str) {
    let str1 = str;  // strがコピーされる
    let str2 = str1; // str1がコピーされる

    dbg!(str2); // OK
    dbg!(str1); // OK
    dbg!(str);  // OK
}

値の借用 Borrow

&演算子(Borrow)によって、参照型(&T)を生成することを「値を借用する」という。

このとき、値の所有権が移動しない

&演算子(Borrow)は、参照型を生成する。

let a = String::from("Hello World");

// &演算子によって参照型を生成する => 変数aの値を借用する => 所有権は移動しない
let ref_a: &String = &a;

借用ルール

  • mutable参照は一度に1つまでしか作れない
  • mutable参照が存在する場合は、借用できない
fn main() {
    let mut s = String::from("Hello");
    let mut ref_s = &mut s;

    // NG
    let mut ref_s2 = &mut s;
    
    ref_s.push(' ');
    ref_s2.push_str("World");

    /*
     error[E0499]: cannot borrow `s` as mutable more than once at a time
  |
3 |     let mut ref_s = &mut s;
  |                     ------ first mutable borrow occurs here
...
6 |     let mut ref_s2 = &mut s;
  |                      ^^^^^^ second mutable borrow occurs here
7 |
8 |     ref_s.push(' ');
  |     ----- first borrow later used here
    */
}

参照

References and Borrowing - The Rust Programming Language

関連

参照型 &T - あるマのメモ書き

ポリモーフィズム with トレイトオブジェクト

ポリモーフィズム (Polymophism)

ポリモーフィズム in Rust

deepLによる翻訳

ポリモーフィズム 多くの人にとって、ポリモーフィズムは継承と同義語です。しかし、実際にはもっと一般的な概念であり、複数の型のデータを扱うことができるコードを指します。継承の場合、それらの型は通常サブクラスです。

その代わりに、Rust はジェネリックを使ってさまざまな可能性のある型を抽象化し、 trait bounds を使ってそれらの型が提供しなければならないものに制約を与えます。これを bounded parametric polymorphism と呼ぶこともあります。

参照

オブジェクト指向経験者のためのRust入門 - Qiita

Object Oriented Programming Features of Rust - The Rust Programming Language

トレイトオブジェクト

Trait objects

動的サイズ決定型 (DSTs)

トレイトオブジェクトは、以下のポインタを持つワイドポインタ(動的サイズ決定型)

  • 値へのポインタ
  • 仮想メソッドテーブル

トレイトオブジェクトの形

  • Box<dyn SomeTrait>
  • &dyn SomeTrait

トレイトオブジェクトの目的

動的ディスパッチを実現する。

trait Printable {
    fn stringify(&self) -> String;
}

impl Printable for i32 {
    fn stringify(&self) -> String {
        self.to_string()
    }
}

// Box
fn print(a: Box<dyn Printable>) {
    println!("{}", a.stringify());
}

// 参照
fn print2(a: &dyn Printable) {
    println!("{}", a.stringify());
}

fn main() {
    print(Box::new(10));
    print2(&20);
}

関連

トレイトオブジェクトになれないトレイト - あるマのメモ書き

関連型を持つトレイトオブジェクトを宣言する - あるマのメモ書き

参照

https://doc.rust-lang.org/reference/types/trait-object.html

https://qiita.com/hadashiA/items/d0c34a4ba74564337d2f#comment-99db3667a99310a68644

&Foo はトレイトオブジェクトですよ。 トレイトオブジェクトで重要なのは、仮想関数テーブルとオブジェクト実体へのポインタの組であるという点です。トレイトオブジェクトそのものはトレイト境界が実装された型の参照からキャストすればいくらでも得ることができるので、ボクシングを行う必要はありません。

ボクシングされたトレイトオブジェクトの利点は、具体的な型情報が消去された状態でトレイトオブジェクトの所有権を管理できるという点です。なので、逆に言えば所有権を管理する必要がなければボクシングは必要ありません。上に例示したコードのように借用で済むはずです。

ジェネリクス関数 - impl trait引数とトレイト境界の違い

元ネタ

rust - What are the differences between an impl trait argument and generic function parameter? - Stack Overflow

回答

impl trait引数は、トレイト境界に脱糖(desugar)される。(ゆえにトレイト境界の糖衣構文である)

したがって以下は同じ。

trait Foo {}

fn func1(_: impl Foo) {}

fn func2<T: Foo>(_: T) {}

唯一の違いは、impl Trait引数は、その型を明白に指定させることができない。(ターボフィッシュが使えない)

trait Trait {}

fn foo<T: Trait>(t: T) {}
fn bar(t: impl Trait) {}

impl Trait for u32 {}

fn main() {
    foo::<u32>(0); // this is allowed
    bar::<u32>(0); // this is not
    /*
     error[E0632]: cannot provide explicit generic arguments when `impl Trait` is used in argument position
  --> src/main.rs:10:11
   |
10 |     bar::<u32>(0); // this is not
   |           ^^^ explicit generic argument not allowed
    */
}

anyhowを使う

anyhow::Error

https://docs.rs/anyhow/1.0.44/anyhow/struct.Error.html

動的エラー型を包み込んだエラー型。

Box<dyn std::error::Error>との違い

  • 静的な型
  • もどり道(backtrace)を保証する
  • narrow pointer

Display representations

Display 表し方

esprintln!("{}", err)

undering error(anyhow::Errorが包み込んだerror) もしくは、contextを表示する。

例: Failed to read instrs from ./path/to/instrs.json

esprintln!("{:#}", err)

anyhowのcausesのデフォルトフォーマットを出力する

例: Failed to read instrs from ./path/to/instrs.json: No such file or directory (os error 2)

esprintln!("{:?}", err)

キャプチャーしたバックトレースを出力する。

例: Error: Failed to read instrs from ./path/to/instrs.json

Caused by: No such file or directory (os error 2)

anyhow::Result<T>

実態

//anyhow
type Result = Result<T, anyhow::Error>

fn mainでは返すことができない。 anyhow::Errorを伝搬してくれる。

anyhow::Context

Result<T, anyhow::Error>に変換するcontextメソッドを提供する。

std::core::Result<T, E>からの変換

let file = File::open("memo.txt").context(format!("unable to open memo.txt)")?;

Option<T>からの変換

let first = vec.first()?.context("unable to get first from vec")

anyhow! bail!

anyhow!マクロ

anyhow::Errorをかんたんに生成

例: stringからの生成

fn lookup(key: &str) -> Result<V> {
    if key.len() != 16 {
        return Err(anyhow!("key length must be 16 characters, got {:?}", key));
    }

    ...

bail!マクロ

実態

return(anyhow!($args....))

これを使うことでかんたんにanyhow::Result<T>にして返すことができる

if !has_permission(user, resource) {
    bail!("permission denied for accessing {}", resource);
}

thiserrorとの組み合わせ

#[derive(Error, Debug)]
enum ScienceError {
    #[error("resouce limit exceeded")]
    RecursionLimitExceeded,
    ...
}

if depth > MAX_DEPTH {
    bail!(ScienceError::RecursionLimitExceeded);
}

anyhowメモ

anyhowのメリット

  • エラーに説明を追加できる
  • anyhow::Result<T>は、エラーの伝播ルートをスタックトレースとして出力してくれる

anyhowがない場合

Result<T, E>std::error::Errorトレイトを使う。

fn count_words<R: Read>(input: &mut R) -> Result<u32, Box<dyn Error>> {
    let reader = BufReader::new(input);
    let mut wordcount = 0;
    for line in reader.lines() { // line = Result<String>
        for _word in line?.split_whitespace() {
            wordcount += 1;
        }
    }
    Ok(wordcount)
}

fn main() -> Result<(), Box<dyn Error>> {
    // 複数ファイル対応
    for filename in env::args().skip(1).collect::<Vec<String>>() {
        // どのファイルが開けなかったのか出力されない
        let mut reader = File::open(&filename)?;

        let wordcount = count_words(&mut reader)?;
        println!("{} {}", wordcount, filename);
    }
    Ok(())
}

anyhowがある場合

anyhow::Result<T>anyhow::Error型を使う。

use anyhow::{Result, Context};

use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;

fn count_words<R: Read>(input: R) -> Result<u32> {
    let reader = BufReader::new(input);
    let mut wordcount = 0;
    for line in reader.lines() {
        for _word in line.context("not line")?.split_whitespace() {
            wordcount += 1;
        }
    }

    Ok(wordcount)
}

fn run() -> Result<()> {
    for filename in env::args().skip(1).collect::<Vec<String>>() {
        // contextで、anyhow::Errorに変換し、説明を付与できる。
        let file = File::open(&filename).context(format!("unable to open {}", filename))?;
        let wordcount = count_words(&file).context(format!("unable to count words in {}", filename))?;
        println!("{} {}", wordcount, filename);
    }
    Ok(())
}

fn main() /*NG main関数ではResult<(), anyhow::Error>を返せない-> Result<()>*/ {
    if let Err(error) = run() {
        // Errorを出力する
        eprintln!("Error: {:?}", error);
        std::process::exit(0);
    }
}

Borrow<Borrowed>とToOwnedトレイト

  • Borrow<Borrowed>トレイト - Borrowed型を借用できることを表す
  • ToOwnedトレイト - Borrow<Borrowed>型を実装すると自動実装される
use std::borrow::Borrow;

#[derive(Debug)]
struct ToolBox(Tool);
#[derive(Debug)]
struct Tool;

impl Borrow<Tool> for ToolBox {
    fn borrow(&self) -> &Tool {
        &self.0
    }
}


let tool_box = ToolBox(Tool);
let tool = tool_box.borrow() as &Tool;
// ToOwnedが自動導出される
let tool_box = tool.to_owned();
dbg!(tool_box);
代表的なBorrow<Borrowed>型
  • String = Borrow<str>
  • Vec<T> = Borrow<[T]>
  • Box<T> = Borrow<T>
  • PathBuf = Borrow<Path>

関連

yossan.hatenablog.com