[Rustのはじめ方] Part19: モジュールの定義とuseによる利用

Rust

Rustプログラミングへようこそ! 🎉 これまでのステップでRustの基本的な文法やデータ構造について学んできました。Step 6では、コードを整理し、再利用しやすくするための重要な機能であるモジュールについて学びます。

プロジェクトが大きくなるにつれて、すべてのコードを1つのファイルに書くのは現実的ではありません。モジュールを使うことで、関連する機能ごとにコードをグループ化し、構造化することができます。これにより、コードの見通しが良くなり、メンテナンス性も向上します 📦。

モジュールの定義 (`mod`)

モジュールは mod キーワードを使って定義します。最も基本的な形は、コードブロック内に直接モジュールを定義する方法です。

// my_module という名前のモジュールを定義
mod my_module {
    // モジュール内のアイテム(関数、構造体など)をここに定義します
    fn internal_function() {
        println!("This is an internal function.");
    }

    // 外部からアクセス可能にするには pub キーワードが必要です
    pub fn public_function() {
        println!("Called my_module::public_function()");
        internal_function(); // モジュール内のプライベート関数を呼ぶ
    }

    // サブモジュールも定義できます
    pub mod sub_module {
        pub fn hello() {
            println!("Hello from sub_module!");
        }
    }
}

fn main() {
    // モジュール内の公開関数を呼び出す
    my_module::public_function();
    my_module::sub_module::hello();

    // モジュール内のプライベート関数は直接呼び出せない
    // my_module::internal_function(); // これはコンパイルエラーになります
}

上記の例では、my_module というモジュールを定義し、その中に internal_functionpublic_function という2つの関数、さらに sub_module というサブモジュールを定義しています。

ファイル分割によるモジュール定義

コードが長くなってきたら、モジュールを別のファイルに分割することが一般的です。Rustでは、モジュール名に対応するファイルまたはディレクトリを作成することで、自動的にモジュールとして認識されます。

ルール:

  • src/main.rs (または src/lib.rs) 内で mod my_module; と宣言した場合、Rustコンパイラは以下のファイルを検索します。
    1. src/my_module.rs
    2. src/my_module/mod.rs (古いスタイルですが、まだ有効です)
  • サブモジュール (例: my_module 内の sub_module) は、親モジュールのディレクトリ内に定義します。
    • src/my_module.rs 内で mod sub_module; と宣言した場合:
      • src/my_module/sub_module.rs
      • src/my_module/sub_module/mod.rs

例:

ファイル構成:

.
├── src
│   ├── main.rs
│   └── network
│       ├── mod.rs   # network モジュールの内容 (または network.rs でも可)
│       └── client.rs  # network::client サブモジュールの内容
└── Cargo.toml

src/main.rs:

mod network; // src/network.rs または src/network/mod.rs を読み込む

fn main() {
    network::connect();
    network::client::ping();
}

src/network/mod.rs (または src/network.rs):

pub mod client; // src/network/client.rs を読み込む

pub fn connect() {
    println!("Connecting to network...");
}

src/network/client.rs:

pub fn ping() {
    println!("Pinging client...");
}

このようにファイルを分割することで、プロジェクトの構造が明確になり、大規模な開発でもコードを管理しやすくなります。

公開 (`pub`) キーワード

デフォルトでは、モジュール内のすべてのアイテム(関数、構造体、列挙型、モジュールなど)はプライベートです。つまり、そのモジュールの外部からはアクセスできません。

アイテムを外部のモジュールから利用できるようにするには、pub キーワードを付けて公開する必要があります。

mod utils {
    // この関数は utils モジュール内でのみ利用可能
    fn private_helper() {
        println!("Private helper called.");
    }

    // pub を付けることで、モジュール外からアクセス可能になる
    pub fn public_task() {
        println!("Public task started.");
        private_helper(); // モジュール内のプライベート関数は呼び出せる
    }

    // 構造体自体を公開
    pub struct Point {
        // フィールドも個別に公開する必要がある
        pub x: i32,
        // y はプライベート (utils モジュール内からのみアクセス可能)
        y: i32,
    }

    impl Point {
        // 関連関数 (コンストラクタなど) も公開する必要がある
        pub fn new(x: i32, y: i32) -> Point {
            Point { x, y }
        }

        // メソッドも公開が必要
        pub fn show(&self) {
            println!("Point(x: {}, y: {})", self.x, self.y); // y にはアクセス可能
        }
    }

    // 列挙型自体を公開すると、そのバリアントもすべて公開される
    pub enum Status {
        Connected,
        Disconnected,
        Error(String),
    }
}

