Rust入門

【Rust入門】モジュールの可視性の基本を分かりやすく解説

【Rust入門】モジュールの可視性の基本を分かりやすく解説
naoki-hn

Rust のモジュールの可視性の基本を初心者にも分かりやすく解説します。

Rust のモジュールの可視性

Rust では、コードを整理して安全性を高めるために「モジュール」と「可視性(visibility)」という仕組みが用意されています。この仕組みの中でも「pub」は、モジュール間やクレート間での参照をうまく制御するための重要なキーワードです。

Rust は、安全性を高めるため「モジュール外から参照できる要素はデフォルトで非公開」となっているという特徴があります。そのため、外部に公開したいものには pub をつけて開発者が明示的に公開範囲を示す必要があります。

この記事では、Rust のモジュール構造を説明しながら、pub を中心とした可視性の仕組みを初心者にも分かりやすく紹介します。

Rust のモジュールとは

Rust では、コードを階層的に分割し、相互の機能を隠ぺい、公開するための強力なモジュールシステムがあります。モジュールは、関数、構造体、定数などを論理的にまとめることができ、mod キーワードを使って以下のように定義します。

// ファイル内でのモジュール定義例
mod sample_mod {
    // 公開関数
    pub fn hello() {
        println!("[sample_mod] 公開関数");
    }
}

fn main() {
    println!("===== ファイル内でのモジュール定義例 =====");
    sample_mod::hello();
}

この sample_mod モジュールは、main.rs のファイル内で定義されているモジュールであるとします。例では、sample_modhello() という関数が定義されていることを意味します。

pub による公開

Rust の大きな特徴として「モジュール外から参照できる要素はデフォルトで非公開」となっています。そのため、mod で定義したモジュール内の要素をスコープ外に公開したい場合には、pub キーワードをつけて公開(public)であることを明示します。

上記の例で「pub fn hello() { ... }」は、mod{} で囲われたスコープの外に hello() 関数を公開していることを意味します。モジュール sample_mod は、名前空間として機能するため main.rs 内の関数(main 関数など)から、hello() 関数を使用するには、sample_mod::hello() のように「::」を使用して呼び出します。

デフォルトは非公開

Rust の大きな特徴として「すべての要素はデフォルトで private(非公開)」となっています。そのため、以下のように mod のスコープ外から内部で pub をつけずに定義した関数へアクセスしようとするとコンパイルエラーとなります。

// ファイル内でのモジュール定義例
mod sample_mod {
    // 公開関数
    pub fn hello() {
        println!("[sample_mod] 公開関数");
        private_hello();
    }

    // 非公開関数
    fn private_hello() {
        println!("[sample_mod] 非公開関数");
    }
}

fn main() {
    println!("===== ファイル内でのモジュール定義例 =====");
    sample_mod::hello();
    // 以下の非公開関数の呼び出しはエラーになる
    // sample_mod::private_hello();
}

例では、private_hello は、非公開関数であり、sample_mod モジュールの中でしか使用できません。そのため、スコープ外の main 関数から呼び出すとコンパイルエラーとなります。

一方で、hello 関数は、モジュール内の関数なので、この関数からは private_hello 関数へアクセスすることが可能です。このようにすることで、モジュールの外部に公開する関数を制御することが可能です。

モジュールの分割と管理方法

Rust では、モジュール定義を mod 宣言により行いますが、その実体をファイルやディレクトリに分割することができます。Rust でよく使われるモジュール分割方法には次のようなものがあります。

  • ファイル単位でモジュールを分割する方法
  • ファイルとディレクトリを組み合わせてサブモジュールを管理する方法
  • mod.rs を使用する従来の方法(現在も使用可能)

以降では、基本的な方法から順に説明していきます。

ファイル単位でモジュールを分割する方法

まずは、基本となるファイル単位でモジュールを分割する方法を説明します。以下のように main.rs とは別に module1.rs を用意した例で紹介します。

src/
├── main.rs      // エントリポイント
└── module1.rs   // module1 モジュール

以下のようなプログラムを用いて詳細を説明します。

[main.rs]

// モジュール読み込み
mod module1; // ファイルで分割

fn main() {
    println!("\n===== ファイルで分割する例 (module1) =====");
    module1::hello();
    // 以下の非公開関数の呼び出しはエラーになる
    // module1::private_hello();
    module1::submod::hello();
    module1::submod::nested::nested_hello();
}

[module1.rs]

// 公開関数
pub fn hello() {
    println!("[module1] 公開関数");
    private_hello();
    private_mod::hello();
}

// 非公開関数
fn private_hello() {
    println!("[module1] 非公開関数");
}

// 公開サブモジュール
pub mod submod {
    // サブモジュール内の公開関数
    pub fn hello() {
        println!("[module1::submod] サブモジュール内の公開関数");
        private_hello();
    }

    // サブモジュール内の非公開関数
    fn private_hello() {
        println!("[module1::submod] サブモジュール内の非公開関数");
    }

    // ネストされた公開サブモジュール
    pub mod nested {
        pub fn nested_hello() {
            println!("[module1::submod::nested] ネストされたサブモジュール内の公開関数");
        }
    }
}

// 非公開のサブモジュール
mod private_mod {
    pub fn hello() {
        println!("[module1::private_mod] 非公開のサブモジュール");
    }
}

main.rsmodule1 を使用するには、ファイルの先頭で次のように宣言します。

mod module1;

この宣言により、module1.rsmodule1 モジュールとして読み込まれて、module1::関数名 のように公開要素を利用できるようになります。

module1.rs 内では、複数の関数とサブモジュールを定義しています。hello 関数は、pub をつけて定義しているため、main.rs のような外部モジュールから呼び出すことができます。一方、private_hello 関数は pub をつけていないため、module1 の内部からのみ利用可能です。

