《Effective C++》第三版-4. 設計與聲明(Design and Declarations)

来源:https://www.cnblogs.com/zjucys/p/18170141
-Advertisement-
Play Games

目錄條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)限制類型和值規定能做和不能做的事提供行為一致的介面條款19:設計class猶如設計type(Treat class de ...


目錄

條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)

限制類型和值

class Date {
public:
	Date(int month, int day, int year);  //可能月日年順序錯,可能傳遞無效的月份或日期
	...
};

可使用類型系統(type system)規避以上錯誤,即引入外覆類型(wrapper type)區別年月日:

struct Day {
explicite Day(int d)
	: val(d) { }
int val;
}
struct Month {
explicite Month(int m)
	: val(m) { }
int val;
}
struct Year{
explicite Year(int y)
	: val(y) { }
int val;
}

class Date {
public:
	Date(const Month& m, const Day& d, const Year& y);  //可能月日年順序錯,可能傳遞無效的月份或日期
	...
};
Date d(Month(3), Day(30), Year(1995));  //可有效防止介面誤用

保證了類型正確之後,需要保證輸入的值有效:

class Month {
public:
	static Month Jan() { return Month(1); }
	static Month Feb() { return Month(2); }
	...
	static Month Dec() { return Month(12); }
	...
private:
	explicit Month(int m);
	...
};
Date d(Month::Mar(), Day(30), Year(1995));

規定能做和不能做的事

if ( a * b = c) ...  //以const修飾操作符*,使其不能被賦值

提供行為一致的介面

為了避免忘記刪除或者重覆刪除指針,可令工廠函數直接返回智能指針:

Investment* createInvestment(); //用戶可能忘記刪除或者重覆刪除指針
std::tr1::shared_ptr<Investment> createInvestment();

若期望用自定義的getRidOfInvestment,則需要避免誤用delete,可考慮將getRidOfInvestment綁定為刪除器(deleter):

刪除器在引用次數為0時調用,故可創建一個null shared_ptr

std::tr1::shared_ptr<Investment> createInvestment()
{
	std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), 
																					getRidOfInvestment);  //創建一個null shared_ptr
	retVal = ... ;  //令retVal指向目標對象
	return retVal;
}

若pInv管理的原始指針能在pInv創立之前確定下來,則將原始指針直接傳遞給pInv的構造函數更好

tr1::shared_ptr會自動使用每個指針專屬的刪除器,從而無須擔心cross-DLL problem:

cross-DLL problem:對象在動態連接程式庫(DLL)中被new創建,但在另一個DLL內被delete銷毀

//返回的tr1::shared_ptr可能被傳遞給任何其他DLL
//其會追蹤記錄從而在引用次數為0時調用那個DLL的delete
std::tr1:;shared_ptr<Investment> createInvestment()
{
	return std::tr1::shared_ptr<Investment>(new Stock);
}

Boost的tr1::shared_ptr特點:

  • 是原始指針的兩倍大
  • 以動態分配記憶體作為簿記用途和刪除器的專屬數據
  • 以virtual形式調用刪除器
  • 在多線程程式修改引用次數時有線程同步化(thread synchronization)的額外開銷

Tips:

  • 好的介面不易被誤用
  • 促進正確使用的方法包括介面一致性和與內置類型的行為相容
  • 阻止誤用的辦法包括建立新類型、限制類型上的操作、束縛對象值、消除客戶的資源管理責任
  • tr1::shared_ptr支持定製型刪除器(custom deleter),這可以防範DLL問題,可被用來自動解除互斥鎖(mutexes)等

條款19:設計class猶如設計type(Treat class design as type design)

