cryptozombiesでSolidityについて学ぶ part2
はじめに
引き続きcryptozombiesでSolidityの基礎を学んでいきます。
前回の記事はこちら。
msg
Solidityでは全ての関数で利用できるグローバル関数があり、msgはその一つです。msgには複数のプロパティがありますが、最も一般的に使用されるのはmsg.senderとmsg.valueです。
- msg.sender
現在のコントラクトを呼び出したアカウントのアドレスを指します。スマートコントラクトの関数を呼び出した際に、トランザクションの発行元のアドレスを取得できます。
pragma solidity ^0.8.29;
contract Example {
address public owner;
constructor() {
// デプロイ時のコントラクト作成者を保存
owner = msg.sender;
}
function getSender() public view returns (address) {
// 関数を実行したアカウントのアドレスを返す
return msg.sender;
}
}
この場合、msg.sender
は Caller コントラクトのアドレスになり、Caller を実行した外部のアドレスではないので注意が必要です。
contract Caller {
function callOther(Example example) public view returns (address) {
// ここで msg.senderはCallerコントラクトのアドレスになる
return example.getSender();
}
}
- msg.value
msg.value
は、トランザクションとともに送信されたイーサリアムの量(wei単位) を示します。これは、スマートコントラクトが受け取るイーサリアムの量を取得するときに使います。
pragma solidity ^0.8.29;
contract Example {
// イーサリアムを受け取る関数では payable 修飾子が必要(なければETHの送金が失敗する)。
function pay() public payable {
// 送信されたETHの量を取得
uint256 amount = msg.value;
}
}
ちなみにイーサリアムにはよく使われる単位が3つあり、それぞれの特徴はこちらです。
wei
- イーサリアムの最小単位。
-
msg.value
のデフォルトの単位(wei単位でETHが扱われます)。 - 1ETH = 10^-18 wei
gwei
- ガス代(手数料)で最もよく使われます。
- 1ETH = 10^-9 gwei
ETH
- 一般的な取引や残高表示で使われます。
エラーハンドリング assert, require, revert, try/catch
Solidityのエラーハンドリングは主に4つあります。
assert
assert
は内部エラーや不変条件のチェックに使用されます。
- 条件がfalseの場合、例外をスローしてコントラクトの実行を停止します。
- 主に重大なエラーや予期せぬ状態を検出するために使用されます。(ユーザー入力のバリデーションに使用するのは
require
) - assetが失敗すると、ガズ代が全額没収され未使用分も没収されます
例
function safeMath(uint256 a, uint256 b) public pure returns (uint256) {
uint256 result = a + b;
// a + bがaより小さくなるのはあり得ないバグなのでassertでチェックする
assert(result >= a);
return result;
}
require
require
は入力値や条件のチェックに使用されます。
- 外部からの入力チェックやアクセス制御 などに使われる。
- カスタムエラーメッセージも表示できます。
- ガス効率が良く、エラー時に未使用のガス代を呼び出し元に返金します。
例
function setAge(uint256 _age) public {
// ageが20歳未満なら場合リバートして処理を中断する
require(_age >= 20, "20歳以上でないといけません。");
age = _age;
}
revert
revert
は条件分岐の中で明示的にエラーを発生させる場面で使います。
-
require
同様 カスタムエラーメッセージも表示できます。 - requireの代わりにif文と組み合わせることによって複雑な条件分岐の中でエラーを発生させることができます。
例
function withdraw(uint256 amount) public {
if (amount > balance) {
// カスタムエラーメッセージ
revert("残高不足です");
}
balance -= amount;
}
継承について
Solidityでは、コントラクト間の継承をサポートしています。継承を使用することで、コードの再利用性を高め、より効率的なコントラクト開発が可能になります。
基本的な継承
is
キーワードを使ってコントラクトを継承できます。
contract ParentContract {
// 親コントラクトの内容
}
contract ChildContract is ParentContract {
// 子コントラクトの内容
}
この場合、ChildContract
は ParentContract
から継承し、その public、external、internal の関数とステート変数を使用できます。
関数のオーバーライド
子コントラクトで親コントラクトの関数をオーバーライドすることも可能です。
contract Parent {
function greet() public pure virtual returns (string memory) {
return "親 hello";
}
}
contract Child is Parent {
function greet() public pure override returns (string memory) {
return "子 hello";
}
}
virtual
修飾子をつけるとオーバーライド可能な関数になり、override
修飾子を使うことで親の関数を上書きできます。
複数の継承
複数の親コントラクトを継承することも可能です。
contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}
contract B {
function bar() public pure returns (string memory) {
return "B";
}
}
// CはAとBの両方を継承
// Aのfoo関数をオーバーライド
contract C is A, B {
function foo() public pure override returns (string memory) {
return "C";
}
}
ただ、複数の継承で同じ関数が存在すると競合が発生し、コンパイルエラーになります。ダイヤモンド継承問題といい共通の祖先を持つ親クラスがいると、どの親のメソッドを継承すべきかが曖昧になる問題のことです。
なので、override(A, B)
のように親コントラクトの優先順位を明示する必要があります。override
は左側の親よりも右側の親が優先されます。
contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}
contract B is A {
function foo() public pure virtual override returns (string memory) {
return "B";
}
}
// override(A, B)を入れないとコンパイルエラーが発生する
// CはAとBを継承するが、foo() は B を優先してオーバーライド。
contract C is A, B {
function foo() public pure override(A, B) returns (string memory) {
return "C";
}
}
抽象コントラクト
抽象コントラクトは、少なくとも1つの未実装の関数を含むコントラクトです。
abstract
キーワードはv0.8.xから必須ではないものの、未定義のコントラクトであることを明示的に表すために使うのが推奨されています。
// 抽象コントラクトの例
abstract contract MyContract {
// このままだとSolidityは未実装の関数を持つコントラクトを不完全だとみなし、デプロイを拒否します。
function greet() public view virtual returns (string memory);
}
abstract
を利用した場合、このコントラクトを継承先のコントラクトで関数を定義する必要があります。
abstract contract MyContract {
function greet() public view virtual returns (string memory);
}
contract MyDerivedContract is MyContract {
function greet() public view override returns (string memory) {
return "Hello!";
}
}
これでデプロイ可能になります。
importについて
他のファイルにあるコードをimport
を使って読み込むことができ、これはコードの再利用や分割に便利です。
書き方はJavascriptと似ておりますが、デフォルトエクスポートをしていなかったり、拡張子の省略はせず.sol
を明示的にするのが一般的です。
// すべての内容をインポート
import "./MyContract.sol";
// 特定のコントラクトをインポート
import {SpecificContract} from "./OtherContract.sol";
// 名前の衝突を避けるためにエイリアスを使用する
import {OtherContract as OC} from "./OtherContract.sol";
// 外部ライブラリのインポート
import "@openzeppelin/contracts/access/AccessControl.sol";
storageとmemory
storage
はブロックチェーンに永続的に保存される変数のことで、memory
は一時的な変数であり、関数の実行が完了すると削除されます。
基本的にはSolidityが自動で判断してくれますが配列や構造体、文字列などの可変長データに関してはstorage
かmemory
かを明示的に記載する必要があります。
contract Example {
function getMessage() public pure returns (string memory) {
string memory message = "Hello, Solidity!"; // `memory` に保存
return message;
}
}
interface
Solidityのinterface
は、スマートコントラクト間の相互作用を可能にする重要な機能です。また、抽象コントラクトと似ていますが、いかなる関数も実装できません。
interface
の特徴はこちらです。
- 関数の宣言のみを含み、実装を持つことはできません
- 他のインターフェースを継承することができます
- 宣言される関数はすべてexternalでなければなりません
- コンストラクタや状態変数を宣言することはできません
// インターフェースの定義
interface IExample {
function getValue() external view returns (uint256);
}
// interface はアドレスを通じて 既存のコントラクトにアクセス できる
contract Caller {
function fetchValue(address exampleContract) public view returns (uint256) {
IExample example = IExample(exampleContract);
return example.getValue();
}
}
interface
を使う大きい理由としては、他のコントラクトの関数を安全に使えるようになるということです**。**(call
メソッドを使用しても他のコントラクトの関数を直接呼ぶことはできますが、型安全性がなくコンパイル時にエラー検知できないため特定なケース以外は使わない方が良いみたいです。)
複数の返り値の処理
Soliditでは関数の戻り値として複数の値を返すことが可能です。
これにより、1回の関数呼び出しで複数のデータを取得でき、ガスコストの削減やコードの可読性向上につながります。
contract MultiReturnExample {
// returns (uint256, bool, string memory)のように複数の型を指定する。
// return (値1, 値2, 値3);でタプルとして返す。
function getValues() public pure returns (uint256, bool, string memory) {
return (42, true, "Hello Solidity");
}
}
contract Caller {
MultiReturnExample example = new MultiReturnExample();
function callGetValues() public view returns (uint256, bool, string memory) {
// 呼び出し側で複数の戻り値を受け取る方法
(uint256 num, bool flag, string memory message) = example.getValues();
return (num, flag, message);
}
function callGetValues() public view returns (uint256, string memory) {
// 一部の戻り値を無視することも可能
(uint256 num, , string memory message) = example.getValues();
return (num, message);
}
}
条件分岐
Solidityに他の言語同様条件分岐する方法はいくつかあるので紹介します。
if / else / else if
書き方はJavascriptとほとんど同じです。ただし、ifに入る値はbool型で他の型入れることができません。
contract ConditionExample {
function checkNumber(uint256 _num) public pure returns (memory string) {
// bool型を入れる。10を入れたりするのは不可能
if (_num > 10) {
return "10より大きいです";
} else if (_num == 10) {
return "10です";
} else {
return "10より小さいです";
}
}
}
// 早期リターンも可能
contract EarlyReturnExample {
function checkNumber(int256 _num) public pure returns (string memory) {
if (_num < 0) return "負の数";
if (_num == 0) return "ゼロ";
return "正の数";
}
}
※ 文字列比較について
Solidityでは、string
型の直接比較 (if (str1 == str2)
) はできません、そのためkeccak256
を使ってハッシュ値を比較することで比較が可能になります。
contract StringComparison {
function compareStrings(string memory str1, string memory str2) public pure returns (bool) {
return keccak256(abi.encodePacked(str1)) == keccak256(abi.encodePacked(str2));
}
}
三項演算子
Javascriptと同様に三項演算子も利用できます。
contract TernaryExample {
function checkEvenOrOdd(uint256 _num) public pure returns (string memory) {
return (_num % 2 == 0) ? "偶数" : "奇数";
}
}
まとめ
条件分岐やimportに関してはJavascriptに似ている構文が多いので覚えやすいですね。ただstorageとmemoryの概念やinterfaceを使った関数呼び出しなど特殊なものも多いのでしっかり覚えて使いこなせるようになりたいです。
次はpart3やっていきます。ここまで読んできただきありがとうございました。👏
Discussion