EffectiveC++筆記 第5章

来源:https://www.cnblogs.com/1Kasshole/archive/2018/05/30/9108681.html
-Advertisement-
Play Games

我根據自己的理解,對原文的精華部分進行了提煉,併在一些難以理解的地方加上了自己的“可能比較準確”的「翻譯」。 Chapter 5 實現 Implementations 適當提出屬於你的class定義以及各種functions聲明相當花費心思。一旦正確完成它們,相應的實現大多直截了當。儘管如此,還是要 ...


我根據自己的理解,對原文的精華部分進行了提煉,併在一些難以理解的地方加上了自己的“可能比較準確”的「翻譯」。

Chapter 5 實現 Implementations

適當提出屬於你的class定義以及各種functions聲明相當花費心思。一旦正確完成它們,相應的實現大多直截了當。儘管如此,還是要小心很多細節。


條款26 : 儘可能延後變數定義式的出現時間

當你定義了一個變數,其類型帶有構造函數和析構函數,當程式控制流(control flow)到達此變數定義式時,你需要承擔構造成本;此變數離開作用域時,便需要承擔析構成本———即使你自始至終都沒有用過它。所以你應避免這種情況

你會問:怎麼可能定義「不被使用的變數」?下麵考慮一個函數,作用是計算通行密碼的加密版本後返回,但前提是密碼足夠長。若太短,函數會拋出異常,類型為logic_error

//此函數過早定義變數“encrypted”
std::string encryptPassword(const std::string& password)
{
    using namespace std;
    string encrypted;
    if(password.length()<MinimumPasswordLength){
        throw logic_error("Password is too short");
    }
    ... //諸如將加密後的密碼放進encrypted內的動作
}

這裡存在的問題是,如果拋出了異常,那encrypted就真的沒被使用———然而你還得付出構造和析構的成本。 看起來較好的解決方案是這樣的:

...
if(password.length()<MinimumPasswordLength){
throw logic_error("Password is too short");
}
string encrypted; //延後定義式,直到真正需要它
...

其實效率不夠高,因為encrypted雖獲定義卻無實參作初值。更好的做法是“直接在構造時指定初值”,這樣的效率高於default構造函數(構造對象再對它賦值)。<我們在條款4討論過效率問題>

現在我們一步一步進行分析。假設將函數encryptPassword的加密部分用 void encrypt(std::string& s); 實現,於是encryptPassword實現如下:

//此版本雖延後了定義,但仍效率低下:
std::string encryptPassword(const std::string& password)
{
    ...    //檢查length,同前
    std::string encrypted; //default constructor,無意義
    encrypted = password;
    encrypt(encrypted);
    return encrypted;
}

更受歡迎的做法是直接將password作為encrypted初值,跳過無意義預設構造:

std::string encrypted(password);

現在我們大概能理解「儘可能延後」的深層含義:你應嘗試延後這份變數定義直到能夠給它初值實參為止。

但遇到迴圈怎麼辦?若我們只在迴圈內用到變數,是該將它定義與迴圈外併在每次迴圈迭代賦值給它,還是將其定義於迴圈內? :

//A方案,定義於迴圈外:       //方法B,定義於迴圈內 :
Widget w;                  for(int i=0;i<n;++i){
for(int i=0;i<n;++i){          Widget w(表達式取決於i值);
    w = 表達式;(取決於i值)      ...
}                             }

首先看A和B做法的成本:

  • A: 1個構造函數 + 1個析構函數 + n個賦值操作
  • B: n個構造函數 + n個析構函數

我們可以理清:

  1. A的適用情況:
    class的一個賦值成本低於一組構造+析構成本 ;否則做法B較好

  2. 另外,A造成名稱w作用域大於B,有潛在對程式可理解性和易維護性的衝突。

結論

除非你知道賦值成本小於“析構+構造” ;
你正在處理代碼中對性能高度敏感(performance-sensitive)部分。
否則你該使用做法B。


條款27: 儘量少做轉型動作 Minimize casting

很不幸,轉型(casts)可能導致各種麻煩,有的顯而易見,有的非常隱晦。

讓我們複習一下轉型的語法:

  • C風格:
    (T)expression 將expression轉為T
  • 函數風格:
    T(expression) 將expression轉為T

它們並無差別,只是小括弧位置不同而已。我們可以稱這兩種為「舊式轉型」(old-style casts)。

C++還提供了四種新式轉型:

  1. const_cast
  2. dynamic_cast
  3. reinterpret_cast
  4. static_cast

