あずんひの日

あずんひの色々を書き留めるブログ

Rust 1.65を早めに深掘り

こんにちは、あずんひ(@aznhe21)です。この歳になってついに運転免許の取得を決意しました。

さて、本日11/4(金)にRust 1.65がリリースされました。 この記事ではRust 1.65での変更点を詳しく紹介します。 もしこの記事が参考になれば、記事末尾から活動を支援頂けると嬉しいです。

ピックアップ

個人的に注目する変更点を「ピックアップ」としてまとめました。 全ての変更点を網羅したリストは変更点リストをご覧ください。

関連型でジェネリクスが使えるようになった

皆さん待望のGATs(Generic Associated Types)と呼ばれる機能が使えるようになりました。 これはトレイトの関連型でジェネリクスが使える機能でトレイトの自由度が格段に上がります。 ただし現在の実装には後述するように多くの制限があることに注意してください。

trait Trait {
    // 普通の関連型
    type A;
    // ジェネリクスを使った関連型
    type B<T>;
    // ジェネリクスと境界を使った関連型
    type C<T>
    where
        T: std::ops::Add;
    // もちろんライフタイムや定数もOK
    type D<'a>;
    type E<const N: usize>;
}

下記の例では関連型Itemにライフタイムを要求するイテレーターを定義しています。 このイテレーターでは要素を保持したまま次の要素に進むことはできません。 これは、ファイルを段階的に読みながら進むような場合に間違った使い方を抑制することができます。

trait LendingIterator {
    type Item<'a> where Self: 'a;
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

// cannot borrow `iter` as mutable more than once at a time
fn hoge<I: LendingIterator>(mut iter: I) {
    let x = iter.next().unwrap();
//          ----------- first mutable borrow occurs here
    let y = iter.next().unwrap();
//          ^^^^^^^^^^^ second mutable borrow occurs here
}

tarクレートArchive::entriesではイテレーターを進めたあとで前の要素を扱うとデータが壊れるとの注意書きがされていますが、 代わりにLendingIteratorを用いることでこの罠を回避することが出来るでしょう。

なお、現在のボローチェッカーではこのLendingIteratorにはfilterメソッドを定義できないなどの問題があります。 他にもGATsにはオブジェクト安全ではない(&dyn Traitにできない)問題やwhere Self: 'aが毎回必要で鬱陶しいといった問題(?)もあります。 これらは将来的には改善されていく予定です。 詳細は、英語ですがGeneric associated types to be stable in Rust 1.65The push for GATs stabilizationなどをご覧ください。

ちなみに、将来的にはGATsを糖衣構文とすることでトレイトでの非同期関数が使えるようになる予定です。

letによる束縛でパターンとelseを書けるようになった

letによる束縛時、左辺には変数名の代わりにパターンを、右辺のあとにelseを書くことで 「パターンにマッチしなかった場合」の処理が書けるようになりました。 Swift言語が分かる方向けに言えばguard let文と概ね同じと言えます。

例えばOption<T>の値がSomeなら中身を取り出しNoneならcontinueすることを考えます。

Rust 1.64まではこの様に4行必要だったのが・・・

let value = match value {
    Some(v) => v,
    _ => continue,
}

Rust 1.65からはこの様に1行で書けるようになります。

let Some(value) = value else { continue };

なお、このelseブロックは!型が返るのを要求するため、returnpanic!()などによりelseブロックの処理を終わらせる必要があります。

// OK
let 1 = 1 else { return };
let 1 = 1 else { panic!() };
let 1 = 1 else { std::process::exit(0) };

// NG: `else` clause of `let...else` does not diverge
let 1 = 1 else { 1 };
//             ^^^^^ expected `!`, found integer

また、elseが不要な場合(反駁不能=irrefutableなパターン)では警告が発されます。

//  warning: irrefutable `let...else` pattern
let x = true else { return };

このlet-elsePerlRubyなどにおけるunless文のように使うこともできますが、混乱するだけなのでやめておいた方が良いでしょう (そもそもlet-elseはパターンマッチであり比較ではない)。

let x = 0;
// これと・・・
if x != 0 {
    return;
}
// これは同じように動く(ただしlet-elseでは所有権を奪うことがある)
let 0 = x else { return };

名前付きブロックにより処理途中で抜けられるようになった

Rust 1.65ではブロックに名前を付けられるようになり、breakに名前を指定することでそのブロックを抜けられるようになりました。

// 普通のブロック。変数の寿命を短くするのに使われることが多い
{
    // このvはブロック内でしか生きられない
    let v = vec![0, 1, 2];
}

// ライフタイムと同じ記法でブロックに名前を付ける
'x: {
    break 'x; // breakに名前を指定してブロックから抜ける
}

