rust 過程巨集

来源:https://www.cnblogs.com/imlgc/archive/2023/12/26/17929429.html
-Advertisement-
Play Games

簡介 Rust 編程語言裡面有兩種巨集系統,一種是聲明巨集(Declarative Macros),另一種為過程巨集(Procedural Macros)。聲明巨集和過程巨集是兩種基本上完全不一樣的巨集系統,編寫的方式也完全不一致,使用方式除了函數式外也不一致。關於聲明巨集學習,Rust 巨集小冊 裡面有比較詳細的 ...


簡介

Rust 編程語言裡面有兩種巨集系統,一種是聲明巨集(Declarative Macros),另一種為過程巨集(Procedural Macros)。聲明巨集和過程巨集是兩種基本上完全不一樣的巨集系統,編寫的方式也完全不一致,使用方式除了函數式外也不一致。關於聲明巨集學習,Rust 巨集小冊 裡面有比較詳細的說明,這裡不再啰嗦。而對於過程巨集,網上是可以搜索到的資料則相對較少,系統介紹學習的資料就更加少了。

過程巨集所做的事情則是從輸入中獲取到標記流處理這些標記流或者生成新的標記流,然後將處理後的標記流返回給編譯器作下一步的處理。需要註意的是,過程巨集操作的是Rust AST(抽象語法樹),所以即使是在巨集裡面,也必須是合法Rust的語法結構。這也就意味著,解析過程巨集的過程中,var表示的是一個合法的標識符,而6var則是非法的。

這篇文章是,對過程巨集進行一些不完全的探討和學習。

三種過程巨集形式

過程巨集必須是一個獨立的庫(很多開源項目喜歡用xxx_derive的名稱命名),這個庫只導出過程巨集的函數,而這個巨集是被編譯器調用的。Cargo.toml裡面必須有以下內容表明是一個過程巨集:

[lib]
proc-macro = true

proc_macroRust 編譯器提供的編寫過程巨集所需的類型和工具,過程巨集有以下三種表示形式:

derive

  1. 函數帶有#[proc_macro_derive(Name)] 屬性或者 #[proc_macro_derive(Name, attributes(attr))]屬性
  2. 函數簽名為 pub fn xxxx (proc_macro::TokenStream) -> proc_macro::TokenStream

函數的名稱叫什麼並不重要,使用時是使用proc_macro_derive裡面的名稱,如下例子

#[proc_macro_derive(Getters, attributes(getter))]
pub fn getters(input: TokenStream) -> TokenStream {
  //...
}

使用

#[derive(Getters)]
struct Test {
  #[getter(name=get_name)]
  name: String
}

函數式

  1. 函數帶有 #[proc_macro] 屬性
  2. 函數簽名為 pub fn xxx (proc_macro::TokenStream) -> proc_macro::TokenStream

函數的名稱就是使用時名稱,如下例子:

#[proc_macro]
pub fn lazy_static(input: TokenStream) -> TokenStream {
  //...
}

使用方式和聲明巨集調用一摸一樣

lazy_static!{
 //...
}

屬性式

  1. 函數帶有#[proc_macro_attribute]屬性
  2. 函數簽名為 pub fn xxx(proc_macro::TokenStream, proc_macro::TokenStream) -> proc_macro::TokenStream

函數的名稱就是使用時名稱,如下例子:

#[proc_macro_attribute]
pub fn retry(attr: TokenStream, input: TokenStream) -> TokenStream {
  //...
}

使用方式和Python裝飾器的使用方式類似

#[retry(times=5, timeout=60s)]
pub fn fetch_data(url: String) -> Result<MyData> {
  //...
}

一般來說,derive 式是對原有功能的擴展,原有的聲明是保留下的,更多是在原有基礎上增加功能,比如增加impl函數,增加泛型約束等等。函數式則更多的是用於自定義語法的解析,如果聲明巨集描述語法困難一般可以考慮用函數式來替代。而屬性式則是完全對原有功能的改寫了,屬於替代性的。特別需要註意的是,過程並不是衛生性的,這點是和聲明巨集不一樣。也就是說,過程巨集它是會污染當前模塊。所以,在過程巨集裡面定義或使用類型時,必須要用全路徑的形式,而自定生成的函數變數等等命名也要特殊考慮,以防止污染了當前模塊。