fn main() {
    utils::public_task();
    // utils::private_helper(); // Error: private function

    let p = utils::Point::new(10, 20);
    println!("Point x: {}", p.x);
    // println!("Point y: {}", p.y); // Error: field `y` of struct `Point` is private
    p.show(); // y の値も表示される (メソッドはモジュール内で定義されているため)

    let status = utils::Status::Connected;
    match status {
        utils::Status::Connected => println!("Status: Connected"),
        utils::Status::Disconnected => println!("Status: Disconnected"),
        utils::Status::Error(msg) => println!("Status: Error({})", msg),
    }
}

構造体の場合、構造体自体を pub にしても、そのフィールドはデフォルトでプライベートです。フィールドごとに pub を付ける必要があります。一方、列挙型を pub にすると、そのすべてのバリアントが公開されます。

より細かいアクセス制御のために、pub(crate)(クレート内でのみ公開)、pub(super)(親モジュールでのみ公開)、pub(in path)(指定したパスのモジュールでのみ公開)といった指定も可能です。これらは、ライブラリ開発などでカプセル化を強化したい場合に役立ちます。

`use` キーワードによるパスの導入

他のモジュールで定義されたアイテムを使うたびに、完全なパス(例: my_module::sub_module::hello())を書くのは冗長です。use キーワードを使うと、アイテムのパスを現在のスコープに持ち込むことができ、より短い名前でアクセスできるようになります。

mod network {
    pub mod client {
        pub fn connect() {
            println!("Client connecting...");
        }
    }
    pub mod server {
        pub fn listen() {
            println!("Server listening...");
        }
    }
}

// use を使って client モジュール内の connect 関数をスコープに導入
use network::client::connect;

// use を使って server モジュール自体をスコープに導入
use network::server;

// use で別名を付ける (as キーワード)
use network::client::connect as client_connect;

fn main() {
    // use で導入したので、短い名前で呼び出せる
    connect();

    // server モジュールを導入したので、server:: から始められる
    server::listen();

    // 別名を付けた関数を呼び出す
    client_connect();

    // 完全パスでも呼び出し可能
    network::client::connect();
    network::server::listen();
}

パスの種類

  • 絶対パス: crate:: から始まるパス。クレートのルートから目的のアイテムを指定します。crate::network::client::connect のように使います。
  • 相対パス:
    • self::: 現在のモジュールからの相対パス。
    • super::: 親モジュールからの相対パス。

一般的には、ライブラリ利用時などは絶対パス、同一モジュール内のアイテム参照などでは相対パスが使われます。

複数のアイテムの導入と `*` (glob)

同じモジュールから複数のアイテムを導入したい場合、{} を使ってまとめることができます。

mod graphics {
    pub mod shapes {
        pub fn draw_circle() { /* ... */ }
        pub fn draw_rectangle() { /* ... */ }
    }
    pub mod colors {
        pub const RED: &'static str = "red";
        pub const BLUE: &'static str = "blue";
    }
}

// shapes モジュールから複数の関数を導入
use graphics::shapes::{draw_circle, draw_rectangle};
// colors モジュールから定数を導入
use graphics::colors::*; // * (glob) を使うとモジュール内のすべての公開アイテムを導入

fn main() {
    draw_circle();
    draw_rectangle();
    println!("Color: {}", RED); // glob で導入された
    println!("Color: {}", BLUE); // glob で導入された
}

* (glob演算子) を使うと、モジュール内のすべての公開アイテムを一度に導入できます。しかし、どのアイテムがどこから来たのか分かりにくくなる可能性があるため、特にライブラリのルートなどでの使用は、名前の衝突を避けるために慎重に行うべきです。アプリケーションのコードやテストコードでは便利な場合があります。

慣例として、関数や定数はそのアイテム自体を use し (例: use std::mem::swap;)、構造体、列挙型、モジュールはその親モジュールまでを use することが多いです (例: use std::collections::HashMap;)。これにより、HashMap::new() のように、どの型に関連する関数なのかが分かりやすくなります。ただし、同じ名前のアイテムが複数ある場合などは、より詳細なパスを use したり、as で別名を付けることが推奨されます。

まとめ

今回は、Rustのコードを整理するためのモジュールシステムについて学びました。

  • mod キーワードでモジュールを定義し、コードをグループ化します。
  • モジュールはファイルやディレクトリに分割して管理できます。
  • pub キーワードでモジュール内のアイテムを外部に公開します。
  • use キーワードで他のモジュールのアイテムを現在のスコープに導入し、短い名前で利用できます。

モジュールを使いこなすことで、コードの可読性、再利用性、保守性が大幅に向上します。次のステップでは、Rustのパッケージ管理システムであるCargoとクレートについて詳しく見ていきましょう!🚀

コメント

タイトルとURLをコピーしました