定義一個新class時也就定義了一個新type。設計高效的類需要考慮以下問題:

  • 新type的對象應如何創建和銷毀(第8章))
    • 影響構造函數和析構函數、記憶體分配函數和釋放函數(operator new,operator new [],operator delete,operator delete [])
  • 對象的初始化和賦值應有什麼差別(條款4)
    • 決定構造函數和賦值操作符的行為
  • 新type的對象如果被pass-by-value意味著什麼
    • 由copy構造函數定義pass-by-value如何實現
  • 什麼是新type的合法值
    • 有效的數值集決定了類必須維護的約束條件(invariants),
      • 進而決定了成員函數(特別是構造函數、析構函數、setter函數)的錯誤檢查
    • 還影響函數拋出的異常和極少使用的函數異常明細列(exception specifications)
  • 新type需要配合某個繼承圖系(inheritance graph)嗎
    • 繼承既有的類,則受那些類束縛,尤其要考慮那些類的函數是否為虛函數
    • 被其他類繼承,則影響析構函數等是否為virtual
  • 新type需要什麼樣的轉換
    • 若允許類型T1隱式轉換為類型T2,可可考慮:
      • 在T1類內寫類型轉換函數(operator T2)
      • 在T2類內些non-explicit-one-argument(可被單一實參調用)的構造函數
      • 若只允許explicit構造函數存在,就得寫專門執行轉換的函數,且沒有類型轉換操作符(type conversion operators)或non-explicit-one-argument構造函數
  • 什麼樣的操作符和函數對於此新type合理
    • 決定需要聲明哪些函數,其中哪些是成員函數
  • 什麼樣的標準函數應駁回
    • 這些必須聲明為private
  • 誰改取用新type的成員
    • 影響public、private、protected的選擇
    • 影響友元類、友元函數、及其嵌套的設計
  • 什麼是新type的未聲明介面(undeclared interface)
    • 要考慮其對效率、異常安全性、資源運用的保證
  • 新type有多麼一般化
    • 若要定義整個type家族,則應該定義新的class template
  • 是否真的需要新type
    • 若定義新的派生類就足夠,則可能定義non-member函數或templates更好

Tips:

  • Class設計就是type設計,需要考慮以上所有問題

條款20:寧以pass-by-reference-to-const替換pass-by-value(Prefer pass-by-reference-to-cons to pass-by-value)

避免構造和析構

class Person {
public:
	Person();
	virtual ~Person();
	...
private:
	std::string name;
	std::string address;
};
class Student: public Person {
public:
	Student();
	~Student();
	...
private:
	std::string schoolName;
	std::string schoolAddress;
};
bool validateStudent(Student s);  //會調用六次構造函數和六次析構函數
bool validateStudent(const Student& s);  //效率提升很多

上述代碼validateStudent函數中pass-by-value會調用六次構造函數和六次析構函數:

  • Student構造+Person構造+Student的2個string+Person的2個string
  • 析構同理

使用pass-by-reference可避免頻繁構造和析構

避免對象切割

對象切割(slicing):派生類以值傳遞並被視為基類對象時,回調用基類的構造函數,而派生類的成分全無

class Window {
public:
	...
	std::string name() const;  //返回視窗名稱
	virtual void display() const;  //顯示視窗和其內容
};
class WindowWithScrollBars: public Window {
public:
	...
	virtual void display() const;
};

void printNameAndDisply(Window w)
{
	std::cout << w,name();
	w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisply(wwsb);  //對象會被切割,因為參數w時Window對象,故調用的Window::display
void printNameAndDisply(const Window& w)  //不會被切割
{
	std::cout << w,name();
	w.display();
}

例外

  • 內置類型和STL的迭代器與函數對象採用pass by value往往效率更高,
  • 小型type不一定適合pass by value
    • 一旦需要複製指針的所指物,則copy構造函數可能成本很高
  • 即使小型對象的copy構造函數不昂貴,其效率也存在爭議
    • 某些編譯器對內置類型和自定義類型的態度截然不同,即使二者底層表示(underlying representation)相同
    • 如可能會把一個double放入緩存器,而只包含一個double的對象則不會
    • by reference則肯定把指針放入緩存器
  • 用戶自定義類型的大小容易變化,因其內部實現可能改變,故不一定適合pass by value
    • 某些標準程式庫實現版本中的string類型比其他版本大七倍

Tips:

  • 儘量以pass-by-reference-to-const替換pass-by-value。前者通常高效且能避免切割問題
  • 以上規則並不適用內置類型和STL的迭代和與函數對象,它們更適合pass-by-value

條款21:必須返回對象時,別妄想返回其reference(Don’t try to return a reference when you must return an object)

考慮有理數乘積:

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1);
	...
private:
	int n, d;  //分子和分母
	friend const Rational operator* (const Rational& lhs, const Rational& rhs);
};

上述代碼中操作符以by value的方式返回值,如果要返回reference則操作符必須自己創建新Rational對象,其途徑有二:在stack或heap空間創建(反例)

//返回local對象的引用,但是local對象在離開函數時就銷毀了
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
	Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
	return result;
}

