一名C++程式員的Rust入門初體驗

来源:https://www.cnblogs.com/88223100/archive/2023/04/02/A-CPP-programmers-initial-experience-with-Rust.html
-Advertisement-
Play Games

作者最近嘗試寫了一些Rust代碼,本文主要講述了對Rust的看法和Rust與C++的一些區別。 背景 S2在推進團隊代碼規範時,先後學習了盤古編程規範,CPP core guidelines,進而瞭解到clang-tidy,以及Google Chrome 在安全方面的探索。 C++是一個威力非常強大 ...


作者最近嘗試寫了一些Rust代碼,本文主要講述了對Rust的看法和Rust與C++的一些區別。

背景

S2在推進團隊代碼規範時,先後學習了盤古編程規範,CPP core guidelines,進而瞭解到clang-tidy,以及Google Chrome 在安全方面的探索。
C++是一個威力非常強大的語言,但是能力越大,責任越大,它的記憶體安全性問題一直飽受詬病。NSA甚至明確提出,停止使用C++這種記憶體不安全的語言。
C++本身的確提出了一系列改進方案,但是遲遲不見落地。Bjarne對於NSA挑戰給出的方案也只能部分解決問題,並且看起來落地也是遙遙無期。
Rust作為一個新晉語言,是Mozilla應對記憶體安全性問題發明的新語言(之前它也是使用C++的),Linux和Chrome都開始先後接納了它,Tikv與大部分區塊鏈項目,在第一天就選擇了它。
遇到若幹次記憶體踩壞問題後,我有了瞭解Rust的衝動。

C++代碼中的風險

圖片

這張圖是Chrome團隊發佈的Chrome的代碼被攻擊的Bug類型的分佈,可以看到,記憶體安全性占了一半以上。
  • Temporal safety: 簡單來說就是use after free
  • Spatial safety: 簡單來說,就是out of bound訪問
  • Logic error
  • DCHECK: 簡單來說,就是debug assert的條件在release版本中被觸發了

其他不多展開。

Rust初體驗

初體驗Rust,實際上更多的是感覺到它的一些小設計非常甜,它會讓我們的編程很舒服。
簡單來說,所有C++通過Best Practice/Effective C++/...等推行的寫法,Rust全部是編譯器強制的。

 

預設不可變

Rust中,所有變數是預設不可變的,可變需要額外的typing。這與C++是完全相反的。然而,這與C++ Core Guidelines中的推薦卻是一致的。

let x = 0;
x = 10;  // error

let mut y = 0;
y = 10;  //ok

 

禁止整數隱式轉換

fn foo(x: u32) {}

let x: i32 = 0;
foo(x);  // error
Be explicit,這條軟體界的普遍規則,在C/C++中卻是完全不適用,真是反直覺。

 

簡化構造、複製與析構

C++中的Rule of 3 or 5 or 6可謂是大名鼎鼎,我們無數次需要寫以下代碼
class ABC
{
public:
    virtual ~ABC();

    ABC(const ABC&) = delete;
    ABC(ABC&&) = delete;

    ABC& operator=(const ABC&) = delete;
    ABC& operator=(ABC&&) = delete;
};

明明是一件非常常規的東西,寫起來卻那麼的複雜。

Rust非常簡單,所以對象預設只支持Destructive move(通過memcpy完成)。需要複製,要類顯式實現Clone trait,複製時寫.clone(), 對於trivial對象,期望能通過=來隱式copy,要顯式實現Copy,實現Copy時,不允許類再實現Drop(即析構函數)。
fn main()
{
    // String類似std::string,只支持顯式clone,不支持隱式copy
    let s: String = "str".to_string();
    
    foo(s);  // s will move
    // cannot use s anymore

    let y = "str".to_string();
    foo(y.clone());

    // use y is okay here
}

fn foo(s: String) {}

// can only be passed by move
struct Abc1
{
    elems: Vec<int>
}

