プログラムが規模を増すにつれ、その管理が徐々に難しくなってくると思います。
これは、プログラム内のクラスやメソッドが増え、それらがどのように関連していてどこで何が定義されているのかを把握するのが難しくなるからです。
そんな時、コードの整理と整頓に役立つ仕組みが「名前空間」と「ファイル分割」です。
- 名前空間:コード内のクラス等の部品を適切にグループ化し、名前の衝突を避ける
- ファイル分割:関連するコードを個別のファイルに分けることで、プログラムの構造を明確にし、管理しやすくする
本記事では、これら二つの概念がプログラムコードの整理整頓にどのように貢献し、大規模なプロジェクトの管理を容易にするのかを解説します。
Visual Studioを用いながら、レストランメニュー表を作成するプログラムを題材として、具体的に名前空間・ファイル分割についてみていきます。
今回の演習で作成したC#プロジェクトはGitHubのリポジトリに置いてあります。
YouTubeの動画もよかったら御覧ください。
演習1:整理整頓されていないコードの問題点
前回演習コードの復習
演習1コードとして、レストランメニューをHTMLで生成するプログラムについて考えます。
前回(オブジェクト指向におけるポリモーフィズムの説明)の演習2で使ったプログラムです。
実行結果のHTMLを表示すると次のようになります。
ソースコードは以下になります。
//■===以下は処理===■
//メニューデータを定義し、Menu配列に格納
Menu[] menus = new Menu[] {
new MainMenu("黒毛和牛ステーキ", "ジューシーで柔らかなステーキです。", 3000, false),
new MainMenu("ベジタブルカレー", "野菜をたっぷりと使った、スパイシーなカレーです。", 2400, true),
new DrinkMenu("メロンソーダ", "爽やかな甘さが楽しめるメロンソーダです。", 400, true),
new DrinkMenu("ホットコーヒー", "丁寧に焙煎されたコーヒー豆を使用しています。", 500, false),
new DessertMenu("チョコレートケーキ", "しっとりとしたチョコレートケーキです。", 600, 4),
};
//メニュー一覧をHTMLコードとして出力
MenuTableGenerator generator = new MenuTableGenerator(menus);
string tableHtml = generator.GenerateTable();
Console.WriteLine("<html>");
Console.WriteLine("<head><title>メニュー</title></head>");
Console.WriteLine("<body>");
Console.WriteLine("<h1>メニュー一覧</h1>");
Console.WriteLine(tableHtml);
Console.WriteLine("</body>");
Console.WriteLine("</html>");
//■===以下はクラス定義===■
//メニューの情報を格納するクラス(DrinkMenu、MainMenu、DessertMenuの親クラス)
abstract class Menu
{
private string name;
private string description;
private int price;
public Menu(string name, string description, int price)
{
this.name = name;
this.description = description;
this.price = price;
}
public string Name
{
get { return name; }
}
public string Description
{
get { return description; }
}
public int Price
{
get { return price; }
}
public int GetPriceWithTax()
{
const double taxRate = 0.1; // 消費税率10%
return (int)Math.Round(Price * (1 + taxRate));
}
public abstract string GetNote();
}
//飲み物メニューの情報を格納するクラス(Menuから派生した子クラス)
class DrinkMenu : Menu
{
private bool isCold;
public DrinkMenu(string name, string description, int price, bool isCold)
: base(name, description, price)
{
this.isCold = isCold;
}
public bool IsCold
{
get { return isCold; }
}
public override string GetNote()
{
return IsCold ? "(冷)" : "";
}
}
//主菜メニューの情報を格納するクラス(Menuから派生した子クラス)
class MainMenu : Menu
{
private bool isVegetarian;
public MainMenu(string name, string description, int price, bool isVegetarian)
: base(name, description, price)
{
this.isVegetarian = isVegetarian;
}
public bool IsVegetarian
{
get { return isVegetarian; }
}
public override string GetNote()
{
return IsVegetarian ? "(菜食)" : "";
}
}
//デザートメニューの情報を格納するクラス(Menuから派生した子クラス)
class DessertMenu : Menu
{
private int sweetnessLevel;
public DessertMenu(string name, string description, int price, int sweetnessLevel)
: base(name, description, price)
{
this.sweetnessLevel = sweetnessLevel;
}
public int SweetnessLevel
{
get { return sweetnessLevel; }
}
public override string GetNote()
{
return $"(甘さレベル{sweetnessLevel})";
}
}
//メニューデータ配列からHTMLテーブルを生成するクラス
class MenuTableGenerator
{
private Menu[] menus;
public MenuTableGenerator(Menu[] menus)
{
this.menus = menus;
}
public string GenerateTable()
{
string table = "<table border='1'>";
table += "<tr><th>メニュー名</th><th>価格(税込み)</th><th>説明</th><th>補足</th></tr>\n";
foreach (Menu menu in menus)
{
table += "<tr>";
table += $"<td>{menu.Name}</td>";
table += $"<td>{menu.GetPriceWithTax()}円</td>";
table += $"<td>{menu.Description}</td>";
table += $"<td>{menu.GetNote()}</td>";
table += "</tr>\n";
}
table += "</table>";
return table;
}
}
メニューアイテムは主菜、飲み物、デザートとしてクラス階層で表現され、それぞれ独自の特性(例:ベジタリアンか?・冷たいか?・甘さレベル)を持っています。
メニューの配列はHTMLテーブルに変換され、各メニューの詳細が行として出力されます。
このプログラムでは、1つのファイルに処理や全てのクラス定義が書かれており、名前空間という概念も登場していませんね。
このプログラムは、C# 9.0から導入されたトップレベルステートメントという仕組みを使って書かれています。
トップレベルステートメントは、コードの書き方をシンプルにするための機能であり、特に小規模なプログラムや学習用途において有用です。
トップレベルステートメントについては、名前空間等の基本的な知識があった上で説明した方がわかりやすいため、本記事の後半であらためて説明します。
コードの問題点
演習1コードでは、プログラムの規模は大きくなってくると以下のような問題が出てきます。
- 問題1:名前が衝突する
- 問題2:可読性・再利用性・変更容易性が低下する
問題1:名前が衝突する
コードが大規模になると、同じ名前のクラスが登場する可能性があります。
今のコードにはレストランメニューを表すMenuクラスがありますね。
例えばWebアプリケーションへ拡張して、Web画面部品のメニューを表すMenuクラスを別に作ったら名前が競合してしまいます。
RestaurantMenu、WebScreenMenuなどとすれば競合しませんが、これではクラス名がどんどん長くなってしまいます。
クラス・メソッド・変数などの名前が競合していると、コンパイルエラーになります。
問題2:可読性・変更容易性・再利用性が低下する
1つのファイルに全てのコードがあるとそのファイルは長く複雑で理解しにくくなり、可読性・変更容易性が低下します。
特定の機能を再利用するためには、ファイルの中から必要なコードをコピーして再利用するか、不必要な部分含めたファイル全体をそのまま使うことになり、再利用性も低下します。
演習2:名前空間とファイル分割でコードを整理整頓する
名前空間・ファイル分割導入による問題解決
名前空間・ファイル分割を導入して問題を解決していきましょう。
- 問題1:名前が衝突する
- 問題2:可読性・再利用性・変更容易性が低下する
これらの問題は次のように解決できます。
「問題1:名前衝突」を解決
名前空間は、コード内の名前をグループ化するための方法です。
コードの異なる部分で同じ名前を使用することが可能になります。
各名前空間は独立しているため、名前空間Aに属するクラスと名前空間Bに属する同名のクラスは衝突せず、名前空間を指定することでどのクラスを参照しているかを明確にできます。
以下のようなイメージです。
この図では名前空間A・BにそれぞれクラスX・Yがありますが、これらは異なる名前空間に所属しており、例えば同名のクラスXはA.X・B.Xというふうに区別できます。
「問題2:可読性・再利用性・変更容易性の低下」を解決
プログラムを書くときには、関連するクラス等を一緒にグループ化し、個別のファイルに分割することが一般的です。
これにより、それぞれのファイルがひと目で理解でき、特定の機能を見つけやすくなります。
ファイルが独立しているため、必要なファイルのみ再利用することが可能になります。
特定の機能に問題が発生した場合、その機能が含まれるファイルを見つけて修正することが容易になります。
演習2コード
名前空間・ファイル分割の方針
Visual Studioで複数のファイルを作成しながらプログラムを書いていきます。
次のような方針で名前空間・ファイル分割を用いてプログラムを整理整頓してみましょう。
- メニュー関連のクラス群、メニュー生成のクラスをそれぞれグループ化して名前空間を分ける。
- 名前空間ごとにフォルダを用意し、クラスごとにファイルを分けて格納する。
名前空間は以下のようになります。
トップレベルをRestaurant名前空間として、その配下にMenus(メニュー関連)・Generators(メニュー生成)の名前空間を作ります。
Restaurant直下のProgramクラスでは、Menus、Generators配下のクラス群を使ってメニュー表生成を行います(演習1コードの最初の「処理」相当ですね)。
フォルダ・ファイル構成は名前空間の構成に合わせて以下のようにします。
\---Restaurant
| Program.cs
+---Generators
| MenuTableGenerator.cs
+---Menus
| DessertMenu.cs
| DrinkMenu.cs
| MainMenu.cs
| Menu.cs
C#の開発では、クラスごとにファイルを用意し、名前空間の構成とフォルダ構成をそろえることが一般的です。
なので、C#でコードを書くときには最初から名前空間によるグループ化をし、クラスごとにファイルを分けてコードを書いていくとよいでしょう。
Visual Studioでプロジェクト作成
Visual Studio2022で設計したフォルダ・ファイル構成の通り、ソースコードファイルを作成していきましょう。
Visual Studioの基本的な使い方について以下の記事も参考にしてください。
まずは、コンソールアプリケーションの新規プロジェクトを作成します。
プロジェクトを作成するときに、「追加情報」で「最上位レベルのステートメントを使用しない」にチェックを入れましょう。
今回は名前空間・C#プロジェクトの構造・プログラムのエントリーポイントといった概念を基本から学ぶため、トップレベルステートメントを使わないことにします。
この新規プロジェクトをベースに名前空間・ファイル分割の方針に従って、演習1のコードを整理整頓しながら、フォルダ、ソースコードファイルを作成します。
ソリューションエクスプローラで、プロジェクト名(Restaurant)やフォルダなどを選んで右クリックします。
そして、「追加>新しい項目」や「追加>新しいフォルダ」で、選んだ要素は以下に新しい項目(クラスなど)やフォルダを追加できます。
方針通りに全てのフォルダ、ファイルを配置すると最終的に、ソリューションエクスプローラは次のようになります。
ソースコードファイル
ソースコードファイルのうちProgram.cs、Menu.cs、MenuTableGenerator.csのコードを以下に載せます。今回の説明で不要な部分は省略しています。
using Restaurant.Generators; //★(a1) usingディレクティブ
using Restaurant.Menus; //★(a2) usingディレクティブ
namespace Restaurant //★(b1) 名前空間の定義
{
internal class Program
{
static void Main(string[] args) //★(c) プログラムのエントリーポイント
{
//メニューデータを定義し、Menu配列に格納
//★(d1) 名前空間を省略してMenuと記述
// 完全修飾名はRestaurant.Menus.Menu
Menu[] menus = new Menu[] {
new MainMenu("黒毛和牛ステーキ", "ジューシーで柔らかなステーキです。", 3000, false),
…
};
//メニュー一覧をHTMLコードとして出力
//★(d2) 名前空間を省略してMenuTableGeneratorと記述
// 完全修飾名はRestaurant.Generators.MenuTableGenerator
MenuTableGenerator generator = new MenuTableGenerator(menus);
…
//★(e) 暗黙的なglobal usingで名前空間を省略してConsoleと記述
// 完全修飾名はSystem.Console
Console.WriteLine("<html>");
…
}
}
}
namespace Restaurant.Menus //★(b2) 名前空間の定義
{
//メニューの情報を格納するクラス(DrinkMenu、MainMenu、DessertMenuの親クラス)
internal abstract class Menu
{
…
}
}
DrinkMenu.cs・MainMenu.cs・DessertMenu.csは、Menu.csとあまり変わらないため省略します。
using Restaurant.Menus; //★(a3) usingディレクティブ
namespace RestaurantMenu.Generators //★(b3) 名前空間の定義
{
//メニューデータ配列からHTMLテーブルを生成するクラス
internal class MenuTableGenerator
{
…
}
}
ポイント:ファイル分割
ポイント1:C#プロジェクト管理の仕組み
C#のプロジェクト管理の仕組みについて簡単に説明します。
Visual Studioのプロジェクトを作成すると、以下のような構成になります。
\---Restaurant
| Program.cs
| Restaurant.csproj
| Restaurant.sln
|
+---bin
| …
|
+---Generators
| MenuTableGenerator.cs
|
+---Menus
| DessertMenu.cs
| DrinkMenu.cs
| MainMenu.cs
| Menu.cs
|
+---obj
| …
ソースコードファイルについては最初に設計した通り、名前空間と対応させたフォルダを作成してそこへ格納しています。
ソースコードファイル以外にも色々なファイル・フォルダがありますね。
それぞれ以下のような役割になっています。
- .slnファイル:ソリューションの管理ファイル
- .csprojファイル:プロジェクトの管理ファイル
- binフォルダ:ビルドの最終生成物(実行形式ファイルなど)を格納するフォルダ
- objフォルダ:ビルドの中間生成物を格納するフォルダ
プロジェクト、ソリューションとは以下のようなものです。
- プロジェクト:複数のソースコード等の集まりです。ビルドによって1つにまとめられ、実行形式ファイル等になる。
- ソリューション:複数のプロジェクトを束ねるものです。今回は小さなプログラムなので、ソリューションの中にプロジェクトは1つしかありません。
ソリューション、プロジェクトのイメージは以下のようになります。
Restaurantソリューション配下に、Restaurantプロジェクトがあり、どちらの設定ファイルも同じRestaurantフォルダにあります。
Restaurantフォルダ配下のC#ソースコードは、全てRestaurantプロジェクトのソースコードとしてまとめてビルドされます。
ポイント2:プログラムのエントリーポイント
プロジェクトにファイルが複数ある場合、ビルドしたプログラムは最初にどこから実行が開始されるのでしょうか?
このような場合、Mainという名前のクラスメソッドからプログラムが実行されます。このメソッドをエントリーポイントといいます。
ProgramクラスのMainメソッドがこのプログラムのエントリーポイントとなります。
static void Main(string[] args) //★(c) プログラムのエントリーポイント
{
…
}
Mainメソッドのargsは、プログラムをコンソール画面から実行したときの実行時引数が格納されますが、今回は使用していません。
詳しくはソリューションとプロジェクトの記事や、エントリーポイント(Mainメソッド)の記事についても参考にしてください。
ポイント:名前空間
ポイント1:名前空間の定義
名前空間を使うことで、型をグループ化することができ、それによって型の名前が衝突することを防げます。
名前空間はnamespace XXX{…}と定義します。
XXXの部分はXXX.YYY.ZZZというふうに「.」で区切って複数書き、階層構造を表現することもできます。
以下が名前空間の定義と、グループ化のイメージです。
例えば、Program.csの(b1)ではRestaurant名前空間を定義し、その名前空間の配下でProgramクラスを定義しています。
Menu.csの(b3)では、「namespace Restaurant.Menus」と階層化された名前空間を定義し、その配下でMenuクラスを定義していますね。
internalという修飾子は、ざっくりは同じプロジェクト内からのみアクセス可能という意味になります。詳しくはこちらの記事も参考にしてください。
ポイント2:usingディレクティブと完全修飾名
ある名前空間内で、別の名前空間のクラス名(型名)を記述するときには、名前空間も含めて型名を記述する必要があります(これを完全修飾名と言います)。
例えば、Restaurant名前空間において、Restaurant.Menus名前空間の型(例えばMenu型)を用いる場合は以下のように記述します。
Restaurant.Menus.Menu[] menus = new Restaurant.Menus.Menu[] { …
このように、「Restaurant.Menus名前空間のMenu型」と完全修飾名で書くことで、別の名前空間で別のMenu型が定義されたとしても区別できるわけです。
しかしながら、型名を毎回このように長ったらしく書くのは面倒ですし、ソースコードの可読性も悪くなるという問題があります。
この問題を解決するのがusingディレクティブです。
using
ディレクティブは、他の名前空間を現在のコンテキストで利用可能にするための構文です。
例えば、Program.csの(a2)はusingディレクティブで、Restaurant.Menus名前空間を完全修飾名なしで使うことを宣言しています。
これにより、Program.csの(d1)では完全修飾名を使わず「Menu」だけ記述して「Restaurant.Menus.Menu」型を参照できます。
…
using Restaurant.Menus; //★(a2) usingディレクティブ
namespace Restaurant //★(b1) 名前空間の定義
{
internal class Program
{
static void Main(string[] args) //★(c) プログラムのエントリーポイント
{
//メニューデータを定義し、Menu配列に格納
//★(d1) 名前空間を省略してMenuと記述
// 完全修飾名はRestaurant.Menus.Menu
Menu[] menus = new Menu[] {
…
これで簡潔に型名を記述できますね。
ただ、usingディレクティブを多用すると、名前が衝突する可能性もあるためその点は注意が必要です。
例えば「名前空間A.クラスX」、「名前空間B.クラスX」と2つのクラスXがあった場合、名前空間A、名前空間Bの両方をusing宣言するとクラスXの名前が衝突します。
このように、名前が衝突してしまうような場合は、usingディレクティブは使わず完全修飾名を使うようにしましょう。
usingディレクティブについてはこちらの記事も参考にしてください。
ポイント3:暗黙的global usingディレクティブ
最後に、プログラムをより簡潔に書ける便利な機能である暗黙的global usingディレクティブについて説明します。
Programクラスの(e)の部分を見てください。
//★(e) 暗黙的なglobal usingで名前空間を省略してConsoleと記述
// 完全修飾名はSystem.Console
Console.WriteLine("<html>");
C#入門編の演習コードでも毎回のように使ってきたConsole.WriteLineメソッドですね。
実は、Consoleクラスの完全修飾名はSystem.Consoleです。Consoleクラスは標準ライブラリのSystem名前空間で定義されているのです。
しかし、Program.csでは「using System」というusingディレティブがないのになぜか単に「Console」と書いて参照できていますね。
これは「暗黙的global usingディレクティブ」を用いてSystem名前空間をプロジェクト全体に対して「using System」した状態になっているためです。
「global」と「暗黙的」がそれぞれ何を意味しているのか説明します。
「global using XXX」とglobal修飾子をつけてusingディレクティブを宣言すると、プロジェクト全体へ、そのusingディレクティブが有効になります。
これをglobal usingディレクティブといいます。
C#10.0(.NET6.0)から、よく使う名前空間についてglobal usingディレクティブを暗黙的に宣言することが可能となりました。
これが、暗黙的global usingディレクティブです。
プロジェクトを新規作成すると、Systemなどよく使う名前空間についてはデフォルトで暗黙的global usingディレクティブとして設定されています。
ソリューションエクスプローラでプロジェクト名を右クリックしてプロパティを選択し、global usingsという項目を見ると、以下のように名前空間の一覧が確認できます。
(ちょっと薄くて見にくいですが…)
暗黙的なglobal usingディレクティブを無効にすることも可能です。
暗黙的なglobal usingディレクティブが有効になっているため、System名前空間配下のConsoleクラスは、単に「Console」と書くだけで参照できたわけですね。
そして、頻繁に使うusingディレクティブ(例:System)も毎回書くのは面倒なので、global usingディレクティブ・暗黙的なglobal usingディレクティブが導入されたのです。
名前空間とファイル分割まとめ
演習2では演習1コードに名前空間とファイル分割の考え方を導入して、コードの整理整頓を行いました。
このぐらいの規模のプログラムだと、名前空間やファイル分割のありがたみはそれほど感じないかもしれません。
しかし、プログラムの規模が大きくなるに從って、このようにコードを整理整頓しておくことの重要性は増します。
プログラムを書くときには、クラスをグループ化して名前空間をつけ、クラスごとにファイルを分けて名前空間の構造にあわせたフォルダ構成にすることを心がけていきましょう。
講義1:トップレベルステートメントとは?
最後に、あらためてトップレベルステートメントについて説明します。
演習2で、プログラムのエントリーポイントであるMainメソッドについて解説しました。
C# 8.0以前ではプログラムを作成するときに、必ず何らかのクラスを作成しMainメソッドを定義する必要がありました。
しかし、簡単なプログラムを作成するときにも毎回このようにMainメソッドを作成するというのは少し煩わしいですね。
そこでC# 9.0 以降、プログラムを書く際にトップレベルステートメントという新たな方法が提供されるようになり、C#のコードをより簡潔に書けるようになりました。
演習1のコードや、これまでのC#入門編のコードはトップレベルステートメントで書かれています。
以下にトップレベルステートメントで書かれた演習1コードを再掲します。
//■===以下は処理(Mainメソッド相当)===■
//メニューデータを定義し、Menu配列に格納
Menu[] menus = new Menu[] {
…
//■===以下はクラス定義===■
//メニューの情報を格納するクラス(DrinkMenu、MainMenu、DessertMenuの親クラス)
abstract class Menu
{
…
}
…
//メニューデータ配列からHTMLテーブルを生成するクラス
class MenuTableGenerator
{
…
}
コードの最初に記述されている「処理」の部分がエントリーポイントであり、Mainメソッドに相当します。
ビルドしたときに、裏側で勝手にMainメソッドが作成され、ここに書いてある処理がMainメソッドの中身として埋め込まれて実行形式ファイルが作成されます。
ちなみに、この演習1コードで定義された型(Menuクラスなど)は「グローバル名前空間」に所属します。
グローバル名前空間とは、具体的な名前空間に所属していない型(つまり名前空間宣言なしで定義された型)が存在するデフォルトの名前空間のことです。
例えば、演習1のコードにおける「Menu型」はグローバル名前空間に所属し、完全修飾名はそのまま「Menu型」となります。
トップレベルステートメントは主に、初心者がC#を学習するときや、ちょっとした作業を自動化する簡単なスクリプトを書くときなどに使います。
ある程度の規模のプログラムを書く場合には、演習2で行ったように名前空間・クラス・メソッドといった構造を使用してコードを整理整頓して管理した方がよいでしょう。
そんなときに、トップレベルステートメントはとても便利です。このC#入門編でもありがたく活用させてもらっています。
まとめ
今回は、名前空間・ファイル分割について説明しました。
名前空間によりコード内のクラス等の部品を適切にグループ化し、名前の衝突を避けることができます。
関連するコードを個別のファイルへ分割することで、プログラムの構造を明確にし、管理しやすくします。
規模の大きなプログラムを作っていく上で、名前空間・ファイル分割を用いてプログラムを整理・整頓しておくことはとても重要です。