//難以對new創建的對象delete,尤其以下連乘的例子
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
	Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
	return *result;
}
//無法取得引用背後的指針
Rational w, x, y, z;
w = x * y * z;  //operator*(operator*(x, y), z)

若使用static Rational避免調用構造函數,則會有如下問題:

//返回local對象的引用,但是local對象在離開函數時就銷毀了
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
	static Rational result;
	result = ... ;
	return result;
}
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
...
if ((a * b) == (c * d)) { ... }  //==總是為true
else { ... }  //,因兩側是同一個同一個stetic Rational對象的引用

必須返回新對象的函數的正確寫法為:

inline const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

Tips:

  • 絕不要返回指針或引用指向local stack對象
  • 絕對不要返回引用指向heap-allocated對象
  • 絕對不要在有可能同時需要多個這樣的對象時返回指針或引用指向local static對象

條款22:成員變數聲明為private(Declare data members private)

這一條似乎沒啥內容_

把成員變數聲明為private的原因如下:

  • 介面的一致性:非public的成員函數只能通過函數訪問,並且可以方便的設置讀寫許可權
  • 封裝
    • 把成員變數隱藏在函數介面背後,可以方便地更改實現方式
    • public成員變數修改後所有使用它們的客戶碼都會被破壞
    • protected成員變數修改後所有使用它們的派生類都會被破壞,其並不比public更具有封裝性

Tips:

  • 切記把成員變數聲明為private,這個賦予訪問數據的一致性、可細微劃分訪問控制、保證約束條件、提供充分的實現彈性
  • protected並不比public更具封裝性

條款23:寧以non-member、non-friend替換member函數(Prefer non-member non-friend functions to member functions)

考慮有個類表示網頁瀏覽器:

class WebBrowser {
public:
	...
	void clearCache();
	void clearHistory();
	void removeCookies();
	void clearEverything();  //執行所有清除動作
	...
};

執行所有清除動作由兩個方案:

  • WebBrowser 提供函數
  • 由non-member函數調用相應的member函數
    • 封裝性更高,且包裹彈性(packaging flexibility)較大,編譯相依度較低,是更好的方案
//WebBrowser 提供函數
class WebBrowser {
public:
	...
	void clearEverything();  //執行所有清除動作
	...
};
//由non-member函數調用相應的member函數
void clearBrowser(WebBrowser& wb)
{
	wb.clearCache();
	wb.clearHistory();
	wb.removeCookies();
}

