Rust 聖書分析の研究 - Rust Learn-16 (高度な特性、マクロ)
高度な特性
関連付けの種類
type キーワードを使用して関連付けられた型を宣言できます.関連付けられた型の機能は、表示型を単純化して非表示にすることです (私の意見では)
- 単純化: 非常に長い型が常に必要な場合、開発者は繰り返しの書き込みにエネルギーを費やす必要があり、変更がある場合は複数の場所で変更する必要があります
- 非表示: 外部の発信者から隠されています。外部の発信者は、すぐに使用できる限り、それが何を指しているのかを知る必要はありません。
trait test {
type Res = Result<i32, Box<&'static str>>;
fn test_res() -> Res{
//...
}
}
ジェネリックではなくタイプを使用する理由
ジェネリックを使うと、実装を使うたびにアノテーションの型を表示する必要がありますが、型を変更する必要がなく、複数の場所で使用されるシーンを対象とする場合、時間と労力がかかることは間違いありません。つまり、タイプは一般的な使用と引き換えに柔軟性の一部を犠牲にします
演算子の過負荷 (重要度レベルが低い)
Rust では、カスタム オペレータの作成や任意のオペレータのオーバーロードは許可されていませんが、std::ops にリストされているオペレータと対応するトレイトは、オペレータ関連のトレイトを実装することでオーバーロードできるため、 などをオーバーロードできます。以下は公式の
例+,/,-,*
です
。
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point {
x: 1, y: 0 } + Point {
x: 2, y: 3 },
Point {
x: 3, y: 3 }
);
}
重複メソッドの明確化
複数のトレイトを実装するとき、同じメソッド名を持つ複数のトレイトに遭遇すると、同じ名前にあいまいさが生じます. この時点で、最新の実装は前のものをオーバーライドします. あいまいさを排除するために、trait::fn(&type)
呼び出しを宣言する
struct a {
}
trait b {
fn get(&self) {
}
}
trait c {
fn get(&self) {
}
}
impl b for a {
fn get(&self) {
todo!()
}
}
impl c for a {
fn get(&self) {
todo!()
}
}
fn main() {
let a_struct = a {
};
b::get(&a_struct);
c::get(&a_struct);
}
入力しない
Rust には ! と呼ばれる特別な型があります。
fn no_feedback()->!{
//...
}
continue の値は !
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
リターンクロージャー
関数の戻り値はクロージャでもいい. これに制限はない. 具体的には, 単純に書き方を理解する.
fn test()->Box<dyn Fn()>{
//...
}
Box を返すことにより、戻り値をヒープに書き込みます
。
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
大きい
基本的に、マクロは他のコードを書くためのコードを書く方法、いわゆるメタプログラミングです。さまざまなトレイトの実装を生成する派生属性については、付録 C で説明します。また、本書では println! マクロと vec! マクロも使用しています。これらのマクロはすべて展開され、手動で記述できるよりも多くのコードを生成します。
メタプログラミングは、記述および保守するコードの量を削減するのに非常に役立ち、関数が果たす役割も果たします。ただし、マクロには、関数にはない追加機能がいくつかあります。
関数シグネチャでは、関数パラメーターの数と型を宣言する必要があります。対照的に、マクロは異なる数の引数を受け取ることができます: 引数を 1 つ指定して println!("hello") を呼び出すか、引数を 2 つ指定して println!("hello {}", name) を呼び出します。さらに、コンパイラによってコードが変換される前に、マクロを展開することができます。たとえば、マクロは特定の型の特性を実装できます。ただし、関数は実行時に呼び出され、特性はコンパイル時に実装する必要があるため、関数はできません。
関数の実装ほど良くないマクロの実装の 1 つの側面は、Rust コードを生成する Rust コードを作成しているため、マクロ定義が関数定義よりも複雑であることです。この間接的なため、一般に、マクロ定義は関数定義よりも読み取り、理解、および維持が困難です。
マクロと関数の最後の重要な違いは、マクロはファイル内で呼び出す前に定義するかスコープに入れる必要があるのに対し、関数はどこからでも定義して呼び出すことができるということです。
— https://kaisery.github.io/trpl-zh-cn/ch19-06-macros.html
カスタム マクロ (宣言マクロ)
次に、マクロを直接カスタマイズし、ナンセンスな話をやめて、直接作業を開始します (これを学ぶ理由は? これを使用して自分で言語を作成することもできるからです)。
マクロの仕組み
Rust コンパイル プロセス
新しい空のマクロを作成
空のマクロを作成する方法は非常に簡単で、直接使用してmacro_rules!
宣言し、内部の形状はパターン マッチングの推論に似ています (実際にはまったくありません)。
macro_rules! test {
() => {
};
}
マクロセレクター
- item: 関数、構造、モジュールなどのアイテム。
- ブロック: コードブロック
- stmt: ステートメント
- パット:パターン
- expr: 式
- ty: タイプ
- ident: 識別子
- パス: パス、例: foo、::std::mem::replace、transmute::<_、int>、…
- meta: #[…] や #![rust macro…] 属性などのメタ情報エントリ
- tt: エントリ ツリー
タームツリーとは
tt エントリ ツリーは、Rust コンパイラによって使用されるデータ構造を参照します。これは通常、マクロ (マクロ) およびコード生成 (コード生成) を処理するために使用されます。
tt は「Token Tree」のことで、一連の「トークン」から構成されるツリー構造です。「トークン」は、キーワード、識別子、演算子、括弧など、プログラミング言語の最も基本的な文法単位です。「Token Tree」は、これらの「Token」を一定の階層構造に並べた木です。
Rust 言語では、マクロは通常 tt エントリ ツリーを入力として使用します。これにより、マクロ定義がより柔軟で強力になります。マクロは、tt エントリ ツリーを再帰、トラバース、および変換することにより、メタプログラミングの効果を実現するコードを生成できます。
マクロに加えて、Rust コンパイラは tt エントリ ツリーも使用して、抽象構文ツリー (AST) の構築やコードの中間表現 (IR) の生成などのコード生成作業を処理します。
各種入力パラメータを設定するマクロセレクター
適切なマクロ セレクターを選択することで、マクロで受け入れられるパラメーターに対応できます。
ログ マクロを実装する
use std::time::{
Instant, SystemTime, UNIX_EPOCH};
macro_rules! log {
($log_name:tt)=>{
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
println!("=======================start-{}=========================",$log_name);
println!("----------------createTime:{:?}",timestamp);
println!("----------------title:{}",$log_name);
println!("========================end-{}========================",$log_name);
};
}
fn main() {
log!("zhangsan");
}
繰り返しパターン マッチングを実行する
println などの複数の入力パラメーターがある場合は、これを使用する必要があります。このマクロでは、出力する必要がある複数のコンテンツを渡すことができます. それぞれに名前を付ける必要がある場合、統一された単純化されたマクロを作成する必要はありません。
繰り返しパターン マッチングの構文:
($($x:expr),*)=>{
}
use std::time::{
Instant, SystemTime, UNIX_EPOCH};
macro_rules! eq_judge {
($($left:expr => $right:expr),*)=>{
{
$(if $left == $right{
println!("true")
})*
}}
}
fn main() {
eq_judge!(
"hello"=>"hi",
"no"=>"no"
);
}
カスタム派生マクロ (手続き型マクロ)
上記との違いは、derive マクロによってマークされた位置が一般に構造体と列挙型上にあることです
。たとえば、次のようになります。
#[derive(Debug)]
struct a{
}
プロジェクト構造を構築します(必ず従わないと間違っています)
以下は公式のケースです. 一度やった後に, 順番を書き直し, 間違いを強調しました. 必ず順番に従ってください. エラーが発生した場合は, ここに書いた間違いを確認してください.
ワークスペースを設定する
最初に何気なくプロジェクトを作成し、次に toml ファイルを変更します
- hello_macro: 実装する必要がある特性を宣言します
- hello_macro_derive: 特定の分析、変換、処理ロジック
- pancakes: メイン実行パッケージ
[workspace]
members=[
"hello_macro","hello_macro_derive","pancakes"
]
lib と main を作成する
cargo new hello_macro --lib
cargo new hello_macro_derive --lib
cargo new pancakes
構造は次のとおりです。
hello_macro
lib.rs
実装する必要がある特性を記述し、pub を使用してそれらを公開します
pub trait HelloMacro {
fn hello_macro();
}
hello_macro_derive
依存関係を追加してアクティブ化するproc-macro
syncrate は、文字列内の Rust コードを解析して操作可能なデータ構造にします。quote は syn によって解析されたデータ構造を Rust コードに変換します。これらのクレートにより、対処しなければならない Rust コードの解析が容易になります。Rust 用のパーサー全体を作成することは簡単ではありません。
proc-macro は、この cratq が proc-macro であることを示します. この構成を追加した後、この crate の特性はいくつか変更されます. たとえば、この crate は内部で定義されたプロセス マクロのみをエクスポートしますが、他の内部で定義されたコンテンツはエクスポートしません. .
cargo add syn
cargo add quote
[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro=true
[dependencies]
quote = "1.0.26"
syn = "2.0.15"
lib.rs
#[proc_macro_derive(HelloMacro)]
ロゴが構造体であり、列挙型でマークされている限り、#[derive(HelloMacro)]
HelloMacro トレイトは自動的に実装されます. 具体的な実装ロジックは実際にはimpl_hello_macro
関数にあります.
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
注意してください(よくお読みください。公式ウェブサイトには非常に明確に記載されています。この場所を理解する必要があります)
ユーザーが型に #[derive(HelloMacro)] を指定すると、hello_macro_derive 関数が呼び出されます。hello_macro_derive 関数に proc_macro_derive とその割り当てられた名前 HelloMacro で注釈を付けたので、割り当てられた名前 HelloMacro が特性名であり、ほとんどの手続き型マクロが従う規則です。
この関数は、最初に TokenStream からの入力を、解釈して操作できるデータ構造に変換します。ここで syn が役に立ちます。syn の parse 関数は TokenStream を受け取り、解析された Rust コードを表す DeriveInput 構造を返します。以下は、文字列 struct Pancakes; から解析された DeriveInput 構造の関連部分を示しています。
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
含まれる新しい Rust コードをビルドするために使用される impl_hello_macro 関数を定義します。ただしその前に、その出力も TokenStream であることに注意してください。返された TokenStream は、クレート ユーザーによって記述されたコードに追加されるため、ユーザーがクレートをコンパイルすると、変更された TokenStream を通じて提供される追加機能を取得できます。
syn::parse 関数の呼び出しが失敗したときに、unwrap を使用して hello_macro_derive 関数をパニックに陥れます。proc_macro_derive 関数は、手続き型マクロ API に準拠するために Result ではなく TokenStream を返さなければならないため、手続き型マクロにはエラー時のパニックが必要です。ここで unwrap を使用するという選択は、例を単純化します; 製品コードでは、panic! または expect を使用して、何が問題なのかについてより明確なエラー メッセージを提供する必要があります。
パンケーキ
依存関係を追加する
ここでは自分で書いたライブラリに依存する必要があるため、パスで指定する必要があります
[package]
name = "pancakes"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hello_macro = {
path = "../hello_macro" }
hello_macro_derive = {
path = "../hello_macro_derive" }
main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
間違い
library が見つからないmarco_t
、ファイル名を lib.path に変更するsrc/lib.rs
、または lib.path を指定できない (単一のプロジェクト パッケージでビルドできない理由)
1 つのパッケージのみでビルドしている場合、[lib] proc-macro = true
追加すると次のエラーが発生します。
Caused by:
can't find library `marco_t`, rename file to `src/lib.rs` or specify lib.path
これは、メインの実行パッケージであるため、現在のパッケージを lib として使用できないことを意味します。
原則: プロセス マクロは、クレートをコンパイルする前にクレート コードを処理するプログラムであり、このプログラムもコンパイルしてコンパイルする必要があると考えてください。実行されました。プロシージャ マクロを定義し、プロシージャ マクロを使用するコードをクレートに記述すると、デッドロックに陥ります。
コンパイルするコードは、最初にプロシージャ マクロを実行して展開する必要があります。そうしないと、コードが不完全になり、クレートがクレートはコンパイルできません.
クレート内の手続き型マクロコードは実行できません. 手続き型マクロで修飾されたコードは展開できません.
それを定義する同じクレートから手続き型マクロを使用することはできません
これを直接削除すると、このエラーが表示されます。つまり、lib でプロセス マクロをビルドする必要があります。
カスタム クラス属性マクロ (個人的に最も重要だと考えている)
クラス属性マクロは、派生属性がコードを生成し、それら (クラス属性マクロ) が新しい属性を作成できることを除いて、カスタム派生マクロに似ています。また、それらはより柔軟です。deriver は構造体と列挙型でのみ使用できます。プロパティは、関数などの他のアイテムでも使用できます。
あらゆるフレームワークに共通!
簡単な例
パッケージ構造
カスタム プロセス マクロと同様に、
解析および処理ロジックを lib の下に配置する必要があります。
[workspace]
members=[
"json_marco","json_test"
]
json_マルコ
依存関係を追加する
[package]
name = "json_marco"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.56"
quote = "1.0.26"
syn = {
version = "2.0.15", features = ["full"] }
ライブラリを書く
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, DeriveInput};
#[proc_macro_attribute]
pub fn my_macro(attr:TokenStream,item:TokenStream)->TokenStream{
println!("test");
println!("{:#?}",attr);
println!("{:#?}",item);
item
}
とてもシンプルで、出力するだけです
json_test
toml マッピングのインポート
[package]
name = "json_test"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
json_marco={
path= "../json_marco"}
main.rs
use json_marco::my_macro;
#[my_macro("test111")]
fn test(a: i32) {
println!("{}", a);
}
fn main() {
test(5);
}
いくつかの例
ベース
ライブラリ
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, DeriveInput};
#[proc_macro_attribute]
pub fn my_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
// 解析输入的类型
let input = parse_macro_input!(item as DeriveInput);
// 获取类型名
let name = input.ident;
// 构建实现代码
let expanded = quote! {
impl #name {
fn my_function(&self) {
println!("This is my custom function!");
}
}
};
// 将生成的代码转换回 TokenStream 以供返回
TokenStream::from(expanded)
}
主要
#[my_macro]
struct MyStruct {
field1: u32,
field2: String,
}
fn main() {
let my_instance = MyStruct {
field1: 42, field2: "hello".to_string() };
my_instance.my_function();
}
flaky_test
ライブラリ
extern crate proc_macro;
extern crate syn;
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_attribute]
pub fn flaky_test(_attr: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = syn::parse_macro_input!(input as syn::ItemFn);
let name = input_fn.sig.ident.clone();
TokenStream::from(quote! {
#[test]
fn #name() {
#input_fn
for i in 0..3 {
println!("flaky_test retry {}", i);
let r = std::panic::catch_unwind(|| {
#name();
});
if r.is_ok() {
return;
}
if i == 2 {
std::panic::resume_unwind(r.unwrap_err());
}
}
}
})
}
主要
#[flaky_test::flaky_test]
fn my_test() {
assert_eq!(1, 2);
}
json_parse
lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, Data, DeriveInput, Fields};
#[proc_macro_attribute]
pub fn serde_json(_args: TokenStream, input: TokenStream) -> TokenStream {
// 将输入解析为 DeriveInput 类型,这是所有 Rust 结构体和枚举的通用 AST
let input = parse_macro_input!(input as DeriveInput);
// 检查这是否是一个结构体,并拿到它的名称、字段列表等信息
let struct_name = input.ident;
let fields = match input.data {
Data::Struct(data_struct) => data_struct.fields,
_ => panic!("'serde_json' can only be used with structs!"),
};
// 生成代码,将结构体转换为 JSON 字符串
let output = match fields {
Fields::Named(fields_named) => {
let field_names = fields_named.named.iter().map(|f| &f.ident);
quote! {
impl #struct_name {
pub fn to_json(&self) -> String {
serde_json::to_string(&json!({
#(stringify!(#field_names): self.#field_names,)*
})).unwrap()
}
}
}
}
Fields::Unnamed(fields_unnamed) => {
let field_indices = 0..fields_unnamed.unnamed.len();
quote! {
impl #struct_name {
pub fn to_json(&self) -> String {
serde_json::to_string(&json!([
#(self.#field_indices,)*
])).unwrap()
}
}
}
}
Fields::Unit => {
quote! {
impl #struct_name {
pub fn to_json(&self) -> String {
serde_json::to_string(&json!({
})).unwrap()
}
}
}
}
};
// 将生成的代码作为 TokenStream 返回
output.into()
}
main.rs
#[serde_json]
struct MyStruct {
name: String,
age: u32,
}
fn main() {
let my_struct = MyStruct {
name: "Alice".to_string(),
age: 25,
};
let json_str = my_struct.to_json();
println!("JSON string: {}", json_str);
}
fn_time
ライブラリ
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn run(_args: TokenStream, input: TokenStream) -> TokenStream {
// 将输入解析为函数节点
let input = parse_macro_input!(input as ItemFn);
// 获取函数名称、参数列表等信息
let func_name = &input.ident;
let func_args = &input.decl.inputs;
// 生成代码,在函数开始和结束时分别打印时间戳
let output = quote! {
#input
fn #func_name(#func_args) -> () {
println!("{} started", stringify!(#func_name));
let start = std::time::Instant::now();
let result = #func_name(#func_args);
let end = start.elapsed();
println!("{} finished in {}ms", stringify!(#func_name), end.as_millis());
result
}
};
// 将生成的代码作为 TokenStream 返回
output.into()
}
主要
#[run]
fn my_function() -> i32 {
// 模拟一些处理时间
std::thread::sleep(std::time::Duration::from_secs(1));
42
}
fn main() {
let result = my_function();
println!("Result = {}", result);
}