[Solidityのはじめ方] Part13: マッピングと構造体

Solidityで複雑なデータを効率的に扱う方法を学びましょう。

はじめに

スマートコントラクトでは、様々なデータを管理する必要があります。ユーザーの情報、トークンの残高、アイテムの属性など、データは多岐にわたります。Solidityには、これらのデータを効率的に整理・管理するための強力な機能として「構造体(Struct)」と「マッピング(Mapping)」が用意されています。

これらを使いこなすことで、より複雑で実用的なスマートコントラクトを構築できるようになります。このセクションでは、構造体とマッピングの基本から、それらを組み合わせた応用までを学んでいきましょう!

構造体 (Struct)

構造体は、関連する複数の異なるデータ型の変数を一つにまとめることができるユーザー定義のデータ型です。例えば、ユーザー情報を管理する場合、「名前(string)」「年齢(uint)」「アドレス(address)」などをひとまとめにして扱えると便利ですよね。構造体はまさにそのためにあります。

定義方法

構造体は struct キーワードを使って定義します。


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract StructExample {

    // ユーザー情報を格納する構造体 `User` を定義
    struct User {
        string name;
        uint age;
        address userAddress;
        bool isActive;
    }

    // アイテム情報を格納する構造体 `Item` を定義
    struct Item {
        string itemName;
        uint price;
        address owner;
    }

    // 構造体型の変数を宣言
    User public adminUser;
    Item public sampleItem;

    constructor() {
        // 構造体のインスタンスを作成し、初期値を設定
        adminUser = User({
            name: "Alice",
            age: 30,
            userAddress: msg.sender, // コントラクトをデプロイした人のアドレス
            isActive: true
        });

        sampleItem = Item("Magic Sword", 100, address(this)); // コントラクト自身のアドレス
    }

    // 構造体のメンバーにアクセスする関数
    function getUserName() public view returns (string memory) {
        return adminUser.name;
    }

    function getItemPrice() public view returns (uint) {
        return sampleItem.price;
    }

    // 構造体を引数や戻り値として使うことも可能
    function createUser(string memory _name, uint _age, address _userAddress) public pure returns (User memory) {
        return User({
            name: _name,
            age: _age,
            userAddress: _userAddress,
            isActive: true
        });
    }
}
    

上記の例では、UserItem という2つの構造体を定義しています。それぞれの構造体は、関連するデータ(名前、年齢など)をメンバーとして持っています。

使い方

  • 定義: struct 構造体名 { メンバーの型 メンバー名; ... } の形式で定義します。
  • 変数宣言: 構造体名 アクセス修飾子 変数名; のように宣言します。
  • インスタンス化と初期化: 変数名 = 構造体名({メンバー名1: 値1, メンバー名2: 値2, ...}); のように初期化します。順番通りであれば 変数名 = 構造体名(値1, 値2, ...); と書くこともできますが、メンバー名を指定する方が可読性が高く推奨されます。
  • メンバーへのアクセス: 変数名.メンバー名 の形式で、構造体の各メンバーにアクセスできます。
ポイント: 構造体を使うことで、関連するデータをまとめて管理でき、コードの可読性と保守性が向上します。

マッピング (Mapping)

マッピングは、キーと値のペアを格納するためのデータ構造です。他のプログラミング言語におけるハッシュテーブルや辞書(Dictionary)に似ています。特定のキーを指定すると、それに対応する値を効率的に取得できます。

Solidityのマッピングは、特に「アドレス」をキーとして「残高」や「ユーザー情報」などを紐付ける際によく利用されます。

定義方法

マッピングは mapping(キーの型 => 値の型) の形式で定義します。


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MappingExample {

    // アドレスをキーとし、uint型(例: 残高)を値とするマッピング
    mapping(address => uint) public balances;

    // uint型(例: ユーザーID)をキーとし、string型(例: ユーザー名)を値とするマッピング
    mapping(uint => string) public userNames;

    // アドレスをキーとし、bool型(例: ホワイトリスト登録状況)を値とするマッピング
    mapping(address => bool) public isWhitelisted;

    constructor() {
        // デプロイ時に初期値を設定
        balances[msg.sender] = 1000; // デプロイ者の残高を1000に設定
        userNames[1] = "Bob";
        isWhitelisted[msg.sender] = true;
    }

    // 指定したアドレスの残高を取得する関数
    function getBalance(address _user) public view returns (uint) {
        return balances[_user]; // キーを指定して値を取得
    }

    // 残高を更新する関数
    function updateBalance(address _user, uint _amount) public {
        balances[_user] = _amount; // キーを指定して値を設定(または更新)
    }

    // 指定したIDのユーザー名を取得する関数
    function getUserNameById(uint _id) public view returns (string memory) {
        return userNames[_id];
    }

    // アドレスをホワイトリストに追加する関数
    function addToWhitelist(address _user) public {
        isWhitelisted[_user] = true;
    }
}
    

使い方

  • 定義: mapping(キーの型 => 値の型) アクセス修飾子 変数名; の形式で定義します。
  • 値の設定・更新: 変数名[キー] = 値; の形式で、指定したキーに対応する値を設定または更新します。
  • 値の取得: 変数名[キー] の形式で、指定したキーに対応する値を取得します。
  • 初期値: マッピングにキーが存在しない場合、値の型のデフォルト値(例: uintなら0, boolならfalse, addressなら`address(0)`)が返されます。