またbreakに値を指定することで、ブロック末尾から値を返すのと同じように、ブロックを抜ける際に値を返すことができます。

// 普通のブロックでは末尾の式から値が返される
let a: usize = {
    let v = vec![1, 2, 3];
    v.iter().copied().sum()
};

// 名前付きブロックではbreak時に値を指定することでも値を返せる
let b: usize = 'b: {
    let v = vec![1, 2, 3];
    if rand::random() {
        break 'b 0;
    }
    v.iter().copied().product()
};

メソッドチェーンをコネコネする代わりに使うと便利かもしれません。

// あなたはどっち派?

// メソッドチェーン派
let x = std::fs::read_to_string("hoge.txt")
    .ok()
    .and_then(|value| value.parse().ok())
    .unwrap_or(0);

// 名前付きブロック派
let x = 'x: {
    let Ok(value) = std::fs::read_to_string("hoge.txt") else { break 'x None };
    let Ok(value) = value.parse() else { break 'x None };
    Some(value)
}.unwrap_or(0);

バックトレースの取得・管理ができるようになった

Rust 1.65ではstd::backtraceモジュールとその中の型が使えるようになり、 バックトレースの取得・管理ができるようになりました。 現時点ではバックトレースの取得と全体の表示のみができます。

バックトレースの取得というのは非常に遅い処理であるため、 標準APIであるBacktrace::captureは初期状態では何も取得しません。 環境変数RUST_LIB_BACKTRACERUST_BACKTRACE0以外の値を設定することでバックトレースを取得できるようになります。 また、Backtrace::force_captureを使うことで強制的に取得することもできます。

// 環境変数によって動作が変わる
let backtrace = std::backtrace::Backtrace::capture();
println!("{backtrace:#?}");
// RUST_LIB_BACKTRACEが未設定では<disabled>

// 環境変数に関わらず常に取得
let backtrace = std::backtrace::Backtrace::force_capture();
println!("{backtrace:#?}");
// Backtrace [
//     { fn: "hoge::main", file: "./src/main.rs", line: 7 },
//     // ごちゃごちゃ
//     { fn: "main" },
//     { fn: "__libc_start_main" },
//     { fn: "_start" },
// ]

多くの言語ではエラー型にバックトレースが含まれており、エラー発生場所が分かることによってデバッグを支援しています。 Rustでもanyhowクレートbacktrace機能を有効にすることで、 Errorオブジェクトにエラー生成場所のバックトレースが含まれるようになります。 なお、これは「anyhowクレートのErrorオブジェクトが生成された場所」であり、 std::io::Errorなど大元のエラーが発生した場所ではないことに注意してください。

// anyhow = { version = "1.0.66", features = ["backtrace"] }

let e = anyhow::Error::msg("hoge");
println!("{e:?}");
// `RUST_LIB_BACKTRACE=1 cargo run`で実行するとこの様に出力される
// hoge
// 
// Stack backtrace:
//    0: hoge::main
//              at ./src/main.rs:2:13
//    ごちゃごちゃ
//    6: main
//    7: <unknown>
//    8: __libc_start_main
//    9: _start
//              at /build/glibc/src/glibc/csu/../sysdeps/x86_64/start.S:115

ただし、現時点ではanyhowクレートがバックトレースの取得に使用しているのは標準ライブラリではなくbacktraceクレートです。 実は標準ライブラリも裏ではこのbacktraceクレートを使用してはいるものの、 Cargoからこのクレートを使えば標準ライブラリと二重にbacktraceクレートが使用されてしまうことになるので、 出来ればanyhowクレートも標準ライブラリを使用するようにして欲しいものです。 なお環境変数による切り替えの仕様は標準ライブラリ、anyhowクレートともに共通しているため、動作に違いはありません。

また、JavaにはThrowable#getStackTrace()があるように、多くの言語にはエラー型からバックトレースを取得するAPIがあります。 Rustにも標準のエラーAPIであるErrorトレイトがありますが、バックトレースを取得する関数は現時点ではありません。 以前のNightlyにはbacktrace関数がありましたが、 現在はバックトレースのみならずあらゆる型が取得できるAPIとして策定中です。

RLSの終焉

Rust用LSP実装のRLSがrustのソースツリーから削除されました。 まだ使用している人はrust-analyzerに移行しましょう。

rustupでRLSをインストールしている場合、rlsバイナリは依然として残り続けます。 ただしこれは言語サーバーとしては実質的に機能はせず、起動時にrust-analyzerへの移行を促す文言が表示されるだけの小さなプログラムに置き換わっています。

RLSアップデート後に表示される、rust-analyzerへの移行を促す文言
RLSアップデート後に表示される、rust-analyzerへの移行を促す文言 (RLSを削除するPRより)

安定化されたAPIのドキュメント

安定化されたAPIのドキュメントを独自に訳して紹介します。リストだけ見たい方は安定化されたAPIをご覧ください。

core::ops::Bound::as_ref

原典

impl<T> Bound<T> {
    #[inline]
    #[stable(feature = "bound_as_ref_shared", since = "1.65.0")]
    pub fn as_ref(&self) -> Bound<&T>
    { /* 実装は省略 */ }
}

&Bound<T>からBound<&T>に変換する。

core::pointer::cast_mut

原典

impl<T: ?Sized> *const T {
    #[stable(feature = "ptr_const_cast", since = "1.65.0")]
    #[rustc_const_stable(feature = "ptr_const_cast", since = "1.65.0")]
    pub const fn cast_mut(self) -> *mut T
    { /* 実装は省略 */ }
}

型を変えることなく定数性を変える。

コードをリファクタリングしても暗黙的に型を変えることがない分、asより少し安全である。

core::pointer::cast_const

原典

impl<T: ?Sized> *mut T {
    #[stable(feature = "ptr_const_cast", since = "1.65.0")]
    #[rustc_const_stable(feature = "ptr_const_cast", since = "1.65.0")]
    pub const fn cast_const(self) -> *const T
    { /* 実装は省略 */ }
}

型を変えることなく定数性を変える。

コードをリファクタリングしても暗黙的に型を変えることがない分、asより少し安全である。

厳密には必要ではない(*mut T*const Tに型強制される)ものの、 これは*const Tにおけるcast_mutとの対称性のために提供されていて、 暗黙の型強制の代わりに使うことで文書的意味を持つことがある。

std::backtrace

原典

OSスレッドにおけるバックトレースの取得への対応。

このモジュールには、OSスレッド自体から、実行中のOSスレッドにおけるバックトレースを取得するための必要な対応が含まれる。 Backtrace型は関数Backtrace::captureおよびBacktrace::force_captureによりスタックトレースの取得に対応する。

バックトレースは、エラー(std::error::Errorを実装するような型など)と紐付け エラー発生場所からの因果関係の連鎖を調べるのに非常に便利である。

正確さ

バックトレースはできるだけ正確になるよう努力されているものの、 バックトレースの完全な正確さに保証はない。 命令ポインタ、シンボル名、ファイル名、行番号などの情報全ては間違いである可能性がある。 とは言え正確さはできるだけ高くなるよう追求されており、また、バグは改善場所を示すためいつでも重宝されるだろう。

ほとんどのプラットフォームでは、バックトレースにファイル名と行番号を出力するためには プログラムをデバッグ情報を含めてコンパイルする必要がある。 デバッグ情報を含めない場合、ファイル名と行番号は出力されないだろう。

プラットフォームの対応

libstdがコンパイルできるすべてのプラットフォームがバックトレースの取得に対応しているわけではない。 一部のプラットフォームでは、バックトレースを取得しようとしても特に何も起こらない。 プラットフォームがバックトレースの取得に対応しているかを確認するには、Backtrace::statusの戻り値である列挙型BacktraceStatusを調べられたい。

前述のように、プラットフォームの対応における正確さはできるだけ追求されている。 場合により、実行時にライブラリが使用できない、あるいは何らかの原因によりバックトレースの取得ができないことがある。 プラットフォームでバックトレースが取得できない問題がある場合はぜひ報告されたい。

環境変数

初期状態では、Backtrace::capture関数は実際にはバックトレースを取得しない。 この動作は次に述べるように2つの環境変数によって制御される。

  • RUST_LIB_BACKTRACE:この値が0に設定されている場合、Backtrace::captureがバックトレースを取得することはない。 これ以外の値が設定されている場合はBacktrace::captureは有効化される。

  • RUST_BACKTRACERUST_LIB_BACKTRACEが設定されていない場合、この変数はRUST_LIB_BACKTRACEと同じルールで考慮に入れられる。

  • 上記の環境変数のどちらも設定されていない場合、Backtrace::captureは無効化される。

バックトレースの取得は非常に実行コストが高い操作であるため、 環境変数により、この実行時の性能低下を強制的に無効化する、または一部のプログラムでは選択的に有効化することができる。

Backtrace::force_capture関数を使ってこれらの環境変数を無視することができることに注意されたい。 また、環境変数の状態は最初のバックトレースが生成される際にキャッシュされるため、 実行時にRUST_LIB_BACKTRACERUST_BACKTRACEを変更しても実際にはバックトレースの取得方法が変わらない可能性があることにも注意されたい。

std::backtrace::Backtrace

原典

#[stable(feature = "backtrace", since = "1.65.0")]
#[must_use]
pub struct Backtrace
{ /* フィールドは省略 */ }

取得したOSスレッドのスタックバックトレース。

この型は取得した時点におけるOSスレッドのスタックバックトレースを表す。 設定のため、Backtrace型は場合によっては内部的に空になることがある。 詳細はBacktrace::captureを参照されたい。

std::backtrace::Backtrace::capture

原典

impl Backtrace {
    #[stable(feature = "backtrace", since = "1.65.0")]
    #[inline(never)] // want to make sure there's a frame here to remove
    pub fn capture() -> Backtrace
    { /* 実装は省略 */ }
}

現在のスレッドにおけるスタックバックトレースを取得する。

この関数は、現在実行中ののOSスレッドにおけるスタックバックトレースを取得し、 あとからスタックトレース全体を表示したり文字列に可視化したりできるBacktrace型を返す。

バックトレース用変数RUST_BACKTRACERUST_LIB_BACKTRACEのどちらも設定されていない場合、この関数は何もしない。 どちらかの環境変数が設定・有効化されている場合、この関数は実際にバックトレースを取得する。 バックトレースの取得はメモリ消費が激しく、かつ遅い処理のため、 これらの環境変数によってBacktrace::captureの使用が許可され、この環境変数が設定されている場合のみ速度の低下を招く。

環境変数に関わらず強制的にバックトレースを取得するにはBacktrace::force_captureを使用すること。

std::backtrace::Backtrace::force_capture

原典

impl Backtrace {
    #[stable(feature = "backtrace", since = "1.65.0")]
    #[inline(never)] // want to make sure there's a frame here to remove
    pub fn force_capture() -> Backtrace
    { /* 実装は省略 */ }
}

環境変数の設定に関わらず、完全なバックトレースを強制的に取得する。

この関数の動作はcaptureと同じであるが、 環境変数RUST_BACKTRACERUST_LIB_BACKTRACEの値を無視して常にバックトレースを取得する点が異なる。

一部のプラットフォームではバックトレースの取得は高コストな操作であるため、性能を重視する部分では使用に注意されたい。

std::backtrace::Backtrace::disabled

原典

impl Backtrace {
    #[stable(feature = "backtrace", since = "1.65.0")]
    #[rustc_const_stable(feature = "backtrace", since = "1.65.0")]
    pub const fn disabled() -> Backtrace
    { /* 実装は省略 */ }
}

環境変数の設定に関わらず、無効化されたバックトレースを強制的に取得する。

std::backtrace::Backtrace::status

原典

impl Backtrace {
    #[stable(feature = "backtrace", since = "1.65.0")]
    #[must_use]
    pub fn status(&self) -> BacktraceStatus
    { /* 実装は省略 */ }
}

このバックトレースにおける、リクエストが対応されなかったか、無効化されているか、またはスタックレースが実際に取得されたかの状態を返す。

std::backtrace::BacktraceStatus

原典

#[stable(feature = "backtrace", since = "1.65.0")]
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum BacktraceStatus {
    #[stable(feature = "backtrace", since = "1.65.0")]
    Unsupported,
    #[stable(feature = "backtrace", since = "1.65.0")]
    Disabled,
    #[stable(feature = "backtrace", since = "1.65.0")]
    Captured,
}

バックトレースの現在の状態。取得できたか、もしくは何らかの理由で空となっていることを示す。

バリアント(非網羅的)

この列挙型は非網羅的とされている 非網羅的な列挙型は将来的にバリアントが追加される可能性がある。 したがって、非網羅的な列挙型のバリアントをパターンマッチする際は、将来のバリアントを考慮し、別にワイルドカードのアームを追加する必要がある。

Unsupported

バックトレースの取得に対応していない。 これは現在のプラットフォーム向けに実装されていないためと考えられる。

Disabled

環境変数RUST_LIB_BACKTRACERUST_BACKTRACEによりバックトレースの取得が無効化されている。

Captured

バックトレースは取得され、そのBacktraceは可視化時に適切な情報が出力されるはずである。

std::io::read_to_string

原典

#[stable(feature = "io_read_to_string", since = "1.65.0")]
pub fn read_to_string<R: Read>(mut reader: R) -> Result<String>
{ /* 実装は省略 */ }

readerから全バイトを新しいStringに読み込む。

これはRead::read_to_stringの手頃な関数である。 この関数を使用すると最初に変数を用意する必要が無くなり、エラーでない場合にのみバッファを使うことができるため、 型安全性が向上する (Read::read_to_stringを使う場合は読み込みが成功したかどうかを確認する必要がある。 さもなければバッファは空になるか、あるいは部分的にしか満たされなくなる)。

性能

この関数は使いやすさと型安全性が向上する反面、性能制御が難しくなる。 例えば、String::with_capacityRead::read_to_stringを使うときのようにメモリを事前確保することはできない。 また、読み込み中にエラーが発生した場合のバッファの使い回しもできない。

この関数の性能は十分であり、大抵の場合は使いやすさと型安全性の兼ね合いとしては見合うものである。 とは言え性能をより細かく制御する必要がある場合には、間違いなく直接Read::read_to_stringを使うべきである。

ファイルを読み込む場合など特殊な状況において、この関数は読み込む入力のサイズに基づいてメモリを事前に確保することに注意されたい。 このような場合、手動で事前確保したバッファにてRead::read_to_stringを使用した場合と同じように性能が向上するはずである。

エラー

この関数は出力(String)がResultに内包されるため、エラー処理を強制される。 発生しうるエラーについてはRead::read_to_stringを参照されたい。 何かエラーが発生した場合はErrを受け取るため、バッファが空だったり部分的に満たされたりする心配はない。

サンプル

fn main() -> io::Result<()> {
    let stdin = io::read_to_string(io::stdin())?;
    println!("標準入力:");
    println!("{stdin}");
    Ok(())
}
use std::io;
fn main() -> io::Result<()> {
    let stdin = io::read_to_string(io::stdin())?;
    println!("標準入力:");
    println!("{stdin}");
    Ok(())
}

変更点リスト

言語

コンパイラ

新しいターゲット

ライブラリ

安定化されたAPI

以下のAPIが定数文脈で使えるようになった。

Cargo

互換性メモ

内部の変更

これらの変更がユーザーに直接利益をもたらすわけではないものの、コンパイラ及び周辺ツール内部では重要なパフォーマンス改善をもたらす。

関連リンク

さいごに

次のリリースのRust 1.66は12/16(金)にリリースされる予定です。 1.66では特定の値への最適化をできるだけ阻止するblack_box関数や、パターンでの..=Xが使えるようになる予定です。

ライセンス表記

  • この記事はApache 2/MITのデュアルライセンスで公開されている公式リリースノート及びドキュメントから翻訳・追記をしています
  • 冒頭の画像中にはRust公式サイトで配布されているロゴを使用しており、 このロゴはRust財団によってCC-BYの下で配布されています
  • 冒頭の画像はいらすとやさんの画像を使っています。いつもありがとうございます