quote, syn, proc_macro2以及trybuildcargo-expand

編寫過程巨集,編譯器只提供proc_macro這個crate,不過它所提供的功能非常有限,單獨使用這個庫的話,編寫比較啰嗦和麻煩。因此大神dtolnay提供了, 和``這三個庫來簡化過程巨集的編寫,有了這三個crate,編寫過程巨集就如同編寫普通的代碼一樣,除了調試困難一點外,基本沒什麼差別。這三個庫基本成功編寫過程巨集事實上的標準。

同時為了使編寫過程更加溫柔點而不至於暴躁,dtolnay提供了trybuild這個庫用於編寫巨集的單元測試,cargo-expand用於過程巨集的展開。

proc_macro2

proc_macro所提供的TokenStreamquotesyn所處理的TokenStream是不相容的,所以另外增加一個 proc_macro2,用於和proc_macroTokenStream互相轉換。實際上這個庫的使用,只是在最後返回值裡面調用一下into而已。

quote

quote是將編寫的代碼轉換為Rust token的方式,提供一種稱之為quasi-quoting的方式,將代碼視為數據,並可以進行插值。比較常用的是這兩個巨集:parse_quote!quote!,以及format_ident!

syn

這是編寫過程巨集最重要的一個,大部分時間都是和這個庫進行打交道,它表示了一個完整的Rust 語法,如果看語言的Reference感覺到抽象,最好來這裡看代碼,它以非常具體的編碼實現告訴你這個Reference是怎麼表示的。

trybuild

平常的單元測試是編譯好的代碼,然後再運行測試用例。然而過程巨集的的測試,處需要測試是否編譯通過,也需要編譯出錯的結果是否正確。這是需要這個trybuild庫了。

cargo-expand

cargo-expand用於巨集代碼的展開,需要註意的是,需要正常編譯通過的代碼才可以進行展開。

使用這quote, syn, proc_macro2三個庫來編寫過程巨集後,框架代碼基本一致,一般有如下三個步驟,如下方式:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyMacro)]
pub fn my_macro(input: TokenStream) -> TokenStream {
    // 將輸入的標記解析成語法樹
    let input = parse_macro_input!(input as DeriveInput);
  
	// 使用quote!進行插值處理
    let expanded = quote! {
        // ...
    };

    // 將proc_macro2的TokenStream轉換為proc_macro的TokenStream
    TokenStream::from(expanded)
}

以下,編寫三種形式是示例,來看下著三種形式的過程過程巨集怎麼編寫以及怎麼使用。示例代碼倉庫:https://github.com/buf1024/my_macro_demo

示例:derive

假設我們需要為結構體生成一系列的getter函數,當然getter的名字是可以自定義的也可以根據預設的欄位名稱生成,也可以設置getter的可見性,同時根據是否註釋生成對應的desc函數。不需要考慮這樣的功能在實際工作中是否有意義,這裡的重點是學校過程巨集的編寫過程。

首先編寫巨集就是為了使用它,所以第一步,要瞭解是怎麼使用這個巨集:

// 首先可以是這樣簡單使用
#[derive(Getters)]
struct MyStruct {
    data: String,
}

// 又或者想變更一下它的名稱
#[derive(Getters)]
struct MyStruct {
    #[getter(name=get_fuck_data)]
    data: String,
}

// 又或者是這樣
#[derive(Getters)]
struct MyStruct {
	#[getter(vis=pub(crate))]
    #[getter(name=get_fuck_data)]
    data: String,
}

// 以及可能有註釋
#[derive(Getters)]
struct MyStruct {
    /// 這是一個data的屬性
	#[getter(vis=pub(crate))]
    #[getter(name=get_fuck_data)]
    data: String,
}