兩點註意事項:

  • 準確地說,封裝性良好的是non-member non-friend函數,而非non-member函數
  • 一個類的non-member non-friend函數可以是可以是另一個類的member
    • 有些語言的函數必須定義在類內(如Eiffel,Java,C#),可以令clearBrowser成為某個工具類(utility class)的一個static member函數,而非WebBrowser的一部分或friend
    • 在C++中可讓clearWebBrowser成為non-member函數且和WebBrowser位於同一命名空間
namespace WebBrowserStuff {
	Class WebBrowser { ... };
	void clearBrowser(WebBrowser& wb);
	...
}

命名空間能跨越多個源碼文件而類不能,故可將同一命名空間下不同功能類型的函數放在不同的頭文件:

標準程式庫也不是一個龐大的單一頭文件,而是有若幹個頭文件,每個頭文件聲明std的某些功能,這樣可以使得用戶只依賴所使用的一小部分系統

//頭文件webbrowser.h,包含WebBrowser自身和核心功能
namespace WebBrowserStuff {
class WebBrowser { ... };
	...  //核心功能,如廣泛使用的non-member函數
}
//頭文件webbrowserbookmarks.h,
namespace WebBrowserStuff {
	...  ////與書簽相關的函數
}
//頭文件webbrowsercookies.h,
namespace WebBrowserStuff {
	...  //與cookie相關的函數
}

Tips:

  • 寧可拿non-member non-friend函數替換member函數,以增肌封裝性、包裹彈性、功能擴展性

條款24:若所有參數皆需類型轉換,請為此採用non-member函數(Declare non-member functions when type conversions should apply to all parameters)

考慮有理數類:

class Rational {
public:
	Rational(int numerator = 0,    //構造函數刻意不為explicit
					 int denominator = 1);  //允許int到Rational的隱式轉換
	int numerator() const;  //分子和分母的訪問函數
	int denominator() const;
private:
	...
};

若操作符*為Rational的成員函數:

class Rational {
public:
	...
	const Rational operator* (const Rational& rhs) const;
};
Rational oneHalf(1, 2);
Rational result = oneHalf * 2;  //正確,發生了隱式類型轉換,根據int創建了Rational
result = oneHalf.operator*(2);  //但如果是explicit構造函數則錯誤

result = 2 * oneHalf;  //錯誤!
result = 2.operator*(oneHalf);  //錯誤!重寫上式,錯誤一目瞭然

result = operator*(2, oneHalf);  //錯誤!本例不存在接受int和Rational的操作符*

只有參數位於參數列內,這個參數才能隱式類型轉換

要支持混合運算,則可讓操作符*成為non-member函數:

const Rational operator*(const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.numerator() * rhs.numerator(),
									lhs.denominator() * rhs.denominator());

member函數的反面是non-member函數,而非friend函數

從Objected-Oriented C++轉換到Template C++且Rational是一個class template時,本條款需要考慮新的問題

Tips:

  • 若需要為某個函數的所有參數(包括this指針所指的那個隱喻參數)進行類型轉換,那這個函數必須是non-member

條款25:考慮寫出一個不拋異常的swap函數(Consider support for a non-throwing swap)

預設的swap

預設情況下swap動作可由標準程式庫提供的swap演算法完成:

namespace std {
	template<typename T>  //只要T支持copying即可實現swap
	void swap(T& a, T& b)
	{
		T temp(a);
		a = b;
		b = temp;
	}
}

特化的swap

預設的swap涉及三個對象的複製,而pimpl手法(pointer to implementation)可避免這些複製:

置換兩個Widget對象值只需要置換其pImpl指針;而預設的swap會複製三個Widget,並且複製三個WidgetImpl對象

class Widget {
public:
	Widget(const Widget& rhs);
	Widget& operator=(const Widget& rhs)  //複製Widget時,就複製WidgetImpl對象
	{
		...
		*pImpl = *(ths.pImpl);
		...
	}
	...
private:
	WidgetImpl* pImpl;  //所指對象內涵Widget數據
};

將std::swap針對Widget特化可解決上述問題:

令Widget聲明public swap成員函數做真正的置換工作(採用成員函數是為了取用private pImpl,non-member函數則不行),再把std::swap特化

class Widget {
public:
	...
	void swap(Widget& other)
	{
		using std::swap;  //這個聲明有必要,稍後解釋
		swap(pImpl, other.pImpl);  //真正做置換工作,
	}
	...
};
namespace std {
	template<>  //表示其是std::swap的全特化(total template specialization)版本
	void swap<Widget>(Widget& a, Widget& b)
	{
		a.swap(b);  //要置換WIdget就調用其swap成員函數
	}
}

上述代碼與STL容器有一致性,因為所有STL容器也都提供有public swap成員函數和std::特化版本(以調用成員函數)

若Widget和WidgetImpl都是class template,可考慮把WidgetImpl內的數據類型參數化:

template<typename T>
class WidgetImpl { ... };
template<typename T>
calss Widget { ... };

此時特化std::swap會遇到問題:

//以下代碼企圖偏特化(partially specialize)一個function template(std::swap)
//但C++只允許對class template偏特化
//故無法通過編譯(雖然少數編譯器錯誤地通過編譯)
namespace std {
	template<typename T>
	void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
	{ a.swap(b);}
}

//偏特化function template時,通常會添加重載版本
//但以下代碼也不合法,因為std不能添加新的templates,這由C++彼岸準委員會決定
namespace std {
	template<typename T>
	void swap(Widget<T>& a, Widget<T>& b)  //註意swap之後沒有<...>
	{ a.swap(b); }
}

解決方案:聲明一個non-memebr swap以調用member swap,但不再將non-member swap聲明為std::swap的特化版本或重載版本

任何代碼如果要置換兩個Widget對象而調用swap,則C++的名稱查找法則(name lookup rules;更具體地說是argument-dependent lookup或Koeing lookup法則)會找到WidgetStuff內的Widget專屬版本

namespace WidgetStuff {  //為簡化,把Widget相關功能都放入WidgetStuff命名空間內
	...
	template<typename T>
	class Widget { ... };
	...
	template<typename T>
	void swap(Widget<T>& a, Widget<T>& b)  //non-member swap函數,不屬於std命名空間
	{
		a.swap(b);
	}
}

若想要class專屬版的swap在儘可能多的語境下被調用,則需呀在該class所在的命名空間內寫一個non-member版本和一個std::特化版本,故應該為該class特化std::swap

若希望調用T專屬版本,並且在該版本不存在的情況下調用std內的一般化版本,可實現如下:

C++的名稱查找法則確保會找到global作用域或T所在的命名空間內的任何T專屬的swap;若沒有專屬swap則using聲明使得能夠調用std::swap

template<typename T>
void doSomething(T& obj1, T& obj2)
{
	using std::swap;  //令std::swap在此函數內可用
	...
	swap)obj1, obj2);  //為T調用最佳swap版本
	...
}

