Inheritance, Polymorphism, and Library Contracts
Learning Objectives
- Master Solidity's inheritance mechanism and polymorphism
- Understand the C3 linearization algorithm
- Master the definition and usage of library contracts
Inheritance Basics
Inheritance Definition
- Use the
iskeyword - Inheritance is implemented by code copying: it becomes a single contract after deployment
Visibility and Inheritance
- private: not visible to child contracts, but cannot define members with the same name
- internal: visible to child contracts
- public: fully visible
- Event and modifier name clashes also cause conflicts
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract Base {
string private name;
event MyEvent();
modifier myMod() {
_;
}
function foo() private {}
}
contract ContractA is Base {
string private name; // private can have the same name (not visible but conflicts exist)
// function foo() private {} // even if private, cannot have the same name
}
Constructor and Inheritance
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
abstract contract Base {
constructor(string memory _name) {}
}
contract ContractA is Base {
string name;
constructor() Base(name) {}
}
Polymorphism
virtual and override
- Parent contract functions marked
virtualcan be overridden - Child contract functions use
overrideto override - Abstract contracts can have unimplemented functions
Override Rules
- If different versions of the same function are inherited from the inheritance path, it must be overridden
overrideneeds to list all overridden parent contracts
C3 Linearization
Why Linearization is Needed
- In multiple inheritance, the inheritance relationships between contracts form a graph structure
- Need to determine: storage layout stacking order, constructor execution order, and what
superpoints to
Linearization Rules
- Parent before child
- Sibling before sibling (traverse from right to left)
- Deterministic result
Algorithm Description
- Start from the current contract, find parent contracts, traversing from right to left
- If all child contracts of a certain contract have been traversed, that contract is enqueued; otherwise, backtrack to find siblings
- End traversal at the root contract, output and reverse
Constructor Execution Order = Linearization Order
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract A {
uint public x = 5;
constructor() {}
}
contract B is A {
constructor() {
x += 10;
}
}
contract C is A {
constructor() {
x *= 10;
}
}
contract D is C, B {
// Linearization: D → B → C → A
// Constructor order: A → C → B → D
// Calculation process: x=5 → x*=10=50 → x+=10=60
// Final x = 60
}
super Keyword
superpoints to the predecessor contract in the linearization sequence (not the direct parent contract)
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;
contract A {
event Log(string message);
function bar() public virtual {
emit Log("A.bar called");
}
}
contract B is A {
function bar() public virtual override {
emit Log("B.bar called");
super.bar(); // Calls C.bar() (not A.bar!)
}
}
contract C is A {
function bar() public virtual override {
emit Log("C.bar called");
super.bar(); // Calls A.bar()
}
}
contract D is B, C {
// Linearization: D → C → B → A
function bar() public override(B, C) {
super.bar();
}
// Call order: D.bar → C.bar → B.bar → A.bar
}
Recommendations for Multiple Inheritance
- Use inheritance sparingly, prefer composition + interfaces
- However, composition in Solidity is constrained by technical limitations
- Do not abuse inheritance
Application Cases
- ERC721: NFT standard, basic functionalities
- ERC721URIStorage: Binding mapping between tokens and external resources
- ERC721Enumerable: Traversal capability
Library Contracts
Definition and Constraints
- Defined using the
librarykeyword - Cannot have member variables, cannot hold Ether, cannot inherit, cannot selfdestruct
- Cannot have a constructor
library LibraryName {
// Can define struct, enum, constant
function f() internal pure {
// Function implementation
}
}
Calling Methods
Direct Call
import "LibraryName.sol";
LibraryName.functionName(args);
using Syntax Sugar
using LibraryName for Type;
// Afterwards, it can be called in the form of value.functionName()
// Equivalent to LibraryName.functionName(value)
Example: SafeMath and Math Libraries
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
library SafeMath {
function add(uint x, uint y) internal pure returns (uint) {
uint z = x + y;
require(z >= x, "uint overflow");
return z;
}
}
library Math {
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
}
contract TestSafeMath {
using Math for uint;
uint public MAX_UINT = 2 ** 256 - 1;
function testAdd(uint x, uint y) public pure returns (uint) {
return SafeMath.add(x, y); // Direct call
}
function testSquareRoot(uint x) public pure returns (uint) {
return x.sqrt(); // using syntax sugar
}
}
Example: Array Library
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
library Array {
function remove(uint[] storage arr, uint index) public {
require(arr.length > 0, "Can't remove from empty array");
arr[index] = arr[arr.length - 1];
arr.pop();
}
}
contract TestArray {
uint[] public arr;
function testArrayRemove() public {
for (uint i = 0; i < 3; i++) {
arr.push(i);
}
Array.remove(arr, 1);
assert(arr.length == 2);
assert(arr[0] == 0);
assert(arr[1] == 2);
}
}
Internal Mechanisms of Libraries
- internal functions: inlined into the caller's code (Embedded Library), no separate deployment needed
- public functions: the library is deployed as a separate contract and called via delegatecall (Linked Library)
- Comparison with Proxy Patterns:
- Proxy pattern: delegatecall borrows code by ensuring compatible storage layout
- Library contract: delegatecall borrows code by passing storage parameters
- Library linking is done at compile time, cannot be dynamically switched at runtime (does not support upgrades)
Structs and Mappings in Libraries
A special capability of Libraries: public/external functions can accept storage parameters (which regular contracts cannot). This allows mappings to be passed as parameters.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
library TransferOperation {
function transfer(
address from,
address to,
uint amount,
mapping(address => uint) storage balanceOf
) public {
uint current = balanceOf[from];
require(current >= amount, "not enough balance!");
uint toc = balanceOf[to];
current -= amount;
toc += amount;
balanceOf[from] = current;
balanceOf[to] = toc;
}
}
// Define and operate on struct in library
pragma solidity >=0.7.0 <0.9.0;
library LibraryShape {
struct Rectangle {
uint width;
uint height;
}
function area(Rectangle storage s) public view returns (uint) {
return s.width * s.height;
}
}
contract Draw {
using LibraryShape for LibraryShape.Rectangle;
LibraryShape.Rectangle shape;
function getArea() public view returns (uint) {
return shape.area(); // using syntax sugar
}
}
Comprehensive Example: IterableMapping
Solves the problem of non-iterable mappings by implementing an iterable mapping through a library.
Key design points:
- Data is stored in the array starting from index 1 (because the index of an unassigned value defaults to 0)
- Array data is not deleted (no pop), but marked as deleted
- When deleting a value in a mapping,
deletemust be used (all bits become 0)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
library IterableMapping {
struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}
struct IndexValue {
uint keyIndex;
uint value;
}
struct KeyFlag {
uint key;
bool deleted;
}
function insert(itmap storage self, uint key, uint value) public returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0) return true;
else {
uint index = self.keys.length;
self.keys.push(KeyFlag(key, false));
self.data[key].keyIndex = index + 1; // Start from 1
self.keys[index].key = key;
self.size++;
return false;
}
}
function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0) return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size--;
return true;
}
function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}
function iterate_start(itmap storage self) internal view returns (uint) {
uint keyIndex = 0;
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return keyIndex;
}
function iterate_valid(itmap storage self, uint keyIndex) internal view returns (bool) {
return keyIndex < self.keys.length;
}
function iterate_next(itmap storage self, uint keyIndex) internal view returns (uint) {
keyIndex++;
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return keyIndex;
}
function iterate_get(itmap storage self, uint keyIndex) internal view returns (uint key, uint value) {
key = self.keys[keyIndex].key;
value = self.data[key].value;
}
}
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/7500