各有不同用途:

  • const_cast通常用來將對象的常量性質除去(cast away the constness)(不是真正除去)。它是唯一有此能力的C++-style轉型操作符。

  • dynamic_cast主要用作“ 安全向下轉型 safe downcasting ”,能決定對象是否屬於繼承體系中某個類型。它是唯一無法用舊式語法執行的動作,並可能耗費大量運行成本。(後面會討論)

  • reinterpret_cast執行低級轉型,實際動作及結果取決於編譯器,這意味它不可移植。

  • static_cast用來強迫隱式轉換(implicit conversions)。例如將non-const對象轉為const對象,將int轉為double等。但將const轉為non-const只有const_cast做得到

新式轉型較受歡迎:

  1. 它們易被辨識(不論人工還是工具)
  2. 可以縮小轉型動作的選擇範圍。比如想去掉常量性(constness),只有const_cast能辦到

使用舊式轉型一般是調用explicit構造函數將對象傳給一個函數:

class Widget{
public:
    explicit Widget(int size);
    ...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15));       //函數式
doSomeWork(static_cast<Widget>(15));   //新式

使用第一種的原因可能是你覺得比較這樣自然,不像第二種蓄意的“生成對象”。但是為了以後代碼的可靠性,還是老老實實用新式轉型吧。

另外,C++的指針會產生偏移(offset)現象:

class Base { ... }
class Derived: public Base { ... };
Dervied d;
Base* pb = &d;      //隱式轉換

註意上方代碼,有時候引用d和指針pb兩個指針(d其實相當於常量指針)的值並不相同。這種情況下,在運行期間會有一個偏移量(offset)被施於Dervied* 指針身上,以獲取正確的Base*指針值。

上面的論述表明,單一對象(例如這裡的Derived對象)可能擁有一個以上的地址(例如對象“以Base* 指向它”時的地址和“以Derived*指向它”時的地址)。其它語言幾乎不可能出現這種情況,然而神奇的C++可以!!實際上C++碰到多重繼承,這事兒其實一直在發生———即使單一繼承也可能發生。

請註意了,對於偏移量(offset),對象佈局和它們的地址計算方式隨編譯器不同而不同————則意味著可能你設計的轉型在某平臺可用,在另一平臺不一定可用!

另一件關於轉型的有趣的問題:許多應用框架(application frameworks)要求dervied class內的virtual函數代碼第一語句即調用其base class的對應函數

我們很容易寫一些看起來很對的轉型代碼:

class Window{
public:
    virtual void onResize() {...} //基類onResize實現
    ...
};

class SpecialWindow: public Window{ //derived class
public:
    virtual void onResize(){
        static_cast<Window>(*this).onResize();
        /* 試圖將*this,即當前對象指針轉為父類,然後調用其
           onResize,not ok! */
        ...  //SpecialWindow專屬動作
    }
}; 

我們在代碼中用了新式轉型(實際上用舊式也是有問題的)。 一如你預期,程式確實將* this轉型為Window基類,對onResize的調用也因此調用了Window::onResize。但你一定沒想到,它調用的並不是當前對象的函數,而是轉型動作「早期」,程式建立的一個 ”this類型但只包含其base class Window成分的對象”*的暫時副本身上的onResize!

我們再理解一遍 :上述代碼並非「在當前對象身上調用Window::onResize後又在該對象身上執行SpecialWindow專屬動作」。不不不,它是「在”當前對象之base class成分”的副本上調用Window ::onResize」,然後才在當前對象上執行SpecialWindow專屬動作。會出現啥問題呢??假設onResize函數作用是改變對象的某內容,調用它時,首先轉型*this指針為Window然後調用Window的onResize,並對Window成分進行專屬操作。 但實際上此時調用的是「含有Window成分」對象副本的onResize,動作根本沒有落實到真正的base class成分上;但SpecialWindow的onResize會真的改動原對象!想象一下,這會使當前對象進入“傷殘”狀態

解決之道是拿掉轉型動作,別去哄騙編譯器將*this視為一個base class對象:

class SpecialWindow: public Window{
public:
    virtual void onResize(){
        Window::onResize(); //調用Window域的onResize作用於*this
        ...
    }
    ...
};

現在談談dynamic_cast。它的許多實現版本執行速度很慢。一個普遍的實現版本基於“class之字元串比較”,如果你在四層深的單繼承體系內某對象身上執行dynamic_cast,這個實現版本每層的一次dynamic_cast可能會耗用四次strcmp調用來比較class名稱!深度繼承和多重繼承成本更高。(有些版本為了必須實現的動態鏈接必須這麼做)但你應在註重效率的代碼中思量是否要使用dynamic_cast。