// can use abc.clone() to explicit clone a new Abc
#[derive(Clone)]
struct Abc2
{
    elems: Vec<int>
}

// implement custom destructor for Abc
impl Drop for Abc2 {
    // ...
}

// foo(xyz) will copy, 不能再定義Drop/析構函數,因為copy和drop是互斥的
#[dervie(Clone, Copy)]
struct Xyz
{
    elems: i32
}
比起C++的move,以及其引入的use after move問題,還有各種Best Practice,Rust的做法實在是高明瞭不少。
媽媽再也不用擔心我會不小心copy一個有1千萬元素的vector了。review時也再也不用糾結,這裡到底是用值還是const&了。

 

顯式參數傳遞

C++中函數參數傳遞的Best Practice能寫一堆,in/out/inout參數如何處理也沒有一個明確的規範。Rust則簡化了參數傳遞,並且將一切由隱式轉變為顯式。

let mut x = 10;

foo(x);  // pass by move, x cannot be used after the call
foo(&x);  // pass by immutable reference
foo(&mut x); // pass by mutable reference

 

統一的錯誤處理
錯誤處理一直是C++中一個非常分裂的地方,截止C++23,目前C++標準庫中,有以下用於錯誤處理的功能:
  • errno
  • std::exception
  • std::error_code/std::error_condition
  • std::expected

看,連標準庫自己都這樣。std::filesystem,所有介面都有至少兩個重載,一個拋異常,一直傳std::error_code。

Rust的方案與Herb提出的static異常類似,並且通過語法糖,讓錯誤處理非常容易。

enum MyError
{
    NotFound,
    DataCorrupt,
    Forbidden,
    Io(std::io::Error)
}

impl From<io::Error> for MyError {
    fn from(e: io::Error) -> MyError {
        MyError::Io(e)
    }
}

pub type Result<T> = result::Result<T, Error>;

fn main() -> Result<()>
{
    let x: i32 = foo()?;
    let y: i32 = bar(x)?;

    foo();  // result is not handled, compile error

     // use x and y
}

fn foo() -> Result<i32>
{
    if (rand() > 0) {
        Ok(1)
    } else {
        Err(MyError::Forbidden)
    }
}

錯誤處理一律通過Result<T, ErrorType>來完成,通過?,一鍵向上傳播錯誤(如同時支持自動從ErrorType1向ErrorType2轉換,前提是你實現了相關trait),沒有錯誤時,自動解包。當忘記處理處理Result時,編譯器會報錯。

 

內置格式化與lint

Rust的構建工具cargo,內置了cargo fmt和cargo clippy,一鍵格式化與lint,再也不用人工配置clang-format和clang-tidy了。

 

標準化的開發流程和包管理

Rust最為C++程式員所羡慕的地方是,它有官方包管理工具cargo。C++非官方包管理工具conan目前有1472個包,cargo的包管理平臺有106672個包。
cargo還原生支持了test與benchmark,一鍵運行
cargo test
cargo bench

cargo規定了目錄風格


benches  // benchmark代碼go here
src
tests  // ut go here

Rust在安全性的改進

上一節提的其實還是非致命的東西,Rust在記憶體安全方面的改進,才是讓它與眾不同的原因。

 

lifetime安全性

use-after-free是這個世界上最為著名的bug之一。解決它的方案一直以來都是依賴運行時檢查,兩個主要流派是GC與引用計數。而Rust在此之外引入了一種新的機制:Borrow Check。
Rust規定,所有對象都是有所有權的,賦值意味著所有權的轉讓。一旦所有權轉讓後,舊的對象將無法再被使用(destructive move)。Rust允許一個對象的所有權暫時被租用給其他引用。所有權可以租借給若幹個不可變引用,或者一個獨占的可變引用。
舉個例子:

let s = vec![1,2,3];  // s owns the Vec
foo(s);  // the ownership is passed to foo, s cannot be used anymore

