本日3/6(金)にリリースされたRust 1.94の変更点を詳しく紹介します。 もしこの記事が参考になれば記事末尾から活動を支援頂けると嬉しいです。
この記事は原文の理解や和訳のために一部生成AIを使用していますが、すべて筆者の考えに基づく文章で構成しており、 漫然と生成AIを使用しているものではありません。
- ピックアップ
- 安定化されたAPIのドキュメント
- <[T]>::array_windows
- <[T]>::element_offset
- LazyCell::get
- LazyCell::get_mut
- LazyCell::force_mut
- LazyLock::get
- LazyLock::get_mut
- LazyLock::force_mut
- std::iter::Peekable::next_if_map
- std::iter::Peekable::next_if_map_mut
- f32::consts::EULER_GAMMA
- f64::consts::EULER_GAMMA
- f32::consts::GOLDEN_RATIO
- f64::consts::GOLDEN_RATIO
- 変更点リスト
- 関連リンク
- さいごに
- ライセンス表記
ピックアップ
個人的に注目する変更点を「ピックアップ」としてまとめました。 全ての変更点を網羅したリストは変更点リストをご覧ください。
スライスを固定長の部分スライスでずらしながらループできるようになった
配列を1つずつずらしながらループしたい場合、これまでは<[T]>::windowsを使えました。
ただwindowsは要素数を実行時引数で指定することから、
要素数が固定であっても可変長スライスとしてしか扱えず不要な分岐が発生していました。
Rust 1.94からは<[T]>::array_windowsが使えるようになり、スライスのウィンドウを固定長スライスとして扱えるようになりました。
fn main() { let slice: &[i32] = &[0, 1, 2]; // windowsでは&[i32]としてしか扱えないため無駄な分岐が必要 for window in slice.windows(2) { let [a, b] = window else { unreachable!() }; println!("{a}, {b}"); } // array_windowsでは&[i32; 2]として扱える for [a, b] in slice.array_windows() { println!("{a}, {b}"); } }
なお重ならない部分スライスを扱う場合、実行時引数では<[T]>::chunksを使うので
固定長ではarray_chunksを探してしまいますが、
この機能は<[T]>::as_chunksとしてRust 1.88から実装されています。
これはchunksがイテレータを返すのに対しas_chunksは&[[T; N]](を含むタプル)を返すためです。
CargoがTOML 1.1に対応した
CargoがTOML 1.1に対応しました。 主な変更点としてはインラインテーブルが複数行で書けるようになったことが挙げられます。
[dependencies] # これまでは1行で書く必要があった tokio = { version = "1.0", features = [ "rt-multi-thread", # 複数スレッドランタイム "net", # ネットワーク "time", # 時間関連 "macros", # マクロ "sync", # 同期プリミティブ "fs", # ファイルシステム ], # ↑末尾のカンマもOK }
Cargoの設定ファイルから別のtomlを取り込めるようになった
Cargoの設定ファイルで別のtomlファイルを取り込めるようになりました。
includeに配列で、そのファイル基準のパスを記述します。
配列の後ろの方が優先度が高く、かつそのファイル自体の記述が最も優先されます。
そのためincludeはファイルの一番最初に記述することが推奨されます。
なおCargo.tomlではなくCargo自体の設定ファイル(~/.cargo/config.tomlなど)の機能であることに注意してください。
例えばyadmやchezmoiなどのdotfile管理ツールを使っている場合、共通の設定とローカルで上書きする設定を分けて管理できるようになります。
# ~/.cargo/config.toml include = [ # 共通する設定。優先度低 # ファイルが存在しない場合はエラーになる "config.common.toml", # ローカルで上書きする設定。優先度高 # optional = trueを付けるとファイルが存在しなくてもエラーにならない { path = "config.local.toml", optional = true }, ] # それ以外の設定。優先度最高
# ~/.cargo/config.common.toml # 開発用にmoldを使う設定 [target.'cfg(target_os = "linux")'] rustflags = ["-C", "link-arg=-fuse-ld=mold"]
安定化されたAPIのドキュメント
安定化されたAPIのドキュメントを独自に訳して紹介します。リストだけ見たい方は安定化されたAPIをご覧ください。
<[T]>::array_windows
impl<T> [T] { #[stable(feature = "array_windows", since = "1.94.0")] #[rustc_const_unstable(feature = "const_slice_make_iter", issue = "137737")] #[inline] #[track_caller] pub const fn array_windows<const N: usize>(&self) -> ArrayWindows<'_, T, N> { /* 実装は省略 */ } }
スライスの先頭から開始し、N要素スライスで重なり合うウィンドウのイテレータを返す。
これはwindowsの定数ジェネリクス版である。
Nがスライスの長さより大きい場合、ウィンドウは1つも返されない。
パニック
Nが0の場合にパニックする。
この検査は実行時の値ではなく定数ジェネリクス引数に対して行われる。 すなわち単相化された関数について考えると、常にパニックするか、または一切パニックしないかのどちらかである。
サンプル
let slice = [0, 1, 2, 3]; let mut iter = slice.array_windows(); assert_eq!(iter.next().unwrap(), &[0, 1]); assert_eq!(iter.next().unwrap(), &[1, 2]); assert_eq!(iter.next().unwrap(), &[2, 3]); assert!(iter.next().is_none());
<[T]>::element_offset
impl<T> [T] { #[must_use] #[stable(feature = "element_offset", since = "1.94.0")] pub fn element_offset(&self, element: &T) -> Option<usize> { /* 実装は省略 */ } }
要素への参照が指している添字を返す。
elementがスライス内の要素先頭を指していない場合はNoneを返す。
このメソッドはslice::splitのようなスライスイテレータを拡張するのに有用である。
このメソッドはポインタ演算を使うため、要素の比較は行わないことに注意。
要素を比較して添字を求めたい場合、代わりに.iter().position()を使用すること。
パニック
Tが大きさのない型場合はパニックする。
サンプル
基本的な使い方
let nums: &[u32] = &[1, 7, 1, 1]; let num = &nums[2]; assert_eq!(num, &1); assert_eq!(nums.element_offset(num), Some(2));
アライメントされていない要素ではNoneを返す
let arr: &[[u32; 2]] = &[[0, 1], [2, 3]]; let flat_arr: &[u32] = arr.as_flattened(); let ok_elm: &[u32; 2] = flat_arr[0..2].try_into().unwrap(); let weird_elm: &[u32; 2] = flat_arr[1..3].try_into().unwrap(); assert_eq!(ok_elm, &[0, 1]); assert_eq!(weird_elm, &[1, 2]); assert_eq!(arr.element_offset(ok_elm), Some(0)); // 0の要素を指す assert_eq!(arr.element_offset(weird_elm), None); // 0と1の要素間を指す
LazyCell::get
impl<T, F> LazyCell<T, F> { #[inline] #[stable(feature = "lazy_get", since = "1.94.0")] pub fn get(this: &LazyCell<T, F>) -> Option<&T> { /* 実装は省略 */ } }
初期化済みなら値への参照を返し、それ以外(未初期化または中毒状態)ならNoneを返す。
サンプル
use std::cell::LazyCell; let lazy = LazyCell::new(|| 92); assert_eq!(LazyCell::get(&lazy), None); let _ = LazyCell::force(&lazy); assert_eq!(LazyCell::get(&lazy), Some(&92));
LazyCell::get_mut
impl<T, F> LazyCell<T, F> { #[inline] #[stable(feature = "lazy_get", since = "1.94.0")] pub fn get_mut(this: &mut LazyCell<T, F>) -> Option<&mut T> { /* 実装は省略 */ } }
初期化済みなら値への可変参照を返し、それ以外(未初期化または中毒状態)ならNoneを返す。
サンプル
use std::cell::LazyCell; let mut lazy = LazyCell::new(|| 92); assert_eq!(LazyCell::get_mut(&mut lazy), None); let _ = LazyCell::force(&lazy); *LazyCell::get_mut(&mut lazy).unwrap() = 44; assert_eq!(*lazy, 44);
LazyCell::force_mut
impl<T, F: FnOnce() -> T> LazyCell<T, F> { #[inline] #[stable(feature = "lazy_get", since = "1.94.0")] pub fn force_mut(this: &mut LazyCell<T, F>) -> &mut T { /* 実装は省略 */ } }
この遅延値の評価を強制し、結果への可変参照を返す。
パニック
初期化クロージャ(new()メソッドに渡したもの)がパニックすると、
そのパニックは呼び出し元へ伝播しセルは中毒状態になる。
その後、セルへの全アクセス(force()または逆参照経由)はパニックする。
サンプル
use std::cell::LazyCell; let mut lazy = LazyCell::new(|| 92); let p = LazyCell::force_mut(&mut lazy); assert_eq!(*p, 92); *p = 44; assert_eq!(*lazy, 44);
LazyLock::get
impl<T, F> LazyLock<T, F> { #[inline] #[stable(feature = "lazy_get", since = "1.94.0")] #[rustc_should_not_be_called_on_const_items] pub fn get(this: &LazyLock<T, F>) -> Option<&T> { /* 実装は省略 */ } }
初期化済みなら値への参照を返し、それ以外(未初期化または中毒状態)ならNoneを返す。
サンプル
use std::sync::LazyLock; let lazy = LazyLock::new(|| 92); assert_eq!(LazyLock::get(&lazy), None); let _ = LazyLock::force(&lazy); assert_eq!(LazyLock::get(&lazy), Some(&92));
LazyLock::get_mut
impl<T, F> LazyLock<T, F> { #[inline] #[stable(feature = "lazy_get", since = "1.94.0")] pub fn get_mut(this: &mut LazyLock<T, F>) -> Option<&mut T> { /* 実装は省略 */ } }
初期化済みなら値への可変参照を返し、それ以外(未初期化または中毒状態)ならNoneを返す。
サンプル
use std::sync::LazyLock; let mut lazy = LazyLock::new(|| 92); assert_eq!(LazyLock::get_mut(&mut lazy), None); let _ = LazyLock::force(&lazy); *LazyLock::get_mut(&mut lazy).unwrap() = 44; assert_eq!(*lazy, 44);
LazyLock::force_mut
impl<T, F: FnOnce() -> T> LazyLock<T, F> { #[inline] #[stable(feature = "lazy_get", since = "1.94.0")] pub fn force_mut(this: &mut LazyLock<T, F>) -> &mut T { /* 実装は省略 */ } }
この遅延値の評価を強制し、結果への可変参照を返す。
パニック
初期化クロージャ(new()メソッドに渡したもの)がパニックすると、
そのパニックは呼び出し元へ伝播しロックは中毒状態になる。
その後、ロックへの全アクセス(force()または逆参照経由)はパニックする。
サンプル
use std::sync::LazyLock; let mut lazy = LazyLock::new(|| 92); let p = LazyLock::force_mut(&mut lazy); assert_eq!(*p, 92); *p = 44; assert_eq!(*lazy, 44);
std::iter::Peekable::next_if_map
impl<I: Iterator> Peekable<I> { #[stable(feature = "peekable_next_if_map", since = "1.94.0")] pub fn next_if_map<R>(&mut self, f: impl FnOnce(I::Item) -> Result<R, I::Item>) -> Option<R> { /* 実装は省略 */ } }
このイテレータの次の値を消費して関数fを適用し、クロージャがOkを返した場合はその結果を返す。
クロージャがErrを返した場合は、次の反復のために値を戻す。
Err側の中身は通常クロージャに渡した元の値であるが、それは必須ではない。
別の値を返した場合は次のpeek()またはnext()呼び出しでその新しい値が得られる。
これはpeek_mut()の出力を書き換えるのと似ている。
クロージャがパニックした場合、次の値は必ず消費されて破棄される。
クロージャが戻し用のErr値を返していないため、パニックを捕捉したとしても同様である。
next_if_map_mutも参照。
サンプル
let mut iter = "125 GOTO 10".chars().peekable(); let mut line_num = 0_u32; while let Some(digit) = iter.next_if_map(|c| c.to_digit(10).ok_or(c)) { line_num = line_num * 10 + digit; } assert_eq!(line_num, 125); assert_eq!(iter.collect::<String>(), " GOTO 10");
#[derive(Debug, PartialEq, Eq)] enum Node { Comment(String), Red(String), Green(String), Blue(String), } /// 連続するすべての`Comment`ノードを1つのノードに結合する fn combine_comments(nodes: Vec<Node>) -> Vec<Node> { let mut result = Vec::with_capacity(nodes.len()); let mut iter = nodes.into_iter().peekable(); let mut comment_text = None::<String>; loop { // 通常`.next_if_map()`のクロージャは入力でマッチし、 // 目的のパターンを`Ok`に抽出し、残りを`Err`に入れる。 while let Some(text) = iter.next_if_map(|node| match node { Node::Comment(text) => Ok(text), other => Err(other), }) { comment_text.get_or_insert_default().push_str(&text); } if let Some(text) = comment_text.take() { result.push(Node::Comment(text)); } if let Some(node) = iter.next() { result.push(node); } else { break; } } result }
#[derive(Debug, PartialEq, Eq)]
enum Node {
Comment(String),
Red(String),
Green(String),
Blue(String),
}
/// 連続するすべての`Comment`ノードを1つのノードに結合する
fn combine_comments(nodes: Vec<Node>) -> Vec<Node> {
let mut result = Vec::with_capacity(nodes.len());
let mut iter = nodes.into_iter().peekable();
let mut comment_text = None::<String>;
loop {
// 通常`.next_if_map()`のクロージャは入力でマッチし、
// 目的のパターンを`Ok`に抽出し、残りを`Err`に入れる。
while let Some(text) = iter.next_if_map(|node| match node {
Node::Comment(text) => Ok(text),
other => Err(other),
}) {
comment_text.get_or_insert_default().push_str(&text);
}
if let Some(text) = comment_text.take() {
result.push(Node::Comment(text));
}
if let Some(node) = iter.next() {
result.push(node);
} else {
break;
}
}
result
}
assert_eq!( // 文書が煩雑にならないようテストを非表示
combine_comments(vec![
Node::Comment("The".to_owned()),
Node::Comment("Quick".to_owned()),
Node::Comment("Brown".to_owned()),
Node::Red("Fox".to_owned()),
Node::Green("Jumped".to_owned()),
Node::Comment("Over".to_owned()),
Node::Blue("The".to_owned()),
Node::Comment("Lazy".to_owned()),
Node::Comment("Dog".to_owned()),
]),
vec![
Node::Comment("TheQuickBrown".to_owned()),
Node::Red("Fox".to_owned()),
Node::Green("Jumped".to_owned()),
Node::Comment("Over".to_owned()),
Node::Blue("The".to_owned()),
Node::Comment("LazyDog".to_owned()),
],
)
std::iter::Peekable::next_if_map_mut
impl<I: Iterator> Peekable<I> { #[stable(feature = "peekable_next_if_map", since = "1.94.0")] pub fn next_if_map_mut<R>(&mut self, f: impl FnOnce(&mut I::Item) -> Option<R>) -> Option<R> { /* 実装は省略 */ } }
イテレータの次の値への可変参照を渡して関数fを適用し、
fがSomeを返した場合はその結果を返してイテレータを前進させる。
fがNoneを返した場合は、次の値は次回反復のために保持される。
fがパニックした場合は、fがSomeを返したときと同様にその要素は消費される。
その値は破棄される。
これはnext_if_mapと似ているが、要素の所有権をfへ渡さない点が異なる。
fがどのみち要素を複製する場合には、こちらが望ましいことがある。
サンプル
文字イテレータの先頭にある10進数を読み取る例。
let mut iter = "125 GOTO 10".chars().peekable(); let mut line_num = 0_u32; while let Some(digit) = iter.next_if_map_mut(|c| c.to_digit(10)) { line_num = line_num * 10 + digit; } assert_eq!(line_num, 125); assert_eq!(iter.collect::<String>(), " GOTO 10");
f32::consts::EULER_GAMMA
#[stable(feature = "rust1", since = "1.0.0")] #[rustc_diagnostic_item = "f32_consts_mod"] pub mod consts { #[stable(feature = "euler_gamma_golden_ratio", since = "1.94.0")] pub const EULER_GAMMA: f32 = 0.577215664901532860606512090082402431_f32; }
オイラー・マスケローニ定数(γ)
f64::consts::EULER_GAMMA
#[stable(feature = "rust1", since = "1.0.0")] #[rustc_diagnostic_item = "f64_consts_mod"] pub mod consts { #[stable(feature = "euler_gamma_golden_ratio", since = "1.94.0")] pub const EULER_GAMMA: f64 = 0.577215664901532860606512090082402431_f64; }
オイラー・マスケローニ定数(γ)
f32::consts::GOLDEN_RATIO
#[stable(feature = "rust1", since = "1.0.0")] #[rustc_diagnostic_item = "f32_consts_mod"] pub mod consts { #[stable(feature = "euler_gamma_golden_ratio", since = "1.94.0")] pub const GOLDEN_RATIO: f32 = 1.618033988749894848204586834365638118_f32; }
黄金比(φ)
f64::consts::GOLDEN_RATIO
#[stable(feature = "rust1", since = "1.0.0")] #[rustc_diagnostic_item = "f64_consts_mod"] pub mod consts { #[stable(feature = "euler_gamma_golden_ratio", since = "1.94.0")] pub const GOLDEN_RATIO: f64 = 1.618033988749894848204586834365638118_f64; }
黄金比(φ)
変更点リスト
言語
- 対応するトレイトおよびトレイト項目の
dead_codeリントレベルを、実装と実装項目が継承するようになった - RVA22U64・RVA23U64プロファイルの大部分を含む、追加の29個のRISC-Vターゲット機能を安定化
const _宣言の可視性に対して、既定警告のリントunused_visibilitiesを追加- Unicode 17へ更新
- クロージャに対する不正確なライフタイムエラーを回避
プラットフォーム対応
Rustのティア付けされたプラットフォーム対応の詳細はPlatform Supportのページ(※訳注:英語)を参照
ライブラリ
安定化されたAPI
<[T]>::array_windows<[T]>::element_offsetLazyCell::getLazyCell::get_mutLazyCell::force_mutLazyLock::getLazyLock::get_mutLazyLock::force_mutimpl TryFrom<char> for usizestd::iter::Peekable::next_if_mapstd::iter::Peekable::next_if_map_mut- x86の
avx512fp16組み込み関数 (安定化前のf16型に直接依存するものを除く) - AArch64 NEONのfp16組み込み関数
(安定化前の
f16型に直接依存するものを除く) f32::consts::EULER_GAMMAf64::consts::EULER_GAMMAf32::consts::GOLDEN_RATIOf64::consts::GOLDEN_RATIO
以下のAPIが定数文脈で使えるようになった。
Cargo
- include設定キーを安定化。最上位のinclude設定キーで追加の設定ファイルを読み込めるため、プロジェクトや環境をまたいだCargo設定の整理・共有・管理がしやすくなる。文書 #16284
- レジストリ添字の
pubtimeフィールドを安定化。クレートのバージョンが公開された時刻を記録することで、将来的には時刻による依存解決できるようになる。なおcrates.ioでは新しいバージョンが公開された際に既存パッケージへ段階的に値を補完するため、まだpubtimeを持たないクレートもある。#16369 #16372 - Cargoがマニフェストと設定ファイルでTOML v1.1を解釈するようになった。これらの機能を
Cargo.tomlで使うと開発時MSRVは上がるが、公開されるマニフェストは旧来のパーサとも互換性を保つ。#16415 CARGO_BIN_EXE_<crate>を実行時に利用可能にした
互換性メモ
dyn型におけるライフタイム境界の自由なキャストを禁止- パターン周辺でのクロージャ捕捉の挙動を一貫的かつ正しくした
パターン照合が精密なクロージャ捕捉へ与える影響の細部が変更された。場合によっては、以前は変数全体をムーブ捕捉していた非
moveクロージャが、変数の一部だけをムーブし、残りを借用で捕捉するようになる。これにより以前は出なかった借用検査エラーが出たり、Dropの実行位置が変わったりする。 - 標準ライブラリのマクロを、注入された
#[macro_use]ではなくprelude経由で取り込むようになった これにより同名のマクロをglobインポートしているとエラーが発生する。 例えばクレート独自のmatchesマクロを定義してそれをglobインポートすると、matchesが独自のものか標準ライブラリのものか曖昧になるため、曖昧性を排除するには名前の明示インポートが必要である。 例外としてcore::panicとstd::panicの取り込みが曖昧な場合は、 新しい警告(ambiguous_panic_imports)が発生する。 これはstdクレートをglobインポートする#![no_std]コードで新しい警告 (ambiguous_panic_imports)を発生させることがある。 その場合、core::panic!とstd::panic!の両方がスコープに入り、どちらを使うか曖昧になる。 - 式文脈の
include!(…)でshebangを除去しないようになった 先頭がshebangのファイルを取り込んでいた場合、以前は動いていたinclude!がコンパイル不能になることがある。 - 曖昧なglob再エクスポートがクレート間でも可視化されるようになった これにより、これらのエクスポートに関するローカル・クレート間のエラー挙動が統一され、新たな曖昧性エラーが発生する可能性がある。
- 適格性検査の前にwhere節を正規化しないようにした
- 本体を持たないトレイトメソッド上のコード生成属性に、将来的互換性警告を導入 これらの属性は現在この位置では効果を持たない。
- Windowsで
std::time::SystemTime::checked_sub_durationがWindowsエポック(1601年1月1日)以前の時刻に対してNoneを返すようになった 'aのようなライフタイム識別子がNFC正規化されるようになった。- クロスコンパイラ間の一貫性向上のためファイル名処理を見直し
コンパイラが出力するパスは、元の相対性と
--remap-path-prefixを常に尊重するようになった。 この変更の副作用として、Cargo内のローカルクレート(パス依存とワークスペースメンバー)のパスは、下流クレートの診断出力では絶対ではなく相対で表示されるようになった。
内部の変更
これらの変更がユーザーに直接利益をもたらすわけではないものの、コンパイラ及び周辺ツール内部では重要なパフォーマンス改善をもたらす。
- エラー出力に
annotate-snippetsを採用 rustcのエラーメッセージ出力は概ね保たれるはずである。
関連リンク
さいごに
次のリリースのRust 1.95は4/16(金)にリリースされる予定です。
Rust 1.95ではパターンマッチでif-letが使えるようになったり、
パターンマッチで表明ができる(debug_)assert_matches!が使えるようになったり、
#[cfg]の値でパターンマッチっぽく分岐できるcfg_select!が使えるようになったりする予定です。