C#でアプリケーションを開発する際、複数のクラスやメソッドから共通してアクセスできる「グローバル変数」を定義したい場面に遭遇することは珍しくありません。

しかし、C#は純粋なオブジェクト指向言語であるため、C言語やC++のようにクラスの外側に変数を記述する真の意味での「グローバル変数」は存在しません。

C#でグローバルな状態を管理するには、静的クラス(staticクラス)Singleton(シングルトン)パターン、あるいは現代的な手法である依存性の注入(DI)を活用する必要があります。

これらの手法は、単に値を共有するだけでなく、アプリケーションの保守性やテストのしやすさに大きく影響を与えます。

本記事では、C#におけるグローバル変数の代替手法について、それぞれの実装方法からメリット・デメリット、そして現場で役立つ使い分けの基準までをプロの視点で詳しく解説します。

C#におけるグローバル変数の考え方

C#において「グローバル変数」を実現するということは、特定のインスタンスに紐付かずにアプリケーション全体で一意の状態を保持することを意味します。

一般的には、以下の3つのアプローチが取られます。

  1. staticクラス・staticメンバ:最もシンプルで直接的な方法。
  2. Singletonパターン:インスタンスを1つだけに制限するデザインパターン。
  3. DI(Dependency Injection)によるシングルトン管理:現代の .NET 開発における標準的な手法。

グローバル変数は便利である反面、どこからでも書き換えが可能であるため、バグの温床になりやすいというリスクも孕んでいます。

そのため、適切な設計思想を持って選択することが重要です。

staticクラスによるグローバル変数の実装

最も手軽にグローバル変数を模倣する方法は、static 修飾子を付けたクラスとプロパティを使用することです。

static クラスはインスタンス化する必要がなく、プログラムの開始から終了までメモリ上に保持されます。

staticクラスの基本構造

以下のコードは、アプリケーション全体で共有する設定情報を管理する単純な static クラスの例です。

C#
using System;

namespace GlobalVariableExample
{
    // staticクラスとして定義することで、どこからでもアクセス可能にする
    public static class AppConfig
    {
        // 読み取り専用のグローバル定数
        public const string AppName = "MyTechnicalApp";

        // 書き換え可能なグローバル変数(プロパティ)
        public static string CurrentUser { get; set; } = "Guest";

        // 共通で利用するユーティリティメソッド
        public static void PrintStatus()
        {
            Console.WriteLine($"[Config] App: {AppName}, User: {CurrentUser}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // インスタンス化せずに直接アクセス
            Console.WriteLine($"初期ユーザー: {AppConfig.CurrentUser}");

            // 値の変更
            AppConfig.CurrentUser = "Administrator";

            // 変更後の確認
            AppConfig.PrintStatus();
        }
    }
}
実行結果
初期ユーザー: Guest
[Config] App: MyTechnicalApp, User: Administrator

staticクラスのメリットとデメリット

メリット

  • 実装が極めて簡単であり、初心者でも直感的に理解できます。
  • new キーワードによるインスタンス化が不要なため、アクセス速度がわずかに速い傾向にあります。
  • ユーティリティ関数(数学計算や文字列操作など)をまとめるのに適しています。

デメリット

  • オブジェクト指向の恩恵を受けにくい:継承(Inheritance)やインターフェースの実装ができません。
  • ユニットテストが困難:グローバルな状態が固定されているため、テストごとに状態をリセットするのが難しく、テストの並列実行に支障をきたすことがあります。
  • スレッドセーフティの問題:マルチスレッド環境で同時にアクセス・書き換えを行う場合、開発者が明示的にロック管理を行う必要があります。

Singletonパターンによるグローバル変数の実装

Singleton(シングルトン)パターンは、特定のクラスのインスタンスがアプリケーション全体で確実に1つだけ生成されることを保証するデザインパターンです。

staticクラスとは異なり、通常のオブジェクトとして振る舞うため、より柔軟な設計が可能です。

スレッドセーフなSingletonの実装

現代のC#では、Lazy<T> クラスを使用することで、スレッドセーフかつ遅延初期化(必要になるまでインスタンスを作らない)に対応したSingletonを簡潔に記述できます。

C#
using System;

namespace GlobalVariableExample
{
    public class UserSession
    {
        // Lazy<T>を使用して、スレッドセーフなシングルトンを実現
        private static readonly Lazy<UserSession> _instance = 
            new Lazy<UserSession>(() => new UserSession());

        // 外部からはこのプロパティを通じてのみアクセスできる
        public static UserSession Instance => _instance.Value;

        // コンストラクタをprivateにすることで、外部からのnewを禁止する
        private UserSession()
        {
            Console.WriteLine("UserSession インスタンスが生成されました。");
            LoginTime = DateTime.Now;
        }

        public string UserName { get; set; } = "Anonymous";
        public DateTime LoginTime { get; }