また、モジュール内ではさらにサブモジュールを定義することができます。submod は、module1 の公開サブモジュールであり、外部からもアクセス可能です。さらに、その中に nested のようなネストしたサブモジュールを定義することもできます。

注意点として、サブモジュールも「pub mod」としない限り外部には公開されません。private_mod は、pub が付いていないため、module1 の内部からしか利用できないモジュールです。

このように、意味のある単位でモジュールを分割し、可視性を適切に制御することで、コードの構造を整理し、安全で見通しの良いプログラムを作成することができます。

ディレクトリでサブモジュールを管理する方法

プロジェクトが大きくなってきた際に、1 つのファイルにすべてのサブモジュールを定義すると構造が分かりにくくなります。そのような場合には、ディレクトリを使ってサブモジュールを分割すると構造が明確になり、保守性も向上します。

以下のような構成を考えます。

src/
├── main.rs      // エントリポイント
├── module2.rs   // module2 モジュール
└── module2/
    ├── submod_1.rs  // サブモジュール1
    └── submod_2.rs  // サブモジュール2

[main.rs]

// モジュール読み込み
mod module2; // ディレクトリでサブモジュールを管理する

fn main() {
    println!("\n===== ディレクトリでサブモジュールを管理する例 (module2) =====");
    module2::submod_1::hello();
    module2::submod_2::hello();
}

[module2.rs]

pub mod submod_1;
pub mod submod_2;

[submod_1.rs]

// 公開関数
pub fn hello() {
    println!("[module2::submod_1] 公開関数");
    private_hello();
}

// 非公開関数
fn private_hello() {
    println!("[module2::submod_1] 非公開関数");
}

[submod_2.rs]

// 公開関数
pub fn hello() {
    println!("[module2::submod_2] 公開関数");
    private_hello();
}

fn private_hello() {
    println!("[module2::submod_2] 非公開関数");
}

この構成では、module2.rsmodule2 モジュールのエントリーポイントになります。module2 ディレクトリ配下に配置したファイルについては、module2.rs 内で mod 宣言することでサブモジュールとして読み込まれます。

なお、ディレクトリにファイルを配置しただけでは、自動的にモジュールにはなりません。親モジュール側(この例では、module2.rs)で mod 宣言する必要がある点に注意してください。

mod.rs を使用する従来の方法

以前の Rust では、ディレクトリをモジュールとして扱う場合、mod.rs を配置する方法が一般的でした。現在でも使用可能ですが、新規プロジェクトでは前述の「ファイル + ディレクトリ」を組み合わせた方法が使われることが多くなっています。

mod.rs は以下のようにモジュール名のディレクトリ配下に配置します。

src/
├── main.rs      // エントリポイント
└── module3/
    ├── mod.rs       // module3 モジュール
    ├── submod_1.rs  // サブモジュール1
    └── submod_2.rs  // サブモジュール2

[main.rs]

// モジュール読み込み
mod module3; // mod.rs を使用する従来の方法

fn main() {
    println!("\n===== mod.rs を使用する従来の方法例 (module3) =====");
    module3::submod_1::hello();
    module3::submod_2::hello();
}

[mod.rs]

pub mod submod_1;
pub mod submod_2;

submod_1.rssubmod_2.rs は先ほどの例と同様なので省略します。

use によるモジュールのスコープへの取り込み

これまでの例では、module::submodule::function のようにフルパスでモジュールの構成要素を呼び出していました。use を使うことで、指定した要素をスコープに取り込み、より簡潔に記述できます。

[main.rs]

// モジュール読み込み
mod module1; // ファイルで分割
mod module2; // ディレクトリでサブモジュールを管理する

// use 宣言でモジュールをスコープに取り込む
use module1::submod::hello;
use module2::submod_1::hello as hello_module2_submod_1;

fn main() {
    println!("\n===== use 宣言でスコープに取り込む例 =====");
    hello();
    hello_module2_submod_1();
}

同名の要素をスコープに取り込む場合には、as を使って別名をつける必要があります。

なお、use は名前解決を簡単にするための仕組みであり、可視性に直接かかわるものではありません。モジュールを使用するためには、mod でモジュールを取り込む必要があることに注意してください。

その他の pub の指定方法

これまでに紹介したように、基本的には pub を使った公開指定の方法とモジュール分割の方法を理解しておけば多くの場合では問題ありません。より細かく公開範囲を指定したい場合には、次のような指定方法も用意されています。

  • pub(super):親モジュールまで公開する
  • pub(in path):指定したモジュールパス (path) 内のみに公開

この記事では詳細な説明は省略しますが、公開範囲を段階的に制御できる仕組みがあるという点は押さえておいてください。

まとめ

Rust のモジュールの可視性の基本について解説しました。

Rust では、モジュール外から参照できる要素はデフォルトで非公開となっており、外部に公開したい関数や型には pub を付けて明示的に公開範囲を指定します。この仕組みにより、意図しない API の公開を防ぎ、安全で保守しやすいコードを書くことができます。

また、モジュールは mod 宣言を用いて定義し、その実体をファイルやディレクトリに分割して管理することが可能です。プロジェクトの規模に応じて、ファイル単位やディレクトリ単位でモジュールを整理すると、コードの見通しが良くなります。

さらに、use を使うことでモジュールの要素をスコープに取り込み、記述を簡潔にすることができます。ただし、use は可視性のルール自体を変更するものではない点には注意が必要です。

まずは、この記事で紹介した pub による公開と基本的なモジュール分割の方法をしっかり理解することで、Rust のモジュール設計の基礎を覚えていただければと思います。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました