Solidityを学び始めた方が最初に戸惑う部分の一つが、データ型と変数のスコープです。JavaScriptやPythonなどの動的型付け言語に慣れていると、Solidityの厳格な型システムは新鮮に感じるかもしれません。
しかし、スマートコントラクトはブロックチェーン上に永続するプログラムであり、型の不一致やスコープの誤解がセキュリティ上の脆弱性やガスの無駄遣いにつながります。型システムをしっかり理解することが、安全で効率的なコントラクト開発への近道です。
本記事では、Solidityの主要なデータ型の特徴と使い分け、変数スコープの違い、よくある落とし穴まで体系的に解説します。コード例を交えながら説明しますので、手元のRemix IDEで試しながら読み進めてみてください。
値型(Value Types)の種類と使い方
整数型:uint と int
Solidityの整数型には符号なし整数 uint と符号あり整数 int があります。それぞれ8〜256ビットの範囲で指定でき、デフォルトは256ビット(uint256, int256)です。
uint8: 0〜255uint16: 0〜65535uint256: 0〜2^256-1(最大の符号なし整数)int8: -128〜127int256: -2^255〜2^255-1
トークン残高や価格はほとんどの場合 uint256 を使います。なお、Solidity 0.8.0以降はオーバーフロー・アンダーフロー発生時に自動でリバートするため、0.7以前と比べて安全性が向上しています。
より小さいビット幅(uint8など)はストレージのパッキングに使う場合に有効ですが、EVM内部の計算ではuint256に拡張されるため、単独での使用では必ずしもガスが節約されるわけではありません。
アドレス型:address と address payable
address 型はイーサリアムのウォレットアドレスやコントラクトアドレスを格納する20バイトの型です。ETHを送金する場合は address payable を使う必要があります。
address public owner;
address payable public treasury;
function setOwner(address _newOwner) public {
owner = _newOwner;
}
function deposit() public payable {
treasury = payable(msg.sender);
}
address 型には .balance(残高)・.code(コントラクトのバイトコード)・.codehash などのプロパティがあります。address payable にはさらに .transfer() と .send() が使えます。ただし現在は低レベル呼び出し .call{value: amount}("") の使用が推奨されています。
参照型(Reference Types)の理解
配列(array)の種類と操作
Solidityには固定長配列と動的配列があります。ストレージに保存するか、メモリ内で使用するかでも挙動が変わります。
// 固定長配列
uint256[5] public fixedArray;
// 動的配列
uint256[] public dynamicArray;
function addElement(uint256 _val) public {
dynamicArray.push(_val);
}
function getLength() public view returns (uint256) {
return dynamicArray.length;
}
function removeLastElement() public {
dynamicArray.pop();
}
配列をメモリで扱う場合(関数内など)は、new キーワードで初期化が必要です。動的配列のストレージ書き込みはガスが高いため、必要最小限にとどめることがガス最適化の基本です。
マッピング(mapping)の仕組み
mapping はキーと値の対応関係を保持するデータ構造です。EVM内部ではハッシュテーブルとして実装されており、全キーの列挙はできません。
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
function getBalance(address _account) public view returns (uint256) {
return balances[_account];
}
存在しないキーへのアクセスはゼロ値(uint256なら0、boolならfalse)を返します。マッピングはストレージにのみ存在でき、メモリやcalldataには直接使えません。全キーを追跡したい場合は別途配列を用意するパターンが一般的です。
変数スコープの3種類
ステート変数(State Variables)
コントラクト本体に宣言された変数はステート変数と呼ばれ、ブロックチェーンのストレージに永続的に保存されます。読み取り(SLOAD)・書き込み(SSTORE)ともにガスコストが高く、特にゼロから非ゼロへの書き込みは最もコストが高い操作の一つです。
contract Counter {
uint256 public count; // ステート変数
function increment() public {
count += 1;
}
}
ステート変数の可視性は public(自動ゲッター生成)・internal(デフォルト)・private から選択できます。private でもブロックチェーン上のデータは閲覧可能なため、秘密情報の格納には適していません。
ローカル変数とメモリ
関数内で宣言された変数はローカル変数です。基本的にはスタックまたはメモリに格納され、関数終了後に消えます。参照型(配列・struct・string・bytes)をローカルで使う場合は、memory または calldata キーワードを明示する必要があります。
function processArray(uint256[] calldata _input) public pure returns (uint256) {
uint256 total = 0; // ローカル変数(スタック)
for (uint256 i = 0; i < _input.length; i++) {
total += _input[i];
}
return total;
}
calldata はトランザクションで渡されたデータを直接参照するため、memory へのコピーが不要でガスが節約できます。外部関数の引数には calldata、内部処理では memory を使うのが基本です。
グローバル変数(ブロック・トランザクション情報)
Solidityには特別なグローバル変数が用意されており、実行コンテキストの情報を取得できます。
msg.sender: 関数を呼び出したアドレスmsg.value: 送られてきたETHの量(wei単位)msg.data: 呼び出しデータ全体block.timestamp: 現在のブロックのタイムスタンプ(UNIX時間)block.number: 現在のブロック番号tx.origin: オリジナルの送信者(フィッシング攻撃に悪用されやすいため使用注意)
block.timestamp はバリデーターによってわずかに操作できるため、厳密なタイミング依存のロジックには使わないことが推奨されています。
構造体(struct)の活用
structの定義と使い方
複数の関連するデータをまとめるには struct が便利です。
struct Proposal {
string description;
uint256 voteCount;
bool executed;
address proposer;
}
Proposal[] public proposals;
function createProposal(string calldata _desc) public {
proposals.push(Proposal({
description: _desc,
voteCount: 0,
executed: false,
proposer: msg.sender
}));
}
structはストレージ・メモリ・calldataのいずれにも格納できます。ストレージのstructを更新する場合は、storage 参照を使うとコピーを避けられます。
structのストレージレイアウトとガス最適化
EVMのストレージスロットは32バイト(256ビット)単位です。structのフィールドが同じスロットにパッキングできる場合、複数の小さい型を並べることでスロット数を削減できます。
例えば uint128 2つは1スロットに収まりますが、uint256 と uint128 を交互に並べると2スロット必要になります。フィールドの順序を意識して、小さい型をまとめて宣言するとガス効率が向上します。
型変換と型キャスト
暗黙的・明示的な型変換
Solidityでは小さい型から大きい型への変換は暗黙的に行われますが、逆方向(uint256→uint8など)には明示的キャストが必要です。情報の欠落が発生する可能性があるため注意が必要です。
uint256 large = 1000;
uint8 small = uint8(large); // 232になる(1000 % 256)
address addr = 0x...;
address payable payableAddr = payable(addr); // address → address payable
数値と文字列の相互変換には標準ライブラリがないため、OpenZeppelinのStringsライブラリやカスタム関数を使います。
bytes と string の使い分け
bytes は任意のバイト列、string はUTF-8でエンコードされた文字列です。Solidity上で文字列の比較や操作はコストが高いため、識別子にはハッシュ(keccak256(abi.encodePacked(str)))を使うのが一般的です。
固定長バイト型(bytes32など)は値型として扱われ、ストレージ効率が高いです。ラベルやIDを格納する場合は bytes32 が有用です。
enumの使い方
状態管理へのenum活用
enum は名前付きの定数集合で、コントラクトの状態管理に最適です。
enum Status { Pending, Active, Completed, Cancelled }
Status public currentStatus;
function activate() public {
require(currentStatus == Status.Pending, "Not in pending status");
currentStatus = Status.Active;
}
enumの内部表現はuint8であり、0から順に割り当てられます。状態機械(State Machine)パターンを実装する際にenumを使うと、コードの可読性が大幅に向上します。
enumのABIエンコードと注意点
enumはABIでuint8としてエンコードされます。コントラクトをアップグレードする場合、enumの順序を変えると既存データの意味が変わるため注意が必要です。また、enumの値の範囲外へのキャストはリバートします(Solidity 0.8.0以降)。
まとめ
Solidityのデータ型と変数スコープを正しく理解することは、安全で効率的なスマートコントラクト開発の土台です。値型・参照型・グローバル変数の違いを把握し、ストレージ・メモリ・calldataの使い分けを意識することで、ガスコストの削減とコードの安全性向上につながります。
特にマッピングと配列の組み合わせ、structのストレージレイアウト、型キャストの落とし穴は初心者が誤りやすいポイントです。Remix IDEで実際にコードを書いて挙動を確認しながら習得していくことをお勧めします。
よくある質問
Q. uint と uint256 は同じですか?
はい、同じです。uint は uint256 の別名(エイリアス)です。同様に int は int256 の別名です。コードの可読性のため、明示的に uint256 と書くスタイルが主流です。
Q. mapping のキーに使える型は何ですか?
値型(uint, address, bytes32など)とstring・bytes が使えます。参照型(配列・struct)はキーに使えません。値型であれば任意のビット幅の型が使用可能です。
Q. storage と memory はどう使い分ければよいですか?
ブロックチェーンに保存する必要があるデータはstorage(ステート変数)に、関数内の一時的な処理にはmemoryを使います。外部関数の引数でコピーが不要な場合はcalldataが最もガス効率が良い選択です。
※本記事は情報提供を目的としており、投資を推奨するものではありません。暗号資産への投資は元本割れのリスクがあります。投資判断はご自身の責任で行ってください。