        public void DisplayInfo()
        {
            Console.WriteLine($"User: {UserName}, LoginAt: {LoginTime}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 初回アクセス時にインスタンスが生成される
            Console.WriteLine("プログラム開始");
            
            UserSession.Instance.UserName = "TechWriter_01";
            UserSession.Instance.DisplayInfo();

            // 2回目のアクセスでは既存のインスタンスが返される
            var session2 = UserSession.Instance;
            Console.WriteLine($"インスタンスの同一性確認: {object.ReferenceEquals(UserSession.Instance, session2)}");
        }
    }
}
実行結果
プログラム開始
UserSession インスタンスが生成されました。
User: TechWriter_01, LoginAt: 202X/XX/XX XX:XX:XX
インスタンスの同一性確認: True

Singletonパターンのメリットとデメリット

メリット

  • インターフェースの実装が可能:これにより、DIコンテナとの親和性が高まり、ユニットテスト時にモック(偽物)に差し替えることが可能になります。
  • 遅延初期化(Lazy Initialization):アプリケーション起動時ではなく、初めて必要になったタイミングでメモリを確保できます。
  • 状態の管理:インスタンスとして存在するため、ライフサイクル管理がしやすくなります。

デメリット

  • コード量が増える:staticクラスに比べると記述が複雑になります。
  • 隠れた依存関係:どこからでも ClassName.Instance でアクセスできてしまうため、クラス間の依存関係が見えにくくなり、スパゲッティコードの原因になることがあります。

staticクラスとSingletonの使い分け

どちらの手法を選択すべきかは、その「グローバル変数」がどのような役割を持つかによって決まります。

使い分けの基準表

特徴staticクラスSingletonパターン
主な用途状態を持たないユーティリティ、定数状態を持つ管理オブジェクト、共有リソース
インターフェース実装不可可能
継承不可可能
インスタンス化不要(クラス名でアクセス)必要(Instance プロパティでアクセス)
メモリ確保のタイミング型が初めて参照された時Instance が初めて呼ばれた時(Lazy使用時)
テストのしやすさ低い(差し替えが困難)高い(インターフェース化すれば容易)

staticクラスが適しているケース

  • Math.Abs() のように、入力に対して出力を返すだけの「純粋関数」をまとめる場合。
  • アプリケーション全体で不変の定数(設定値のキー名など)を定義する場合。

Singletonが適しているケース

  • データベース接続の管理や、ログ出力の制御など、リソースの競合を避けるためにインスタンスを1つに絞りたい場合。
  • 将来的に「グローバルではなく、特定の範囲内でのみ共有」する形に変更される可能性がある場合。

現代的な手法:DI(依存性の注入)による管理

近年の .NET(ASP.NET Core や .NET 8/9 以降)では、staticクラスや明示的なSingletonパターンを自前で実装する代わりに、DIコンテナによるシングルトンライフサイクル管理を行うのが主流です。

DIを使用すると、クラス内で ClassName.Instance と書く必要がなくなり、コンストラクタ経由で共有インスタンスを受け取ることができます。

これにより、グローバル変数の利便性を保ちつつ、テスタビリティを劇的に向上させることが可能です。

DIを用いたシングルトンの登録例

C#
// サービス(共有変数を持つクラス)の定義
public interface IGlobalSettings
{
    string Theme { get; set; }
}

public class GlobalSettings : IGlobalSettings
{
    public string Theme { get; set; } = "Dark";
}

// プログラム開始時の登録処理 (Generic Host等の場合)
// services.AddSingleton<IGlobalSettings, GlobalSettings>();

このように登録されたオブジェクトは、DIコンテナによって「唯一のインスタンス」として管理され、必要とするすべてのクラスに配布されます。

これが現代における最も推奨されるグローバル変数の形です。

グローバル変数を使用する際の注意点とベストプラクティス

グローバル変数は強力なツールですが、無計画な使用はプロジェクトの破綻を招きます。

以下の注意点を守って実装しましょう。

1. スレッドセーフティの確保

複数のスレッドから同時にグローバル変数にアクセスする場合、データの整合性が崩れる可能性があります。

書き換えが発生する場合は、lock 文を使用するか、ConcurrentDictionary などのスレッドセーフなコレクションを使用することを検討してください。

2. 状態の最小化

「何でもグローバルに入れる」のは避けてください。

本当にアプリケーション全体で共有する必要があるデータ(ユーザー認証情報、システム設定など)のみに限定し、それ以外は引数で渡すか、適切なスコープのインスタンスに持たせるべきです。

3. 命名規則の徹底

グローバル変数はどこからでも参照されるため、その変数が「共有されているものであること」が一目でわかるように命名を工夫しましょう。

  • 例:GlobalAppSettingsSharedUserContext など。

4. 読み取り専用の活用

可能であれば、値を変更できない readonlyconst を使用してください。

不変(Immutable)な状態は、デバッグの苦労を大幅に減らしてくれます。

まとめ

C#には言語仕様としてのグローバル変数は存在しませんが、staticクラスSingletonパターンを用いることで、同等の機能を実現できます。

  • 手軽に共通処理や定数を使いたい場合は staticクラス
  • 状態を持ち、将来的な拡張性やテストを重視する場合は Singletonパターン
  • 現代的な .NET アプリケーションであれば DI(依存性の注入)

これらを適切に使い分けることが、美しく保守性の高いコードへの第一歩となります。

グローバル変数の「どこからでもアクセスできる」というメリットは、裏を返せば「どこから書き換わったか分からなくなる」というリスクでもあります。

この特性を理解した上で、用途に応じた最適な実装パターンを選択してください。