通常用到dynamic_cast的場景為:
你想在一個dervied class對象身上執行專屬於此之類自己的函數,但你只有一個base class類型的pointer或reference。

有兩個一般性做法可以避免窘境:

通過STL直接儲存指向dervied class對象的指針(通常為智能指針,見條款13)。

假設之前的Window/SpecialWindow繼承體系中,SpecialWindow有專屬函數void blink() ,不要這麼寫代碼:

class Window {...};
class SpecialWindow: public Window{
public:
    void blink();
    ...
};
typedef
std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
        iter != winPtrs.end();++iter){
    if(SpecialWindow* psw=dynamic_cast<SpecialWindow*>(iter->get()))    //dynamic_cast效率低下
    psw->blink();
}

應這麼寫:

typedef
std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for(VPSW::iterator iter = winPtrs.begin();
        iter!=winPtrs.end();++iter)
    (*iter)->blink();
//不用dynamic_cast的實現

當然,這種寫法會使你無法在一個容器內儲存「可指向所有base派生類」的指針。若確實需要處理多種類型,可能需要多個容器,他們必須具備類型安全性。

另一種做法可讓你通過base class介面處理所有base派生類,那就是在base里提供一個virtual函數。這類似Java里的抽象類:

class Window{
public:
    virtual void blink(){}
    //預設實現代碼「什麼也沒做」,交給子類實現。以後會告訴你這可能是餿主意
    ...
};
class SpecialWindow: public Window{
public:
    virtual void blink() {...};
    //子類的blink里做一些事。
    ...
};

typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;  //容器內含base類型指針
...
for(VPW::iterator iter = winPtrs.begin();
        iter!=winPtrs.end();++iter)
    (*iter)->blink();

上述兩種方法並非具有強大的普遍性,但是很多時候你應該以此替代dynamic_cast。

有一個你絕對,必須避免的東西:連串(cascading)dynamic_casts。也就是看起來像這樣的東西:

class Window {...};
...
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
        iter!=winPtrs.end();++iter)
{
    if(SpecialWindow1* pswl = 
        dynamic_cast<SpecialWindow1*>(iter->get())) {...}
    else if(SpecialWindow2* psw2 = 
        dynamic_cast<SpecialWindow2*>(iter->get())) {...}
    else if(SpecialWindow3* psw3 = 
        dynamic_cast<SpecialWindow3*>(iter->get())) {...}
    ...
}

這樣產生的代碼又腫又慢,基礎不穩。例如一旦加入新的dervied class,上述連串判斷可能就要加入新的分支。這樣的代碼應以“基於virtual函數調用”的東西取代。

優良的C++代碼很少使用轉型,但完全擺脫轉型操作不切實際。

最後,請記住:

  • 儘量避免轉型,特別是在註重效率的代碼中避免dynamic_cast。
  • 如果轉型必要,試著將它隱藏在某個函數後,客戶只需調用函數,而不用將具體實現加進他們的代碼里。
  • 寧用C++-style(新式)轉型。

條款28: 避免返回handles指向對象內部成分

Avoid returning “handles” to object internals

現在假設你的程式涉及矩形,每個矩形由左上角和右下角的點坐標確定。為了讓一個Rectangle對象儘可能小,你可能決定把定義的點放在輔助點struct內

class Point{ //此類表示“點”
public:
    Point(int x,int y);
    ...
    void setX(int newVal);
    void setY(int newVal);
    ...
};

struct RectData{ //這些“點”數據用來表現矩形
    Point ulhc;  //"upper left-hand comer"(左上角)
    Point lrhc;  //"lower right-hand comer"(右下角)
};

class Rectangle{
    ...
private:
    std::tr1::shared_ptr<RectData> pData;
};

使用Rectangle的客戶需要計算Rectangle範圍,所以此類提供upperLeft和lowerRight函數來返回左上角和右下角的坐標。根據條款20的討論,我們讓函數返回引用,代表底層的Point對象:

class Rectangle{
public:
    ...
    Point& upperLeft() const { return pData->ulhc; }
    Point& lowerRight() const { return pData->lrhc; }
    ...
}

這種設計有一個重大缺陷:雖然兩個函數被設計為const從而不能修改類成員函數,但是它所返回的reference卻可以直接指向private內部數據,例如:

Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1,coord2); //一個const矩形
rec.upperLeft().setX(50); //??一個const矩形的值竟然被改變了?

伙計,rec應該是不可變的啊!這給我們一個教訓:

成員變數的封裝性最多等於「返回其reference」的函數的訪問級別

如果類似的函數返回指針或迭代器的,相同的事情還是會發生。原因很簡單,references、pointers和迭代器統統是所謂的 handles(號碼牌,用來取得某對象)。所以返回一個“代表對象內部數據”的handle會帶來降低對象封裝性的風險。

之前的問題可以通過一個開頭的修飾符輕鬆解決:

const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }

這樣一來,返回的引用的許可權僅為“只讀”。

可即使如此,在其它場合可能還是會有問題。它可能導致_dangling handles(空懸的號碼牌);_也就是說handles所指物(的所屬對象)不復存在。這種問題常見來源是函數返回值。例如某函數返回GUI對象的外框,這個框採用矩形形式:

class GUIObject {...};
const Rectangle
    boundingBox(const GUIObject& obj);
//以by value形式返回矩形

現在客戶可能這麼使用此函數:

GUIObject* pgo;
... //讓pgo指向某個GUIObject
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
//取得一個指針指向外框左上角坐標

上述的操作中,boundingBox返回一個臨時的Rectangle副本對象,它沒有名稱,暫且稱它temp。隨後upperLeft作用於temp並返回一個reference指向temp內部的Point成分,然後指針pUpperLeft指向那個Point對象。然鵝。。。在語句結束後,temp將會被銷毀,間接導致temp內的Points析構,最終導致pUpperLeft指向不存在的對象。

這就是為啥函數返回一個handle總是危險的原因。但這不是說你絕不能讓成員函數返回handles,有時你必須這麼做。比如operator[]返回的引用允許你取得string對象或vector對象的個別元素。


條款29: 為“異常安全”而努力是值得的 Strive for exception-safe code.

假設有一個class表現帶背景圖案的GUI菜單。這個class希望用於多線程環境,所以它有個互斥器(mutex)作為併發控制(concurrency control)之用:

class PrettyMenu{
public:
    ...
    void changeBackground(std::istream& imgSrc); //修改菜單背景
    ...
private:
    Mutex mutex;  //互斥器
    Image* bgImage;  //目前的背景圖案
    int imageChanges;  //記錄背景被改次數
};
//下麵是changeBackground的可能實現

void PrettyMenu::changeBackground(std::istream& imgSrc){
    lock(&mutex);   //取得互斥器(見條款14)
    delete bgImage;  //擺脫舊背景圖案
    ++imageChanges;  //增加次數
    bgImage = new Image(imgSrc);  //安裝新背景
    unlock(&mutex); //釋放互斥器
}

從“異常安全性”的兩個條件來看,這個函數很糟:

  1. 不泄漏任何資源 一旦 new Image(imgSrc) 導致異常,對unlock的調用就不會執行,於是互斥器就永遠被把持住了。

  2. 不允許數據敗壞new Image(imgSrc) 拋出異常,bgImage即指向一個已被刪除的對象,imageChanges也被累加,然而其實沒有新圖像被成功安裝。
    - - - -
    解決資源泄漏很容易,以前的條款14曾討論過,導入Lock class作為一種「確保互斥器幾被及時釋放」的方法:

void PrettyMenu::changeBackground(std::istream& imgSrc){
    Lock ml(&mutex); //來自條款14
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
}

如Lock流的“資源管理類(resource management classes)”通常能使代碼更短。在上例的體現在於省去了unlock函數。
- - - -
接下來解決數據的敗壞。

首先,異常安全函數(exception-safe functions)提供以下三個保證之一:

  • 基本承諾: 一旦異常拋出,程式內任何事物仍保持在有效狀態下。沒有任何對象或數據結構因此敗壞,處於前後一致狀態。但現實狀態(exact state)不可預料,比如我們可以設計changeBackground為,一旦拋出異常,PrettyMenu可繼續持有原背景或賦給預設背景圖案。

  • 強烈保證: 一旦異常拋出,程式狀態不變。這樣的函數,只要成功,就完全成功;如果失敗,程式會恢復到“調用函數之前”狀態。

  • 不拋擲保證(nothrow): 承諾絕不拋出異常,因為它們總是能完成它們原先承諾的功能。 C++內置類型都能提供nothrow保證。

事實上最先考慮的應該是第三個保證,因為只有不能完全做到它我們才會考慮前兩個保證。

但是在C part of C++領域中很難完全不調用可能拋出異常的函數。任何使用動態記憶體的東西(比如STL),若記憶體不足以滿足需求,通常拋出bad_alloc異常(請見以後的條款49)。