// 設置機構體可能複雜, 帶上了生命周期參數和泛型
#[derive(Getters)]
struct MyStruct<'a, T: Sync+Send+Constraint> {
	#[getter(vis=pub(crate))]
    #[getter(name=get_fuck_data)]
    data: &'a str,
    constraint: T
}

確定了過程巨集的使用方式後,我就可以可以定義我們的導出函數了:

#[proc_macro_derive(Getters, attributes(getter))]
pub fn getters(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let token_stream = expand_getters(input);
    token_stream
        .unwrap_or_else(|e| e.into_compile_error())
        .into()
}

我們將輸入token流解析為DeriveInput,是因為DeriveInput實現了Parse trait。定義如下:

pub trait Parse: Sized {
    fn parse(input: ParseStream) -> Result<Self>;
}

pub struct DeriveInput {
        pub attrs: Vec<Attribute>,
        pub vis: Visibility,
        pub ident: Ident,
        pub generics: Generics,
        pub data: Data,
    }

pub enum Data {
        Struct(DataStruct),
        Enum(DataEnum),
        Union(DataUnion),
    }

DeriveInput所實現的ParseDeriveInput數據結構可以看出,derive 式過程巨集只支持StructEnumUnion三種數據結構。

寫過程巨集的一個重要的工作就是獲取所修飾的數據結構的基本信息,而對於derive 式過程巨集來說,這些數據放到attrs這個屬性裡面,用Attribute這個結構來表示,Meta則是存儲這樣數據的。

pub struct Attribute {
        pub pound_token: Token![#],
        pub style: AttrStyle,
        pub bracket_token: token::Bracket,
        pub meta: Meta,
    }

pub enum Meta {
        Path(Path),

        /// A structured list within an attribute, like `derive(Copy, Clone)`.
        List(MetaList),

        /// A name-value pair within an attribute, like `feature = "nightly"`.
        NameValue(MetaNameValue),
    }

Meta是什麼鬼?按照syn的文檔:

    /// text
    /// #[derive(Copy, Clone)]
    ///   ~~~~~~Path
    ///   ^^^^^^^^^^^^^^^^^^^Meta::List
    ///
    /// #[path = "sys/windows.rs"]
    ///   ~~~~Path
    ///   ^^^^^^^^^^^^^^^^^^^^^^^Meta::NameValue
    ///
    /// #[test]
    ///   ^^^^Meta::Path
    /// 

需要註意的是,註釋文檔是解析為#[doc = r" Single line doc comments"]的。所以,文檔註釋的獲取:

let doc_str: String = f
                .attrs
                .iter()
                .filter(|attr| attr.path().is_ident("doc"))
                .try_fold(String::new(), |acc, attr| {
                    let mnv = match &attr.meta {
                        syn::Meta::NameValue(mnv) => mnv,
                        _ => return Err(syn::Error::new_spanned(attr, "expect name value!")),
                    };
                    let doc_str = match &mnv.value {
                        syn::Expr::Lit(syn::ExprLit {
                            lit: syn::Lit::Str(lit),
                            ..
                        }) => lit.value(),

                        _ => return Err(syn::Error::new_spanned(attr, "expect string literal!")),
                    };
                    Ok(format!("{}\n{}", acc, doc_str))
                })?;

前面提到DeriveInput實現了Parsetrait進行解析,而我們要對Attribute裡面的內容進行解析,則是需要實現該trait:

impl Parse for GetterMeta {
    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(kw::name) {
            let _: kw::name = input.parse()?;
            let _: Token![=] = input.parse()?;

            let name: Ident = if input.peek(LitStr) {
                let sl: LitStr = input.parse()?;
                let value = sl.value();

                format_ident!("{}", value.trim())
            } else {
                input.parse()?
            };

            Ok(Self {
                name: Some(name),
                vis: None,
            })
        } else if lookahead.peek(kw::vis) {
            let _: kw::vis = input.parse()?;
            let _: Token![=] = input.parse()?;

            let vis = input.parse()?;

            Ok(Self {
                name: None,
                vis: Some(vis),
            })
        } else {
            Err(lookahead.error())
        }
    }
}