注意点: Solidityのマッピングは、キーや値のリストを取得したり、ループ処理で全ての要素を順番に処理したりすることはできません。マッピングのサイズ(要素数)を取得することも直接的にはできません。

マッピングと構造体の組み合わせ

マッピングと構造体は、組み合わせて使うことで真価を発揮します。例えば、「ユーザーアドレス」をキーとして、そのユーザーの詳細情報(名前、年齢、アクティブ状況など)を格納したい場合、マッピングの値の型として構造体を利用できます。

組み合わせ例


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CombinedExample {

    // ユーザー情報を格納する構造体
    struct UserProfile {
        string name;
        uint age;
        bool isActive;
        uint registrationDate;
    }

    // アドレスをキーとし、UserProfile構造体を値とするマッピング
    mapping(address => UserProfile) public userProfiles;

    // 新しいユーザー情報を登録する関数
    function registerUser(string memory _name, uint _age) public {
        address senderAddress = msg.sender;

        // 既に登録されていないかチェック(任意)
        require(!userProfiles[senderAddress].isActive, "User already registered.");

        // マッピングに構造体のインスタンスを格納
        userProfiles[senderAddress] = UserProfile({
            name: _name,
            age: _age,
            isActive: true,
            registrationDate: block.timestamp // 現在のブロックのタイムスタンプ
        });
    }

    // 自分のユーザー情報を取得する関数
    function getMyProfile() public view returns (string memory name, uint age, bool isActive, uint registrationDate) {
        address senderAddress = msg.sender;
        UserProfile storage profile = userProfiles[senderAddress];
        return (profile.name, profile.age, profile.isActive, profile.registrationDate);
    }

    // 特定のアドレスのユーザー情報を取得する関数
    function getUserProfile(address _userAddress) public view returns (UserProfile memory) {
        // 存在しないアドレスを指定した場合、デフォルト値が返る
        return userProfiles[_userAddress];
    }

    // ユーザー情報を非アクティブ化する関数
    function deactivateUser() public {
        address senderAddress = msg.sender;
        // ユーザーが存在するかチェック
        require(userProfiles[senderAddress].isActive, "User not found or already inactive.");
        userProfiles[senderAddress].isActive = false;
    }
}
    

この例では、mapping(address => UserProfile) により、各アドレスに紐づく詳細なユーザープロファイル(UserProfile構造体)を管理しています。これにより、アドレスひとつで関連する複数の情報を効率的に扱えるようになります。

実践的なユースケース

  • ERC20トークンコントラクト: mapping(address => uint256) private _balances; で各アドレスのトークン残高を管理します。
  • NFTコントラクト: mapping(uint256 => address) private _owners; で各トークンIDの所有者アドレスを管理します。
  • 投票システム: mapping(address => Voter) public voters; で投票者の情報(投票済みか、どの候補に投票したかなど)を管理します。
  • アクセス制御: mapping(address => bool) public isAdmin; で特定のアドレスが管理者権限を持つか管理します。

注意点とベストプラクティス

  • マッピングの初期値: マッピングにキーが存在しない場合、値はその型のデフォルト値(0, false, address(0) など)を返します。値が存在しないことと、値がデフォルト値であることの区別が必要な場合は、構造体内にフラグ(例: bool isSet;)を持たせるなどの工夫が必要です。
  • ガス効率:
    • 構造体やマッピングの読み書きはストレージ操作となり、ガスを消費します。特に書き込みは高コストです。
    • 構造体のメンバーが多いほど、ストレージのスロットを多く消費し、ガスコストが増加する可能性があります。
    • マッピングのキーアクセスは効率的ですが、存在しないキーに書き込む場合と、既に存在するキーの値を更新する場合でガス代が異なることがあります。
  • 削除:
    • マッピングから特定のキーと値のペアを「削除」する専用の操作はありません。値をデフォルト値に戻すことで、実質的な削除(クリア)を行います。例えば、balances[userAddress] = 0;userProfiles[userAddress] = UserProfile( ... デフォルト値 ... ); のようにします。Solidityには delete キーワードがあり、delete balances[userAddress]; のように使うと、そのキーに対応する値をデフォルト値に戻します。
    • delete を使うとガスが返還される場合があります(EIP-1559以降の仕様変更に注意)。
  • イテレーション不可への対策: マッピング内の全要素を処理したい場合は、別途キーのリストを配列(動的配列)などで管理し、その配列をループ処理してマッピングにアクセスする、といった方法が考えられます。ただし、配列の要素数が多くなるとガス代が高騰する可能性があるため注意が必要です。

まとめ

今回は、Solidityにおける重要なデータ管理機能である「構造体」と「マッピング」について学びました。

  • 構造体 (Struct): 関連する複数のデータをまとめて扱うためのカスタム型。
  • マッピング (Mapping): キーと値のペアを効率的に格納・取得するためのデータ構造。
  • 組み合わせ: マッピングの値として構造体を使うことで、アドレスなどのキーに紐づく複雑な情報を管理できる。

これらの機能を理解し活用することで、スマートコントラクトで扱えるデータの幅が広がり、より実用的なアプリケーションを開発できるようになります。特に、アドレスとユーザー情報、トークンIDとメタデータなど、ブロックチェーンアプリケーション特有のデータ管理において、マッピングと構造体は不可欠な要素です。

次のステップでは、配列やストレージ/メモリの違い、そしてガス最適化の基本について学んでいきましょう! 💪