let x = vec![1,2,3];
let a1 = &x[0];
let a2 = &x[0];  // a1/a2 are both immutable ref to x

x.resize(10, 0);  // error: x is already borrowed by a1 and a2

println!("{a1} {a2}");

這種unique ownership + borrow check的機制,能夠有效的避免pointer/iterator invalidation bug以及aliasing所引發的性能問題。

在此之上,Rust引入了lifetime概念,即,每個變數有個lifetime,當多個變數間存在引用關係時,編譯器會檢查這些變數之間的lifetime關係,禁止一個非owning引用,在其原始對象lifetime結束之後再被訪問。
let s: &String;

{
    let x = String::new("abc");
    s = &x;
}

println!("s is {}", s);  // error, lifetime(s) > lifetime(x)

這個例子比較簡單,再看一些複雜的。

// not valid rust, for exposition only
struct ABC
{
    x: &String,
}

fn foo(x: String)
{
    let z = ABC { x: &x };

    consume_string(x);  // not compile, x is borrowed by z
    drop(z);  // call destructor explicitly

    consume_string(x);  // ok

    // won't compile, bind a temp to z.x
    let z = ABC { x: &String::new("abc") };
    // use z

    // Box::new == make_unique
    // won't compile, the box object is destroyed soon
    let z = ABC{ x: &*Box::new(String::new("abc") };
    // use z
}

再看一個更加複雜的,涉及到多線程的。


void foo(ThreadPool* thread_pool)
{
    Latch latch{2};

    thread_pool->spawn([&latch] {
        // ...
        latch.wait();  // dangle pointer訪問
    });

    // forget latch.wait();
}

這是一個非常典型的lifetime錯誤,C++可能要到運行時才會發現問題,但是對於Rust,類似代碼的編譯是不通過的。因為latch是個棧變數,其lifetime非常短,而跨線程傳遞引用時,這個引用實際上會可能在任意時間被調用,其lifetime是整個進程生命周期,rust中為此lifetime起了一個專門的名字,叫'static。正如cpp core guidelines所說:CP.24: Think of a thread as a global container ,never save a pointer in a global container。

在Rust中,rust編譯器會強制你使用引用計數,來顯示說明共用需求(em...發現這句話問題的,已經是Rust高手了)。

fn foo(thread_pool: &mut ThreadPool)
{
    let latch = Arc::new(Latch::new(2));
    let latch_copy = Arc::clone(&latch);

    thread_pool.spawn(move || {
        // the ownership of latch_copy is moved in
        latch_copy.wait();
    });

    latch.wait();
}

再看一個具體一些例子,假設你在寫一個文件reader,每次返回一行。為了降低開銷,我們期望返回的這一行,直接引用parser內部所維護的buffer,從而避免copy。


FileLineReader reader(path);

std::string_view<char> line = reader.NextLine();
std::string_view<char> line2 = reader.NextLine();

// ops
std::cout << line;
再看看Rust

let reader = FileReader::next(path);
let line = reader.next_line();

// won't compile, reader is borrowed to line, cannot mutate it now
let line2 = reader.next_line();

println!("{line}");

// &[u8] is std::span<byte>
fn foo() -> &[u8] {
    let reader = FileReader::next(path);
    let line = reader.next_line();

    // won't compile, lifetime(line) > lifetime(reader)
    return line;
}
 

總結來說,Rust定義了一套規則,按照此規則進行編碼,絕對不會有lifetime的問題。當Rust編譯器無法推導某個寫法的正確性時,它會強制你使用引用計數來解決問題。

 

邊界安全性

Buffer overflow以及out of bound訪問也是一類非常重要的問題,這類問題相對好解,給標準庫實現加下bound check就好了。Rust標準庫會進行bound check。
這方面,C++稍微努力下,還是能避免的。
啥?bound check性能差。Em...看看Chrome發佈的漏洞報告吧,人呢,還是不要太自信得好。畢竟,Bjarne都開始妥協了。Herb的slide中有對out of bound問題一些數字的說明。

 

類型安全性

Rust預設強制變數初始化,並且禁止隱式類型轉換。

let i: i32;

if rand() < 10 {
    i = 10;
}

println!("i is {}", i);  // do not compile: i is not always initialized

Rust的多線程安全性

如果說lifetime + ownership模型是Rust的安全核心的話,Rust的多線程安全性就是在此基礎上結出的果實。Rust的多線程安全性,完全是通過庫機制來實現的。
首先介紹兩個基礎概念:
  • Send: 一個類型是Send,表明,此類型的對象的所有權,可以跨線程傳遞。當一個新類型的所有成員都是Send時,這個類型也是Send的。幾乎所有內置類型和標準庫類型都是Send的,Rc(類似local shared_ptr)除外,因為內部用的是普通int來計數。
  • Sync: 一個類型是Sync,表明,此類型允許多個線程共用(Rust中,共用一定意味著不可變引用,即通過其不可變引用進行併發訪問)。

Send/Sync是兩個標準庫的Trait,標準庫在定義它們時,為已有類型提供了對應實現或者禁止了對應實現。

通過Send/Sync與ownership模型,Rust讓Data race完全無法出現。
簡單來說:
  • lifetime機制要求:一個對象要跨線程傳遞時,必須使用Arc(Arc for atomic reference counted)來封裝(Rc不行,因為它被特別標註為了!Send,即不可跨線程傳遞)
  • ownership+borrow機制要求:Rc/Arc包裝的對象,只允許解引用為不可變引用,而多線程訪問一個不可變對象,是天生保證安全的。
  • 內部可變性用於解決共用寫問題:Rust預設情況下,共用一定意味著不可變,只有獨占,才允許變。如果同時需要共用和可變,需要使用額外的機制,Rust官方稱之為內部可變性,實際上叫共用可變性可能更容易理解,它是一種提供安全變更共用對象的機制。如果需要多線程去變更同一個共用對象,必須使用額外的同步原語(RefCell/Mutex/RwLock),來獲得內部/共用可變性,這些原語會保證只有一個寫者。RefCell是與Rc一起使用的,用於單線程環境下的共用訪問。RefCell被特別標註為了!Sync,意味著,如果它和Arc一起使用,Arc就不是Send了,從而Arc<RefCell<T>>無法跨線程。

看個例子:假設我實現了一個Counter對象,希望多個線程同時使用。為瞭解決所有權問題,需要使用Arc<Counter>,來傳遞此共用對象。但是,以下代碼是編譯不通過的。


struct Counter
{
    counter: i32
}

fn main()
{
    let counter = Arc::new(Counter{counter: 0});
    let c = Arc::clone(&counter);
    thread::spawn(move || {
        c.counter += 1;
    });

    c.counter += 1;
}

因為,Arc會共用一個對象,為了保證borrow機制,訪問Arc內部對象時,都只能獲得不可變引用(borrow機制規定,要麼一個可變引用,要麼若幹個不可變引用)。Arc的這條規則防止了data race的出現。

為瞭解決這個問題,Rust引入了內部可變性這個概念。簡單來說,就是一個wrapper,wrapper可以獲得一個內部對象的可變引用,但是wrapper會進行borrow check,保證只有一個可變引用,或者若幹個不可變引用。
單線程下,這個wrapper是RefCell,多線程下,是Mutex/RwLock等。當然,如果你嘗試寫這樣的代碼,也是編譯不通過的

fn main()
{
    let counter = Arc::new(RefCell::new(Counter{counter: 0}));
    let c = Arc::clone(&counter);
    thread::spawn(move || {
        c.get_mut().counter += 1;
    });

    c.get_mut().counter += 1;
}

為啥?因為RefCell不是Sync,即不允許多線程訪問。Arc只在內部類型為Sync時,才為Send。即,Arc<Cell<T>>不是Send,無法跨線程傳遞。

Mutex是Send的,因此,可以這麼寫:

struct Counter
{
    counter: i32
}

fn main()
{
    let counter = Arc::new(Mutex::new(Counter{counter: 0}));
    let c = Arc::clone(&counter);
    thread::spawn(move || {
        let mut x = c.lock().unwrap();

        x.counter += 1;
    });
}

Rust的性能

作為C++的挑戰者,更多的人會關註Rust的性能到底怎麼樣。Rust的官方哲學是zero cost principle,是不是和C++的zero overhead原則很像。當然,這個名字起的實際上沒有C++好,畢竟,做事情就是有cost的,怎麼可能是zero cost呢。
Rust添加了bound check,可能會比C++弱一點點,但也有限。同時Rust支持unsafe模式,完全跳過bound check。這讓Rust的上限可以和C++持平。
另外,Rust禁止了很多conditionally正確的用法,也會有一定性能損失,比如跨線程必須shared_ptr(額外的動態分配)。

// for demo purpose

fn foo(tasks: Vec<Task>)
{
    let latch = Arc::new(Latch::new(tasks.len() + 1));

    for task in tasks {
        let latch = Arc::clone(&latch);
        thread_pool.submit(move || {
            task.run();
            latch.wait();
        });
    }

    latch.wait();
}

這裡,latch必須用Arc(即shared_ptr)。

在某些場景下,Rust會比C++還快。優化聖經有言,阻礙編譯器優化的兩大天敵:
  • 函數調用
  • 指針別名

C++和Rust都可以通過inline來消除函數調用引起的開銷。但是C++面對指針別名時,基本上是無能為力的。C++對於指針別名的優化依賴strict aliasing rule,不過這個rule出了名的噁心,Linus也罵過幾次。Linux代碼中,會使用-fno-strict-aliasing來禁止這條規則的。

不過,C好歹有__restricted__來救救命,C++程式員就只有god knows了。
而Rust通過所有權和借用機制,是能保證沒有aliasing現象的。考慮下麵這段代碼

int foo(const int* x, int* y)
{
    *y = *x + 1;

     return *x;
}

rust版本


fn foo(x: &i32, y: &mut i32) -> i32
{
    *y = *x + 1;

    *x
}

對應的彙編如下:


# c++
__Z3fooPKiPi:
    ldr    w8, [x0]
    add    w8, w8, #1
    str    w8, [x1]
    ldr    w0, [x0]
    ret

# rust
__ZN2rs3foo17h5a23c46033085ca0E:
    ldr    w0, [x0]
    add    w8, w0, #1
    str    w8, [x1]
    ret

看出差別沒?

感想

最近嘗試寫了一些Rust代碼,發覺體驗真的不錯。按照Rust社區的說法,使用Rust後,可以無畏地編程,再也不用擔心記憶體錯誤與Data Race。
然而,對於大量使用C++實現的產品來說,C++是負債,更是資產。已經存在的C++生態很難向Rust進行遷移,Chrome也只是允許在三方庫中使用Rust代碼。
網上對於Rust與C++的爭論也是十分激烈。從我的角度來說,
  • C++的安全性演進是趨勢,但是未來很不明朗:C++在全世界有數十億行的存量代碼,期望C++在維持相容性的基礎上,提升記憶體安全性,這是一個幾乎不可能的任務。clang-format與clang-tidy,都提供了line-filter,來對特定的行進行檢查,避免出現改動一個老文件,需要把整個文件都重新format或者修改掉所有lint失敗的情況。大概也是基於此,Bjarne才會一直嘗試通過靜態分析+局部檢查來提升C++的記憶體安全性。
  • Rust有利於大團隊協作:Rust代碼只要能編譯通過,並且沒有使用unsafe特性,那麼是能夠保證絕對不會有記憶體安全性或者線程安全性問題的。這大大降低了編寫複雜代碼的心智負擔。然而,Rust的安全性是以犧牲語言表達力而獲得的,這對於團隊合作與代碼可讀性,可能是一件好事。至於其他,在沒有足夠的Rust實踐經驗前,我也無法作出更進一步的判斷。

Relax and enjoy coding!

參考資料:

1、Cpp core guidelines: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines

2、Chrome在安全方面的探索:https://docs.google.com/document/d/e/2PACX-1vRZr-HJcYmf2Y76DhewaiJOhRNpjGHCxliAQTBhFxzv1QTae9o8mhBmDl32CRIuaWZLt5kVeH9e9jXv/pub

3、NSA對C++的批評:https://www.theregister.com/2022/11/11/nsa_urges_orgs_to_use/

4、C++ Lifetime Profile: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#SS-lifetime

5、C++ Safety Profile: https://open-std.org/JTC1/SC22/WG21/docs/papers/2023/p2816r0.pdf?file=p2816r0.pdf

6、Herb CppCon2022 Slide: https://github.com/CppCon/CppCon2022/blob/main/Presentations/CppCon-2022-Sutter.pdf

7、Rust所有權模型理解:https://limpet.net/mbrubeck/2019/02/07/rust-a-unique-perspective.html

8、Rust無畏地併發編程:https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html
作者|吳強強(去鴻)

 

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/A-CPP-programmers-initial-experience-with-Rust.html


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

-Advertisement-
Play Games
更多相關文章
  • 3 月的碎碎念,大致總結了商業人生、付費軟體、創業方向選擇、創業感性還是理性、如何解決複雜問題及如何成長這幾個方面的內容。 商業人生 商業人生需要試錯能力和快速信息收集與驗證校準; 商業邏輯需要試錯能力,收集各種渠道信息後整理決策。快速信息收集和驗證校準很重要。 付費軟體 付費軟體產品可以依托大平臺 ...
  • 概述 背景 函數式編程的理論基礎是阿隆佐·丘奇(Alonzo Church)於 1930 年代提出的 λ 演算(Lambda Calculus)。λ 演算是一種形式系統,用於研究函數定義、函數應用和遞歸。它為計算理論和電腦科學的發展奠定了基礎。隨著 Haskell(1990年)和 Erlang(1 ...
  • 一門語言教程被搜索的次數越多,大家就會認為該語言越受歡迎。這是一個領先指標。原始數據來自谷歌Trends 如果您相信集體智慧,那麼流行編程語言排名可以幫助您決定學習哪門語言,或者在一個新的軟體項目中使用哪一門語言 ...
  • 請編寫一個程式,使用兩個線程分別輸出數字和字母,要求輸出的結果為:1A2B3C4D5E6F7G8H9I10J。 提示:可以使用Java中的wait()和notify()方法來實現線程間的通信。 public class NumberLetterPrinter { // 定義一個靜態的鎖對象 priv ...
  • 原文鏈接: Go 語言數組和切片的區別 在 Go 語言中,數組和切片看起來很像,但其實它們又有很多的不同之處,這篇文章就來說說它們到底有哪些不同。 另外,這個問題在面試中也經常會被問到,屬於入門級題目,看過文章之後,相信你會有一個很好的答案。 數組 數組是同一種數據類型元素的集合,數組在定義時需要指 ...
  • 流程式控制制 選擇結構(分支語句) ​ 因為switch只能匹配固定值,推薦使用if-else做條件篩選 if-else判斷 package main import "fmt" func main() { var tmpA int fmt.Scanln(&tmpA) if tmpA >= 90 { fm ...
  • 一 》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》 下載nacos https://github.com/alibaba/nacos nacos-server-2.0.3.zip Windows 版 解壓後,資料庫新建nacos庫,將 X:\nacos\ ...
  • 本文主要介紹在 Tomcat 集群中如何進行 Session 複製,文中所使用到的軟體版本:Centos 7.9.2009、Java 1.8.0_321、Tomcat 8.5.87。 1、快速配置 取消 conf/server.xml 文件中的以下註釋來啟用集群: <Cluster classNam ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...