區塊鏈100講:Solidity語法的過載,繼承的定義
1
摘要
以太坊智慧合約語言Solitidy是一種面向物件的語言,本文結合面嚮物件語言的特性,講清楚Solitidy語言的多型(Polymorphism)(重寫,過載),繼承(Inheritance)等特性。
2
合約說明
Solidity 合約類似於面嚮物件語言中的類。合約中有用於資料持久化的狀態變數,和可以修改狀態變數的函式。 呼叫另一個合約例項的函式時,會執行一個 EVM 函式呼叫,這個操作會切換執行時的上下文,這樣,前一個合約的狀態變數就不能訪問了。
面向物件(Object Oriented,OO)語言有3大特性:封裝,繼承,多型,Solidity語言也具有著3中特性。
面嚮物件語言3大特性的說明解釋如下:
-
封裝(Encapsulation)
封裝,就是把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。一個類就是一個封裝了資料以及操作這些資料的程式碼的邏輯實體。在一個物件內部,某些程式碼或某些資料可以是私有的,不能被外界訪問。通過這種方式,物件對內部資料提供了不同級別的保護,以防止程式中無關的部分意外的改變或錯誤的使用了物件的私有部分。
-
繼承(Inheritance)
繼承,指可以讓某個型別的物件獲得另一個型別的物件的屬性的方法。它支援按級分類的概念。繼承是指這樣一種能力:它可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴充套件。 通過繼承建立的新類稱為“子類”或“派生類”,被繼承的類稱為“基類”、“父類”或“超類”。繼承的過程,就是從一般到特殊的過程。要實現繼承,可以通過 “繼承”(Inheritance)和“組合”(Composition)來實現。繼承概念的實現方式有二類:實現繼承與介面繼承。實現繼承是指直接使用 基類的屬性和方法而無需額外編碼的能力;介面繼承是指僅使用屬性和方法的名稱、但是子類必須提供實現的能力。
-
多型(Polymorphism)
多型,是指一個類例項的相同方法在不同情形有不同表現形式。多型機制使具有不同內部結構的物件可以共享相同的外部介面。這意味著,雖然針對不同物件的具體操作不同,但通過一個公共的類,它們(那些操作)可以通過相同的方式予以呼叫。
另外也解釋一下過載和重寫。
過載(Override)是多型的一種形式,是一個類的內部,方法中多個引數,根據入參的個數不同,會返回不同的結果。
重寫(Overwrited),是子類繼承父類,重寫父類的方法。多型性是允許你將父物件設定成為一個或更多的他的子物件相等的技術,賦值之後,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。簡單的說,就是一句話:允許將子類型別的指標賦值給父類型別的指標。多型性在Object Pascal和C++中都是通過虛擬函式的。
3
函式過載(Override)
合約可以具有多個不同引數的同名函式。這也適用於繼承函式。以下示例展示了合約 A 中的過載函式 f。
pragma solidity ^0.4.16;
contract A {
function f(uint _in) public pure returns (uint out) {
out = 1;
}
function f(uint _in, bytes32 _key) public pure returns (uint out) {
out = 2;
}
}
過載函式也存在於外部介面中。如果兩個外部可見函式僅區別於 Solidity 內的型別而不是它們的外部型別則會導致錯誤。
// 以下程式碼無法編譯
pragma solidity ^0.4.16;
contract A {
function f(B _in) public pure returns (B out) {
out = _in;
}
function f(address _in) public pure returns (address out) {
out = _in;
}
}
contract B {
}
以上兩個 f 函式過載都接受了 ABI 的地址型別,雖然它們在 Solidity 中被認為是不同的。
3.1 過載解析和引數匹配
通過將當前範圍內的函式宣告與函式呼叫中提供的引數相匹配,可以選擇過載函式。 如果所有引數都可以隱式地轉換為預期型別,則選擇函式作為過載候選項。如果一個候選都沒有,解析失敗。
pragma solidity ^0.4.16;
contract A {
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}
呼叫 f(50) 會導致型別錯誤,因為 50 既可以被隱式轉換為 uint8 也可以被隱式轉換為 uint256。 另一方面,呼叫 f(256) 則會解析為 f(uint256) 過載,因為 256 不能隱式轉換為 uint8。
註解:返回引數不作為過載解析的依據。
4
繼承
通過複製包括多型的程式碼,Solidity 支援多重繼承。
所有的函式呼叫都是虛擬的,這意味著最遠的派生函式會被呼叫,除非明確給出合約名稱。
當一個合約從多個合約繼承時,在區塊鏈上只有一個合約被建立,所有基類合約的程式碼被複制到建立的合約中。
總的來說,Solidity 的繼承系統與 Python的繼承系統 ,非常 相似,特別是多重繼承方面。
下面的例子進行了詳細的說明。
pragma solidity ^0.4.16;
contract owned {
function owned() { owner = msg.sender;}
address owner;
}
// 使用 is 從另一個合約派生。派生合約可以訪問所有非私有成員,包括內部函式和狀態變數,
// 但無法通過 this 來外部訪問。
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}
// 這些抽象合約僅用於給編譯器提供介面。
// 注意函式沒有函式體。// 如果一個合約沒有實現所有函式,則只能用作介面。
contract Config {
function lookup(uint id) public returns (address adr);
}
contract NameReg {
function register(bytes32 name) public;
function unregister() public;
}
// 可以多重繼承。請注意,owned 也是 mortal 的基類,
// 但只有一個 owned 例項(就像 C++ 中的虛擬繼承)。
contract named is owned, mortal {
function named(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// 函式可以被另一個具有相同名稱和相同數量/型別輸入的函式過載。
// 如果過載函式有不同型別的輸出引數,會導致錯誤。
// 本地和基於訊息的函式呼叫都會考慮這些過載。
function kill() public {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// 仍然可以呼叫特定的過載函式。
mortal.kill();
}
}
}
// 如果建構函式接受引數,
// 則需要在宣告(合約的建構函式)時提供,
// 或在派生合約的建構函式位置以修飾器呼叫風格提供(見下文)。
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
function get() public view returns(uint r) { return info; }
uint info;
}
注意,在上邊的程式碼中,我們呼叫 mortal.kill() 來“轉發”銷燬請求。 這樣做法是有問題的,在下面的例子中可以看到:
pragma solidity ^0.4.0;
contract owned {
function owned() public { owner = msg.sender;}
address owner;
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public { /* 清除操作 1 */ mortal.kill(); }
}
contract Base2 is mortal {
function kill() public { /* 清除操作 2 */ mortal.kill(); }
}
contract Final is Base1, Base2 {
}
呼叫 Final.kill() 時會呼叫最遠的派生過載函式 Base2.kill,但是會繞過 Base1.kill, 主要是因為它甚至都不知道 Base1 的存在。解決這個問題的方法是使用 super:
pragma solidity ^0.4.0;
contract owned {
function owned() public { owner = msg.sender; }
address owner;
}
contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is mortal {
function kill() public { /* 清除操作 1 */ super.kill(); }
}
contract Base2 is mortal {
function kill() public { /* 清除操作 2 */ super.kill(); }
}
contract Final is Base1, Base2 {
}
如果 Base2 呼叫 super 的函式,它不會簡單在其基類合約上呼叫該函式。 相反,它在最終的繼承關係圖譜的下一個基類合約中呼叫這個函式,所以它會呼叫 Base1.kill() (注意最終的繼承序列是——從最遠派生合約開始:Final, Base2, Base1, mortal, ownerd)。 在類中使用 super 呼叫的實際函式在當前類的上下文中是未知的,儘管它的型別是已知的。 這與普通的虛擬方法查詢類似。
4.1 基類建構函式的引數
派生合約需要提供基類建構函式需要的所有引數。這可以通過兩種方式來完成:
pragma solidity ^0.4.0;
contract Base {
uint x;
function Base(uint _x) public { x = _x; }
}
contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) public {
}
}
一種方法直接在繼承列表中呼叫基類建構函式(is Base(7))。 另一種方法是像 修飾器modifier 使用方法一樣, 作為派生合約建構函式定義頭的一部分,(Base(_y * _y))。 如果建構函式引數是常量並且定義或描述了合約的行為,使用第一種方法比較方便。 如果基類建構函式的引數依賴於派生合約,那麼必須使用第二種方法。 如果像這個簡單的例子一樣,兩個地方都用到了,優先使用 修飾器modifier 風格的引數。
4.2 多重繼承與線性化
程式語言實現多重繼承需要解決幾個問題。 一個問題是 鑽石問題。 Solidity 借鑑了 Python 的方式並且使用“ C3 線性化 ”強制一個由基類構成的 DAG(有向無環圖)保持一個特定的順序。 這最終反映為我們所希望的唯一化的結果,但也使某些繼承方式變為無效。尤其是,基類在 is 後面的順序很重要。 在下面的程式碼中,Solidity 會給出“ Linearization of inheritance graph impossible ”這樣的錯誤。
// 以下程式碼編譯出錯
pragma solidity ^0.4.0;
contract X {}
contract A is X {}
contract C is A, X {}
程式碼編譯出錯的原因是 C 要求 X 重寫 A (因為定義的順序是 A, X ), 但是 A 本身要求重寫 X,無法解決這種衝突。
可以通過一個簡單的規則來記憶: 以從“最接近的基類”(most base-like)到“最遠的繼承”(most derived)的順序來指定所有的基類。
4.3 繼承有相同名字的不同型別成員
當繼承導致一個合約具有相同名字的函式和 修飾器modifier 時,這會被認為是一個錯誤。 當事件和 修飾器modifier 同名,或者函式和事件同名時,同樣會被認為是一個錯誤。 有一種例外情況,狀態變數的 getter 可以覆蓋一個 public 函式。
本文作者:HiBlock區塊鏈社群技術佈道者輝哥
原文釋出於簡書
以下是我們的社群介紹,歡迎各種合作、交流、學習:)