寫法可以完全參考DeriveInput的寫法,調用Atrribute的,parse_argsparse_args_with,則可以調用到Parsetrait。

獲取到基本的數據後,自然就生成代碼,這裡兩個重要的巨集: quote!parse_quote!。最後生成的代碼用#[automatically_derived]進行裝飾,說明這是自動生成的代碼。

let (impl_generic, type_generic, where_clause) = input.generics.split_for_impl();

    Ok(quote! {
        #[automatically_derived]
        impl #impl_generic #st_name #type_generic #where_clause {
            pub fn hello(&self) {
                println!("hello!");
            }
            #getters
        }
    })

過程巨集寫好之後,我們就要寫單元測試了,當然也可以先寫單元測試,再寫過程巨集,這稱為測試用例驅動的開發模式。不過,作為示例,我們不寫很詳細的測試用例,我們只寫兩個測試用例,一個是成功的,一個是失敗的,展示怎麼使用即可。失敗的用例,我們需要提供一個和測試文件一致,以.stderr結尾的輸出,如果編譯器的輸出一致則測試通過,這個輸出可以先編譯出來,讓編譯器生成,然後自己對比是不是自己想要的結果。

過程巨集使用後,是否符合自己的需要,需要巨集展開來觀察,使用是cargo exand命令:

#![allow(dead_code)]

use my_derive::Getters;

#[derive(Getters)]
struct MyStructRef<'a> {
    /// 你好呀
    #[getter(vis=pub(crate))]
    #[getter(name = "get_fuck_data")]
    data: &'a str,
}

fn main() {}



//展開代碼

#![feature(prelude_import)]
#![allow(dead_code)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use my_derive::Getters;
struct MyStructRef<'a> {
    /// 你好呀
    #[getter(vis = pub(crate))]
    #[getter(name = "get_fuck_data")]
    data: &'a str,
}
#[automatically_derived]
impl<'a> MyStructRef<'a> {
    pub fn hello(&self) {
        {
            ::std::io::_print(format_args!("hello!\n"));
        };
    }
    pub(crate) fn get_fuck_data(&self) -> &'a str {
        &self.data
    }
    pub fn get_fuck_data_desc(&self) -> &'static str {
        "你好呀"
    }
}
fn main() {}

示例:屬性式

假設我們有這樣一個需求,可以對原有函數進行增加一些其他功能,比如retry,可以設置調用超時時間,超時後或者出錯後,可以進行重新調用,類似於Python裝飾器,可以考慮用屬性式過程巨集表示。同樣,因為是測試例子,所以,沒有超時和重試功能,只做怎麼獲取屬性巨集的數據和生成代碼。

首先確定調用的形式:

#[retry(times=5, timeout=60)]
fn remote_request(a: i32, b: i32) -> i32 {
    println!("@remote_request!");
    a + b
}

再確定其展開形式:

// cargo exapnd 生成的
fn remote_request(a: i32, b: i32) -> i32 {
    fn __new_remote_request(a: i32, b: i32) -> i32 {
        {
            ::std::io::_print(format_args!("@remote_request!\n"));
        };
        a + b
    }
    for _ in 0..5 {
        __new_remote_request(a, b);
    }
    for _ in 0..60 {
        __new_remote_request(a, b);
    }
    __new_remote_request(a, b)
}

屬性式的簽名是這個樣子的:

#[proc_macro_attribute]
pub fn retry(attr: TokenStream, input: TokenStream) -> TokenStream

屬性和輸入流已經作為參數傳遞給我們了,而我需所要做的是需要將屬性解析出來和對原函數的重寫。

    let item_fn = parse_macro_input!(input as ItemFn);

    let args = parse_macro_input!(attr as Args);

derive式的一樣,通過實現Parsetrait來解析:

impl Parse for RetryAttr {
    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(kw::times) {
            let _: kw::times = input.parse()?;
            let _: Token![=] = input.parse()?;

            let times: LitInt = if input.peek(LitInt) {
                input.parse()?
            } else {
                return Err(lookahead.error());
            };

            Ok(Self {
                times: Some(times),
                timeout: None,
            })
        } else if lookahead.peek(kw::timeout) {
            let _: kw::timeout = input.parse()?;
            let _: Token![=] = input.parse()?;

            let timeout: LitInt = if input.peek(LitInt) {
                input.parse()?
            } else {
                return Err(lookahead.error());
            };

            Ok(Self {
                times: None,
                timeout: Some(timeout),
            })
        } else {
            Err(lookahead.error())
        }
    }
}

示例:函數式

假設我們要計算二元二次方程組的值,我們計劃是這樣使用的:

let (x, y) = formula!(1 * x + 1 * y = 2, 2 * x + 1 * y = 9);

這個巨集直接就在編譯期間就計算出x,y的值(當然是存在有解的情況下),無解就panic。為了使問題簡單,我們假設x,y前面都是有繫數的。過程巨集的操作過程就是解析出裡面的表達式:

pub(crate) struct FormulaArgs {
    formula: Vec<Formula>,
}

impl Parse for FormulaArgs {
    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
        let attrs = Punctuated::<Formula, Token![,]>::parse_terminated(input)?;
        let formula: Vec<_> = attrs.into_iter().collect();
        if formula.len() != 2 {
            panic!("require two formula")
        }
        Ok(FormulaArgs { formula })
    }
}

#[derive(Default)]
pub(crate) struct Formula {
    x: i32,
    y: i32,
    rs: i32,
}

impl Parse for Formula {
    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
        let lookahead = input.lookahead1();

        let x = if lookahead.peek(LitInt) {
            let x_lit: LitInt = input.parse()?;
            let x: i32 = x_lit.to_string().parse().unwrap();
            let _: Token![*] = input.parse()?;
            let _: kw::x = input.parse()?;

            x
        } else {
            return Err(lookahead.error());
        };

        let r1: Result<Token![+]> = input.parse();
        let r2: Result<Token![-]> = input.parse();
        let factor = match (r1, r2) {
            (Ok(_), Err(_)) => 1,
            (Err(_), Ok(_)) => -1,
            (_, _) => return Err(lookahead.error()),
        };

        // let factor = if lookahead.peek(Token![+]) {
        //     let _: Token![+] = input.parse()?;
        //     1
        // } else if lookahead.peek(Token![-]) {
        //     let _: Token![-] = input.parse()?;
        //     -1
        // } else {
        //     return Err(lookahead.error());
        // };

        let y = if lookahead.peek(LitInt) {
            let y_lit: LitInt = input.parse()?;
            let y: i32 = y_lit.to_string().parse().unwrap();
            let _: Token![*] = input.parse()?;
            let _: kw::y = input.parse()?;
            y * factor
        } else {
            return Err(lookahead.error());
        };

        let _: Token![=] = input.parse()?;

        let rs_lit: LitInt = input.parse()?;
        let rs: i32 = rs_lit.to_string().parse().unwrap();

        if x == 0 && y == 0 && rs != 0 {
            return Err(syn::Error::new_spanned(rs_lit, "invalid equal"));
        }

        Ok(Self { x, y, rs })
    }
}

當解析出所需要的數據後,直接就可以進行插值生成自己所需要的數據。

總結

過程巨集的編寫過程有兩個步驟。

首先是解析出過程巨集所需要的信息,這個步驟一般是通過實現syn所提供的Parsetrait實現的。由於syn表示的也是合法的語法結構,所以並不是所以的寫法都是支持的。有時候,解析的時候出出現一些莫名奇妙解析不了的問題,比如解析二元方程時:

       // let factor = if lookahead.peek(Token![+]) {
        //     let _: Token![+] = input.parse()?;
        //     1
        // } else if lookahead.peek(Token![-]) {
        //     let _: Token![-] = input.parse()?;
        //     -1
        // } else {
        //     return Err(lookahead.error());
        // };

這段代碼總是無法解析成功,原因未解,或者使用姿勢不對,又或者是syn可能潛在有bug。不過都有變通的方法實現。

其次解析出自己所要的數據後,就可以根據具體的情況進行插值處理,只有是使用到quote這個庫,而大多數情況之下只使用到兩個巨集: quote!parse_quote!。當然也不是說沒有坑。比如說解析出函數的block時,再重組時,要加上大擴號。再比如,解析出行數調用列表時,解析成一個元組表達式,而插值時,可以放個函數名稱在前面,就變成了函數調用。而這些都是變成過程巨集編寫的慣例吧,習慣就好。

錯誤處理是給巨集的使用者看的,友好的錯誤提示很容易就讓調用者知道哪裡錯了。而使用錯誤處理是比較簡單的,直接掉用syn::Error生成一個Span即可,Span也是可以combine的裡面有個token的參數。不過如果不知道token的情況之下怎麼處理呢?目前自己的做法是panic,這並非是一種明智的方式。或者有更加靈活的處理方式。

使用cargo expand是可以查看到巨集展開的內容,不過cargo expand的問題是過程巨集沒有問題時,才可以正常的展開,出現編譯問題不展開,這也就造成了調試過程巨集的困難,目前也沒有什麼好的辦法去解決。

總體來說,過程巨集的編寫並不是非常困難,syn表示了一個完整的Rust語法,查看裡面語法的表示,比看語言Reference強太多了,而這對於更深入瞭解聲明巨集的片段分類符更有幫助!proc-macro-workshop 是大神dtolnay設計的過程巨集系統,全部做出來,估計寫過程巨集沒有什麼問題了。

再貼一下示例代碼倉庫:https://github.com/buf1024/my_macro_demo

Post AT:

https://mp.weixin.qq.com/s/17jPRjzyU4lkSD-mS1wxvg


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在運行程式時有時候會需要查看資源占用,以方便部署在其他伺服器上時進行參考。以下是總結了我在linux上查找程式進程資源的兩種方法(cpu和gpu都有)。 ...
  • 在 Apache Flink 中實現高效的 Top N 數據處理,尤其是涉及時間視窗和多條件排序時,需要精細地控制數據流和狀態管理。 普通計算TopN: 1. 定義數據源(Source) 首先,我們需要定義數據源。這可能是 Kafka 流、文件、資料庫或任何其他支持的數據源。 val stream: ...
  • 在回答這個問題之前,首先我們看看 MySQL 中有哪些常用的 JDBC 連接池: c3p0 DBCP Druid Tomcat JDBC Pool HikariCP 這些連接池中,c3p0 是一個老牌的連接池,很多流行框架,在其老版本中,都將 c3p0 作為預設的連接池。 DBCP 和 Tomcat ...
  • 出現分散式死鎖現象後,如果沒有外部干預,通常是一方等待鎖超時報錯後,事務回滾清理持有鎖資源,另一方可繼續執行。 ...
  • 背景 由於業務變遷,合規要求,我們需要刪除大量非本公司的數據,涉及到上百張表,幾個T的數據清洗。我們的做法是先從基礎數據出發,將要刪除的數據id收集到一張表,然後再由上往下刪除子表,多線程併發處理。 我們使用的是阿裡的polardb,完全相容mysql協議,5.7版本,RC隔離級別。刪除過程一直很順 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 工程化最開始就是package.json開始的,很多人學了很多年也沒搞清楚這個為什麼這麼神奇,其實有些欄位是在特定場景才有效的,那每個屬性的適用場景和作用是什麼,又牽扯很多知識點,今天先解讀一些常見的屬性,關註我,後期在遇到特定場景也會再 ...
  • 本章目標:偵聽器watch是如何相容ref、響應式對象和getter函數等不同數據源的?回調時機immediate是如何實現的?關於onCleanup,一個用於註冊副作用清理的回調函數是如何實現的? ...
  • 隨著小程式使用場景越發廣泛,用戶體驗愈發受重視,如何通過技術手段提升小程式性能成為重中之重,本篇文章以京東購物小程式性能優化實踐為例,帶您實現性能翻倍! ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...