所以只能說儘可能提供nothrow保證,但很多情況只能做到「基本保證」或「強烈保證」。

那我們如何為之前討論的changeBackground函數提供「強烈保證」呢?首先改變PrettyMenu的成員bgImage的類型,從Image*改為智能指針:

class PrettyMenu{
    ...
    std::tr1::shared_ptr<Image> bgImage;
    ...
};

void PrettyMenu::changeBackground(std::istream& imgSrc){
    Lock ml(&mutex);
    bgImage.reset(new Image(imgSrc));
    ++imageChanges;
    //不再需要手動delete,智能指針已經完成
}

這個操作幾乎足夠讓此函數提供「強烈保證」,美中不足的是參數imgSrc。比如若Image構造函數拋出異常,可能輸入流(input stream)的讀取記號(read marker)已被移走,而這種搬移對程式其餘部分是一種可見的狀態改變。

有一個設計策略叫做copy and swap,可以改善問題,原則是這樣的:

為打算修改的對象原件做一份副本,再在副本上作需要的修改。一旦有任何異常拋出,原件保持不變。若所有改動皆成功則將修改的副本和原件在一個不拋出異常的操作中置換(swap)

對於PrettyMenu寫法如下:

struct PMImpl{        //稍後說明為毛是結構體
    std::tr1::shared_ptr<Image> bgImage;
    int imageChanges;
};
class PrettyMenu{
    ...
private:
    Mutex mutex;
    std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc){
    using std::swap;    //條款25
    Lock ml(&mutex);    //獲得mutex副本數據
    std::tr1::shared_ptr<PMImpl>
        pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc)); //修改副本
    ++pNew->imageChanges;
    swap(pImpl,pNew); //置換同時釋放mutex
}

持續更新中................................


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

-Advertisement-
Play Games
更多相關文章
  • 簡介: 為其他對象提供一種代理以控制對這個對象的訪問。(在訪問真實對象時,引入一定程度的間接性,因為這種間接性可以附加多種用途)【結構型模式】 結構圖: 優點: 缺點: 應用場景: 註意事項: 示例: 1.結構類圖的實現: ISubject類,定義了RealSubject和Proxy的公用介面。在任 ...
  • 領域驅動設計(2)怎麼使用溝通 廢話 溝通的重要性:溝通很重要,不論在生活中,還是工作中溝通處理不好,我想為人處事這塊肯定有問題.LZ接觸社會比較早,做過焊工、銷售、跑過業務...,一路走來在溝通上同樣的也吃過很多的虧,受了不少的不會溝通的害處。我在做業務的時候常常用一句話告誡自己“一句話能死,一句 ...
  • 簡介: 動態的給一個對象添加一些額外的職責,就增加功能來說,裝飾模式比生產子類更加靈活——《大話設計模式》。【結構型模式】 結構圖: 優點: 缺點: 應用場景: 註意事項: 示例: 1.結構類的實現: Component定義一個對象介面,可以給這些對象動態的添加職責。 ConcreteCompone ...
  • 我們的對象並不只是存在記憶體中,還需要傳輸網路,或者保存起來下次再載入出來用,所以需要Java序列化技術。Java序列化技術正是將對象轉變成一串由二進位位元組組成的數組,可以通過將二進位數據保存到磁碟或者傳輸網路,磁碟或者網路接收者可以在對象的屬類的模板上來反序列化類的對象,達到對象持久化的目的。 如果 ...
  • Java開源生鮮電商平臺-系統報表設計與架構(源碼可下載) 說明:任何一個運行的平臺都需要一個很清楚的報表來顯示,那麼作為Java開源生鮮電商平臺而言,我們應該如何設計報表呢?或者說我們希望報表來看到什麼數據呢? 通過報表我們可以分析出目前整個公司的運營情況,以及下一步的調整方向,這樣更加有理有據的 ...
  • 1、通過getClass()方法:(通過對象) Class c1 = obj.getClass(); 2、通過對象實例方法獲取對象:(通過對象) Class c2= obj.class; 3、類的全路徑:(通過類名) Class c3 = Class.forName("package.classNa ...
  • Python源代碼如下: ...
  • 在上一篇文章中,我介紹了關於GC機制中,GC在確認垃圾對象後,是如何回收這些垃圾對象的幾種演算法。現在介紹下GC機制一般是如何定位(或者叫做標記)出這些垃圾對象的。我們先來問下自己,如何判介紹了斷一個對象可以被回收。答案很簡單也很直白。這個對象再也不會被調用到了 ,那麼就可以被回收了。那麼怎麼判斷一個 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...