std::swap(obj1, obj2);  //錯誤的方式!強迫編譯器調用std::swap

使用swap的總結

swap的使用總結如下:

  1. 若預設的swap的效率可接受,則無需做額外的事
  2. 若預設的swap效率不足,則可考慮:
    1. 提供public swap成員函數,使其置換相應類型的兩個對象值,且絕不拋出異常
    2. 在class或template所在的命名空間內提供一個non-member swap,並調用上述swap成員函數
    3. 若正在編寫class(而非class template),則特化std::swap並使其調用swap成員函數
  3. 若調用swap,則需要包含using聲明式,使std::swap在函數內可見,之後不加namespace直接調用swap

成員版swap絕不可拋出異常,其最好的應用是幫助class或class template提供強烈的異常安全性(exception-safety)保障

Tips:

  • 當std::效率不高時,提供一個swap成員函數,並確保其不拋出異常
  • 如果提供一個member swap,則要提供一個non-member swap調用前者。對於class(而非template),也最好特化std::swap
  • 調用swap時應聲明 using std:;swap,之後不帶命名空間修飾地調用swap
  • 為自定義類型進行std template全特化可以,但是不要再std內加入新東西`

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

-Advertisement-
Play Games
更多相關文章
  • 本文源代碼:https://gitee.com/obullxl/PythonCS/tree/master/CS-CY2405 Python中*和**很常見 禪師在閱讀Python代碼過程中,經常看到一個函數或方法的入參是*args和**kwargs(如:def func(*args, **kwarg ...
  • 服務的提供者和消費者 服務之間可以通過Spring提供的RestTemplate來進行http請求去請求另一個Springboot的項目,這就叫做服務間的遠程調用。 當一個服務通過遠程調用去調用另一個服務時,被調用的服務就叫做服務的提供者,調用服務的服務就叫做服務的消費者。 一個服務可以既是服務的提 ...
  • 概述:拷貝並交換(Copy-and-Swap)是C++編程中的慣用法,用於實現賦值操作符和確保異常安全的拷貝構造函數。其核心思想是通過拷貝構造函數創建臨時副本,再通過交換確保操作的異常安全性。這種方法在C++11之前和之後都適用,但在C++11中,移動語義和右值引用的引入使得實現可以更加高效。 拷貝 ...
  • 在日常的企業經營和個人生活中,我們難免會遇到大量的增值稅發票,這些發票包括普票、專票、全電發票、捲票等各種形式,這些發票不僅記錄了我們的購買和銷售信息,還是我們報銷和審計的重要依據。然而,手動處理這些發票數據是一項繁瑣而耗時的工作。為瞭解決這個問題,現在有了一個非常方便的工具——增值稅發票識別 AP ...
  • #!/usr/bin/env python import this Python之禪: The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple i ...
  • 概述:在C++中,遍歷由空格分隔的字元串的單詞有多種方法,包括使用`std::istringstream`、手動遍歷字元和正則表達式。其中,`std::istringstream`是簡單高效的選擇,通過流提取單詞。手動遍歷字元較為繁瑣,正則表達式方法更靈活但可能有性能開銷。根據實際需求選擇方法,本文 ...
  • 1、BOM對象 BOM:Broswer object model,即瀏覽器提供我們開發者在javascript用於操作瀏覽器的對象。 1.1、window對象 視窗方法 // BOM Browser object model 瀏覽器對象模型 // js中最大的一個對象.整個瀏覽器視窗出現的所有東西都 ...
  • title: 從零開始:Django項目的創建與配置指南 date: 2024/5/2 18:29:33 updated: 2024/5/2 18:29:33 categories: 後端開發 tags: Django WebDev Python ORM Security Deployment Op ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...