C#にグローバル変数はない?staticで代用する方法と安全な設計を初心者向けに解説

はじめに

C#でプログラミングを始めると、「複数のクラスから同じ変数を使いたい」「グローバル変数を宣言したい」と考えることがあります。

しかし、C#にはC言語などで使われるような、クラスやメソッドの外側に直接宣言するグローバル変数はありません。代わりに、staticフィールドやstaticプロパティを利用すると、アプリケーション内で値を共有できます。

ただし、static変数をどこからでも自由に変更できる状態にすると、処理を追いにくくなったり、並列処理で不具合が発生したりする可能性があります。特にASP.NET CoreのWebアプリでは、ユーザーごとの情報をstaticに保存してはいけません。

この記事では、C#におけるグローバル変数の考え方、staticによる代用方法、安全な設計、Singletonや依存性注入との使い分けを初心者向けに解説します。

1. C#にグローバル変数は存在する?最初に知っておきたい結論

1-1. C#には言語仕様上のグローバル変数がない

C#には、どのクラスにも属さないグローバル変数を宣言する仕組みがありません。

たとえば、次のように名前空間の直下へ変数を書くことはできません。

namespace SampleApp{int score = 0; // コンパイルエラー}

C#では、フィールドをクラス、構造体、レコードなどの型の内部に宣言する必要があります。メソッド内で宣言した変数は、そのメソッドやブロックに属するローカル変数になります。

1-2. C#の変数は必ずクラスやメソッドなどのスコープに属する

スコープとは、変数を参照できる範囲のことです。

次のコードでは、messageShowMessageメソッド内でのみ使用できます。

public class MessageService{public void ShowMessage(){string message = "こんにちは";Console.WriteLine(message);}}

一方、クラス内にフィールドとして宣言した値は、そのクラスのインスタンスを通じて利用できます。

public class User{public string Name = "";}

C#では、すべての変数がローカルスコープ、インスタンス、型など、何らかの所有範囲に属します。

1-3. staticフィールドがグローバル変数の代わりとして使われる理由

staticを付けたフィールドは、個別のインスタンスではなく型そのものに属します。

そのため、クラスをnewで生成しなくても、次のようにクラス名からアクセスできます。

public static class GlobalData{public static int Score = 0;}

Console.WriteLine(GlobalData.Score);

GlobalData.Scoreは、同じアプリケーション内の複数のクラスから参照できます。この性質がグローバル変数に似ているため、C#ではstaticフィールドが代用として使われます。

静的クラスはインスタンス化できず、クラス名を使ってメンバーへアクセスします。また、静的メンバーは個別のオブジェクトではなく型に属します。

1-4. グローバル変数とstatic変数の違い

一般的なグローバル変数は、特定のクラスやオブジェクトに属さず、広い範囲から名前だけで参照できる変数です。

一方、C#のstatic変数はクラスや構造体などの型に属します。

GlobalData.Score = 100;

このように、基本的には型名.メンバー名という形でアクセスします。

つまり、staticフィールドはグローバル変数に近い使い方ができますが、実際には型のメンバーです。この違いを理解しておくと、C#らしい設計を行いやすくなります。

2. C#の変数スコープとstaticの基礎知識

2-1. ローカル変数・インスタンスフィールド・staticフィールドの違い

C#でよく使う変数は、主に次の3種類に分けられます。

種類宣言場所値を保持する単位主な用途
ローカル変数メソッドやブロック内メソッドの実行ごと一時的な計算結果
インスタンスフィールドクラス内オブジェクトごとユーザー名や商品の価格
staticフィールドクラス内型ごと共有設定や共通カウンター

具体例を見てみましょう。

public class Player{public string Name = "";       // インスタンスフィールドpublic static int PlayerCount; // staticフィールド

public void Move(){int distance = 10;         // ローカル変数Console.WriteLine(distance);}

}

Nameはプレイヤーごとに異なる値を持ちますが、PlayerCountはすべてのPlayerインスタンスで共有されます。

2-2. staticを付けるとインスタンス化せずにアクセスできる

通常のフィールドを使うには、クラスのインスタンスが必要です。

var user = new User();user.Name = "田中";

staticフィールドの場合は、インスタンスを作る必要がありません。

public class ApplicationState{public static bool IsMaintenanceMode;}

ApplicationState.IsMaintenanceMode = true;

手軽にアクセスできる反面、さまざまな場所から値を変更しやすくなる点には注意が必要です。

2-3. staticフィールドの値がアプリケーション内で共有される仕組み

インスタンスフィールドは、オブジェクトを生成するたびに別々の領域を持ちます。

var user1 = new User();var user2 = new User();

user1.Name = "佐藤";user2.Name = "鈴木";

一方、staticフィールドは型に対して保持されます。

public class User{public static int Count;}

User.Count++;

同じ実行環境で同じ型を参照しているコードは、基本的に同じUser.Countへアクセスします。

ただし、別々に起動したアプリケーション、別プロセス、複数台のWebサーバーでは値は共有されません。複数の実行環境で値を共有したい場合は、データベースや分散キャッシュなどが必要です。

2-4. public・private・internalによるアクセス範囲の違い

アクセス修飾子を使うと、フィールドやプロパティを利用できる範囲を制限できます。

public static class AppState{public static int PublicValue = 1;internal static int InternalValue = 2;private static int PrivateValue = 3;}

publicは、ほかのアセンブリを含む広い範囲からアクセスできます。

internalは、原則として同じアセンブリ内からアクセスできます。

privateは、宣言したクラスの内部からしかアクセスできません。

共有データを安全に扱うには、フィールドをprivateにして、必要な操作だけをプロパティやメソッドとして公開する設計が基本です。

2-5. staticクラスと通常クラスの使い分け

クラス全体が共通処理や共有データだけで構成される場合は、static classを利用できます。

public static class TemperatureConverter{public static double CelsiusToFahrenheit(double celsius){return celsius * 9 / 5 + 32;}}

一方、インスタンスごとに異なる状態を持たせたい場合は通常クラスを使います。

public class ShoppingCart{public List<string> Items { get; } = new();}

目安として、入力だけで結果が決まり、内部に変更可能な状態を持たない処理はstaticと相性がよいです。ユーザー、注文、カートなど、個別の状態を持つものは通常クラスとして設計します。

3. staticでグローバル変数を代用する基本的な方法

3-1. staticクラスに共有変数を定義する書き方

最も単純な書き方は、staticクラスの中にstaticフィールドを定義する方法です。

public static class GlobalData{public static int Score = 0;public static string CurrentUserName = "";}

staticクラス内のメンバーは、基本的にすべてstaticである必要があります。

ただし、上記のように公開フィールドを直接変更できる設計は、規模が大きくなるほど管理しにくくなります。学習用の小さなコード以外では、プロパティや専用メソッドを使う方法がおすすめです。

3-2. 別のクラスからstaticフィールドを参照・更新する方法

staticフィールドには、クラス名を指定してアクセスします。

public static class GlobalData{public static int Score = 0;}

public class Game{public void AddScore(){GlobalData.Score += 10;}}

public class ResultScreen{public void Show(){Console.WriteLine(GlobalData.Score);}}

Gameクラスで更新した値を、ResultScreenクラスから参照できます。

便利な反面、更新箇所が増えると「いつ、どの処理が値を変更したのか」を追いにくくなります。

3-3. 定数を共有するならconstを使う

コンパイル時から値が決まっていて、実行中に変更しない値にはconstを使います。

public static class AppConstants{public const int MaxRetryCount = 3;public const string ApplicationName = "Sample App";}

使用方法は次のとおりです。

Console.WriteLine(AppConstants.ApplicationName);

constは暗黙的に静的なメンバーとして扱われるため、static constとは書きません。

なお、公開ライブラリのpublic constは利用側のコードへ値が埋め込まれることがあります。後から値を変更した場合、利用側の再コンパイルが必要になる可能性があるため、将来変更される値には向いていません。

3-4. 実行時に決まる変更不可の値にはstatic readonlyを使う

実行時に値を決定し、その後は別の値を代入させたくない場合はstatic readonlyを使います。

public static class AppInfo{public static readonly DateTime StartedAt = DateTime.Now;}

環境変数や設定ファイルから値を取得することもできます。

public static class ConnectionSettings{public static readonly string ApiUrl =Environment.GetEnvironmentVariable("API_URL")?? "https://example.invalid";}

readonlyフィールドは、宣言時または対応するコンストラクター内で代入できます。static readonlyの場合は、宣言時または静的コンストラクターで初期化できます。

ただし、参照型にstatic readonlyを付けても、変更できないのは参照先そのものではなく、別オブジェクトへの再代入です。

public static readonly List<string> Names = new();

Names.Add("田中"); // リストの内容は変更できる

変更不可能なデータにしたい場合は、読み取り専用コレクションや不変コレクションも検討しましょう。

3-5. プロパティを使って値の変更を制御する

フィールドを直接公開する代わりに、プロパティを使うと更新範囲を制限できます。

public static class ApplicationState{public static int Score { get; private set; }

public static void AddScore(int points){if (points &lt; 0){throw new ArgumentOutOfRangeException(nameof(points));}Score += points;}

}

外部から値を読み取ることはできますが、直接代入することはできません。

ApplicationState.AddScore(10);Console.WriteLine(ApplicationState.Score);

// ApplicationState.Score = 100; // コンパイルエラー

値の検証やログ出力も専用メソッドへ集約できるため、安全性が高まります。

3-6. 名前空間をまたいで共有変数へアクセスする方法

別の名前空間にあるクラスへは、完全修飾名を使うか、usingを追加してアクセスします。

namespace SampleApp.Shared{public static class AppState{public static int Score { get; set; }}}

完全修飾名を使う例は次のとおりです。

SampleApp.Shared.AppState.Score = 100;

usingを追加すれば、クラス名だけでアクセスできます。

using SampleApp.Shared;

AppState.Score = 100;

using staticを使う方法もあります。

using static SampleApp.Shared.AppState;

Score = 100;

ただし、どのクラスのメンバーなのか分かりにくくなることがあるため、共有状態ではAppState.Scoreのようにクラス名を明示したほうが読みやすい場合があります。

4. static変数を使った実践的なコード例

4-1. アプリケーション全体で設定値を共有する例

変更されない設定値であれば、読み取り専用のプロパティとして公開できます。

public static class AppSettings{public static string ApplicationName { get; } = "Task Manager";public static int PageSize { get; } = 20;public static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(30);}

利用側は次のようになります。

Console.WriteLine(AppSettings.ApplicationName);Console.WriteLine(AppSettings.Timeout);

ただし、実際の業務アプリでは、設定値をコードへ直接書くより、設定ファイルや環境変数から読み込み、DIで受け渡す設計のほうが柔軟です。

4-2. ログイン中のユーザー情報を保持する例

コンソールアプリや単一ユーザー向けデスクトップアプリであれば、現在のユーザー情報を共有するクラスを作ることがあります。

public sealed record LoginUser(int Id, string Name);

public static class UserSession{public static LoginUser? CurrentUser { get; private set; }

public static bool IsLoggedIn =&gt; CurrentUser is not null;public static void Login(LoginUser user){CurrentUser = user ?? throw new ArgumentNullException(nameof(user));}public static void Logout(){CurrentUser = null;}

}

使用例は次のとおりです。

UserSession.Login(new LoginUser(1, "田中"));

if (UserSession.IsLoggedIn){Console.WriteLine(UserSession.CurrentUser?.Name);}

この方法は、同じプロセスを1人で使うアプリに限定するべきです。ASP.NET Coreなど、複数ユーザーのリクエストを同時に処理するWebアプリでは使用してはいけません。

4-3. カウンターや処理回数を共有する例

単純なカウンターはstaticフィールドで実装できますが、複数スレッドから更新される可能性がある場合はInterlockedを使います。

using System.Threading;

public static class RequestCounter{private static int _count;

public static int Count =&gt; Volatile.Read(ref _count);public static int Increment(){return Interlocked.Increment(ref _count);}public static void Reset(){Interlocked.Exchange(ref _count, 0);}

}

_count++は、読み取り、加算、書き込みが一体化した操作ではありません。複数スレッドから同時に実行すると、更新が失われる可能性があります。

4-4. Unityでゲームのスコアや状態を共有する例

Unityでは、シンプルなゲーム状態をstaticクラスで共有することがあります。

public static class GameState{public static int Score { get; private set; }public static bool IsGameOver { get; private set; }

public static void AddScore(int points){if (!IsGameOver){Score += points;}}public static void FinishGame(){IsGameOver = true;}public static void Reset(){Score = 0;IsGameOver = false;}

}

シーン開始時にResetを呼ばないと、前回の値が残った状態でゲームが始まる可能性があります。

規模が大きくなった場合は、ゲーム設定や共有データをScriptableObjectへ分離する方法もあります。シーンをまたいで特定のGameObjectを保持したい場合は、DontDestroyOnLoadも選択肢です。Unityの公式ドキュメントでも、ScriptableObjectはデータを保持するアセットとして利用でき、DontDestroyOnLoadはシーン読み込み時の破棄を防ぐ機能として説明されています。

4-5. コンソール・デスクトップ・Webアプリで挙動が異なる点

staticの基本的な仕組みは同じですが、アプリの実行形態によって影響が異なります。

コンソールアプリでは、通常はプロセスの開始から終了まで値が保持されます。

デスクトップアプリでは、ウィンドウを閉じてもプロセスが終了していなければ、staticの値が残ることがあります。

Webアプリでは、同じサーバープロセスが複数ユーザーのリクエストを処理します。そのため、staticフィールドはユーザー単位ではなく、サーバープロセス内で共有されます。

また、Webアプリを複数台のサーバーや複数プロセスで動かしている場合、それぞれが別々のstatic値を持ちます。永続化やサーバー間共有が必要なデータには、データベースや分散キャッシュを使います。

5. staticをグローバル変数のように使うデメリット

5-1. どこからでも変更できると処理を追いにくくなる

共有変数を多数のクラスから変更すると、値が変化した原因を特定しにくくなります。

GlobalData.Status = "Running";

このような代入がプロジェクト全体に散らばっていると、不具合が発生したときにすべての更新箇所を調査しなければなりません。

値の変更は、意味の分かる専用メソッドへ集約することが大切です。

ApplicationState.StartProcessing();

5-2. 値が意図せず書き換えられる危険がある

公開された可変フィールドには、どこからでも不正な値を代入できます。

public static int RetryCount;

次のような値も設定できてしまいます。

RetryCount = -100;

プロパティやメソッドを使って、許可する値を検証しましょう。

5-3. クラス同士の依存関係が強くなる

クラス内でGlobalDataを直接参照すると、そのクラスはGlobalDataなしでは動作しにくくなります。

public class OrderService{public void Execute(){string userName = GlobalData.UserName;}}

このような隠れた依存関係が増えると、クラスの再利用や変更が難しくなります。

コンストラクターやメソッドの引数として依存する値を渡せば、必要な情報が明確になります。

5-4. 単体テストが難しくなる

テスト同士が同じstatic状態を利用すると、実行順序によって結果が変わることがあります。

GlobalData.Score = 100;

あるテストが値を元に戻し忘れると、次のテストが100から始まってしまいます。テストを並列実行した場合は、さらに不安定になりやすくなります。

テストしやすさを重視するなら、インターフェースとDIを利用して依存オブジェクトを差し替えられるようにします。

5-5. staticフィールドが保持され続けることによるメモリ上の注意点

staticフィールドがオブジェクトを参照している間、そのオブジェクトは不要になってもガベージコレクションの対象にならない場合があります。

public static class Cache{public static readonly List<byte[]> Items = new();}

このリストへ大きなデータを追加し続けると、プロセスのメモリ使用量が増加します。

staticを使っただけでメモリリークになるわけではありませんが、データを保持し続けるキャッシュには、件数上限、期限、削除処理などが必要です。

5-6. 並列処理やマルチスレッドで競合が発生する

複数のスレッドが同じ値を同時に読み書きすると、競合状態が発生します。

public static int Count;

Count++;

Count++はスレッドセーフではありません。正確なカウントが必要なら、Interlocked.Incrementlockを使います。

ただし、ロックを追加すればすべて解決するわけではありません。複数の値をまとめて更新する場合は、処理全体の整合性を考える必要があります。

5-7. ASP.NET Coreでユーザーごとの情報をstaticに保存してはいけない理由

ASP.NET Coreでは、1つのアプリケーションプロセスが複数ユーザーのリクエストを同時に処理します。

次のような実装は危険です。

public static class CurrentUser{public static int UserId { get; set; }}

ユーザーAのリクエストでUserIdを設定した直後に、ユーザーBのリクエストが値を書き換える可能性があります。その結果、別ユーザーの情報を参照する重大な不具合につながります。

ユーザーごとの情報には、認証情報、HttpContext、セッション、リクエスト単位のScopedサービスなどを利用します。ASP.NET CoreのDIでは、Transient、Scoped、Singletonというライフタイムが提供されています。

6. static変数を安全に設計するためのポイント

6-1. publicフィールドを直接公開しない

次のような公開フィールドは避けましょう。

public static int Score;

外部から自由に変更できるため、値の検証や変更履歴の記録が難しくなります。

基本的にはprivateフィールドとプロパティ、専用メソッドを組み合わせます。

6-2. privateフィールドとプロパティで読み書きを制限する

外部からは読み取りだけを許可し、更新はクラス内部に限定できます。

public static class ScoreManager{private static int _score;

public static int Score =&gt; _score;public static void Add(int points){if (points &lt;= 0){throw new ArgumentOutOfRangeException(nameof(points));}_score += points;}

}

ScoreManager.Scoreへ直接代入できないため、不正な変更を防ぎやすくなります。

6-3. 変更不要な値はconstまたはstatic readonlyにする

値の性質に応じて使い分けます。

public const int MaxUsers = 100;public static readonly DateTime StartedAt = DateTime.Now;

コンパイル時に確定する単純な定数にはconst、実行時に決まる値やオブジェクトにはstatic readonlyが適しています。

将来変更される可能性がある公開値や、配列、クラスのインスタンスなどはconstにできないため、static readonlyを検討します。

6-4. 値を変更する処理を専用メソッドに集約する

単純なsetを公開するより、操作の意味を表すメソッドを用意すると安全です。

public static class SystemStatus{public static bool IsRunning { get; private set; }

public static void Start(){if (IsRunning){return;}IsRunning = true;}public static void Stop(){IsRunning = false;}

}

更新処理を集約すれば、入力チェック、ログ記録、イベント通知などを後から追加しやすくなります。

6-5. 共有するデータを必要最小限に絞る

すべての情報を1つのGlobalDataクラスへ集める設計は避けましょう。

public static class GlobalData{public static string UserName;public static int Score;public static string ConnectionString;public static List<string> Logs;}

責務ごとに分割すると管理しやすくなります。

public static class AppConstants{public const int MaxRetryCount = 3;}

public static class ScoreManager{// スコアだけを管理}

共有する必要がない値は、ローカル変数やインスタンスフィールドに戻すことも重要です。

6-6. 命名規則を統一して共有状態だと分かるようにする

クラス名から役割が分かるようにします。

AppConstantsApplicationStateScoreManagerRequestCounter

意味が広すぎるGlobalCommonだけの名前は、データが増え続ける原因になりがちです。

また、非公開フィールドには_scoreのような命名を使うなど、プロジェクト内で規則を統一すると読みやすくなります。

6-7. lockやInterlockedでスレッドセーフにする

複数の値や複雑な処理をまとめて保護する場合は、lockを使います。

public static class BankAccount{private static readonly object _syncRoot = new();private static decimal _balance;

public static decimal Balance{get{lock (_syncRoot){return _balance;}}}public static void Deposit(decimal amount){if (amount &lt;= 0){throw new ArgumentOutOfRangeException(nameof(amount));}lock (_syncRoot){_balance += amount;}}

}

単純な整数の加算や交換であれば、Interlockedを利用できます。

private static int _count;

public static int Increment(){return Interlocked.Increment(ref _count);}

ロック用オブジェクトは外部へ公開せず、private static readonlyとして管理しましょう。

6-8. コレクションを共有する場合はConcurrentDictionaryなどを検討する

通常のDictionary<TKey, TValue>は、複数スレッドからの同時更新を前提としていません。

同時に読み書きする場合は、ConcurrentDictionary<TKey, TValue>などを検討します。

using System.Collections.Concurrent;

public static class UserCache{private static readonly ConcurrentDictionary<int, string> _users = new();

public static void AddOrUpdate(int id, string name){_users.AddOrUpdate(id, name, (_, _) =&gt; name);}public static bool TryGet(int id, out string? name){return _users.TryGetValue(id, out name);}

}

System.Collections.Concurrentには、複数スレッドから利用するためのコレクションが用意されています。

ただし、コレクションの各メソッドがスレッドセーフでも、複数のメソッドを組み合わせた一連の業務処理まで自動的にアトミックになるわけではありません。

7. static以外でデータを共有する設計方法

7-1. 必要な値をメソッドの引数で渡す

最も単純で分かりやすい方法は、必要な値を引数として渡すことです。

public decimal CalculateTotal(decimal price, int quantity){return price * quantity;}

共有状態へ依存しないため、処理の入力と出力が明確になります。単体テストも簡単です。

7-2. インスタンスを生成してフィールドやプロパティに保持する

複数の処理で同じ状態を使いたい場合は、状態を持つクラスを作ります。

public class ShoppingSession{public string UserName { get; }public List<string> Items { get; } = new();

public ShoppingSession(string userName){UserName = userName;}

}

必要なクラスへ同じインスタンスを渡せば、staticを使わずにデータを共有できます。

var session = new ShoppingSession("田中");

var cartService = new CartService(session);var checkoutService = new CheckoutService(session);

7-3. 依存性注入(DI)で共有サービスを受け渡す

依存性注入では、クラスが必要とするサービスを外部から渡します。

public interface IClock{DateTime Now { get; }}

public sealed class SystemClock : IClock{public DateTime Now => DateTime.Now;}

public class ReportService{private readonly IClock _clock;

public ReportService(IClock clock){_clock = clock;}public string CreateReport(){return $"作成日時: {_clock.Now}";}

}

テストでは、固定日時を返す偽物へ差し替えられます。

public sealed class FixedClock : IClock{public DateTime Now { get; } = new(2026, 1, 1);}

staticを直接参照する設計より、依存関係が明確になり、テストしやすくなります。

7-4. Singletonパターンを利用する

Singletonは、クラスのインスタンスを1つだけ提供する設計パターンです。

public sealed class AppState{private static readonly Lazy<AppState> _instance =new(() => new AppState());

public static AppState Instance =&gt; _instance.Value;public int Counter { get; private set; }private AppState(){}public void Increment(){Counter++;}

}

利用側は次のようになります。

AppState.Instance.Increment();

Singletonはインスタンスを持つため、インターフェースを実装したり、通常のコンストラクターやインスタンスメソッドを利用したりできます。

ただし、AppState.Instanceをコード全体から直接参照すると、staticと同じような隠れた依存関係が生まれます。可能であれば、DIコンテナでSingletonとして登録する方法を検討しましょう。

7-5. 設定値はappsettings.jsonや環境変数で管理する

接続先URL、機能の有効・無効、タイムアウトなどの設定値は、ソースコードへ直接書かずに外部化すると管理しやすくなります。

{"ApiSettings": {"BaseUrl": "https://api.example.com","TimeoutSeconds": 30}}

設定を受け取るクラスを定義します。

public sealed class ApiSettings{public string BaseUrl { get; set; } = "";public int TimeoutSeconds { get; set; }}

ASP.NET Coreでは、appsettings.json、環境別の設定ファイル、環境変数などを組み合わせて構成を読み込めます。環境ごとに値を切り替えられるため、staticフィールドへ設定を直接埋め込むより柔軟です。

パスワードやAPIキーなどの秘密情報は、公開される設定ファイルへ平文で保存しないようにしましょう。

7-6. ASP.NET CoreではDIのSingleton・Scoped・Transientを使い分ける

ASP.NET CoreのDIでは、サービスの寿命を選択できます。

builder.Services.AddSingleton<IAppCache, AppCache>();builder.Services.AddScoped<IUserService, UserService>();builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();

Singletonは、アプリケーション内で同じインスタンスを共有します。変更可能な状態を持つ場合は、スレッドセーフな設計が必要です。

Scopedは、通常のWebアプリでは1リクエスト内で同じインスタンスを使用します。データベース処理やユーザー単位の処理でよく使われます。

Transientは、サービスが要求されるたびに新しいインスタンスを作ります。軽量で状態を持たない処理に向いています。

ユーザーごとの情報を扱うサービスは、安易にSingletonへ登録しないことが重要です。

7-7. UnityではScriptableObjectやDontDestroyOnLoadを検討する

Unityで設定データやゲームバランス値を共有する場合は、ScriptableObjectを利用できます。

using UnityEngine;

[CreateAssetMenu(menuName = "Game/Game Settings")]public class GameSettings : ScriptableObject{public int InitialLives = 3;public float PlayerSpeed = 5f;}

Inspectorから設定できるため、コードを変更せずに値を調整しやすくなります。

シーンをまたいで特定の管理オブジェクトを保持する場合は、DontDestroyOnLoadを利用できます。

using UnityEngine;

public class GameManager : MonoBehaviour{private void Awake(){DontDestroyOnLoad(gameObject);}}

ただし、シーンを読み込むたびに同じ管理オブジェクトが生成されると重複します。既存インスタンスを検出して破棄するなどの対策が必要です。

8. static・Singleton・DIの使い分け

8-1. staticが適しているケース

staticが適しているのは、次のようなケースです。

  • 数学関数や変換処理など、状態を持たない共通処理

  • コンパイル時に決まる定数

  • 実行中に変更されない読み取り専用情報

  • ごく小規模なプログラムの単純な共有状態

  • スレッドセーフに実装された共通カウンター

たとえば、温度変換処理は入力によって結果が決まるため、staticメソッドと相性がよいでしょう。

public static double ToFahrenheit(double celsius){return celsius * 9 / 5 + 32;}

8-2. Singletonが適しているケース

Singletonは、インスタンスとして管理したいものの、アプリケーション内で1つだけ存在させたいサービスに適しています。

例としては、共有キャッシュ、アプリケーション全体の監視サービス、作成コストが高いサービスなどがあります。

ただし、変更可能な状態を持つSingletonは、同時アクセスとテストのしやすさを考慮する必要があります。手動でInstanceを公開するより、DIコンテナでSingletonとして登録したほうが依存関係を管理しやすくなります。

8-3. DIが適しているケース

DIは、中規模以上のアプリケーションや、テスト、保守、機能追加を重視する開発に適しています。

特に次のような場合に有効です。

  • データベースや外部APIへアクセスする

  • 本番用とテスト用の実装を切り替えたい

  • クラスの依存関係を明確にしたい

  • サービスごとに寿命を管理したい

  • ASP.NET Coreを利用している

DIは記述量が少し増えますが、規模が大きくなるほど利点が大きくなります。

8-4. 小規模プログラムと大規模開発で選び方が変わる理由

数十行程度の学習用プログラムでは、staticを使うことで簡単に実装できます。

一方、大規模なシステムでは、同じ共有データを多くのクラスから変更すると、影響範囲が広がります。複数人で開発する場合は、誰がどの値を変更するのかも把握しにくくなります。

小規模なコードでは簡潔さ、大規模なコードでは依存関係の明確さや変更容易性を重視して選びましょう。

8-5. テストのしやすさを基準に選ぶ方法

迷ったときは、「テスト時に値や実装を簡単に差し替えられるか」を考えると判断しやすくなります。

方法手軽さ状態の管理テストのしやすさ主な用途
static高い難しくなりやすい低め定数、純粋な共通処理
手動Singleton中程度注意が必要低め小規模な単一インスタンス
DIのSingleton中程度管理しやすい高いアプリ全体の共有サービス
DIのScoped中程度範囲ごとに分離高いWebリクエスト単位の処理
通常インスタンス中程度オブジェクトごと高い個別の状態を持つ処理

時刻、ファイル、データベース、外部API、ログインユーザーなど、テスト時に差し替えたくなる可能性がある機能は、staticよりDIを選ぶとよいでしょう。

9. C#のグローバル変数でよくある疑問

9-1. Program.csで宣言した変数はグローバルに使える?

トップレベルステートメントを使うと、Program.csへ次のように書けます。

int score = 100;Console.WriteLine(score);

見た目はクラスやメソッドの外側にありますが、scoreがグローバル変数になるわけではありません。コンパイラによって生成されるエントリーポイントの処理に属するローカル変数として扱われます。

別のクラスから直接scoreを参照することはできません。共有したい場合は、クラスのフィールド、DIサービス、引数などを利用します。トップレベルステートメントは、ProgramクラスやMainメソッドを明示せずにエントリーポイントを記述するための構文です。

9-2. namespaceの直下に変数を宣言できる?

できません。

namespace SampleApp{int count = 0; // コンパイルエラー}

変数やフィールドは、メソッド、クラス、構造体など、適切な宣言場所へ記述する必要があります。

名前空間は型の名前を整理するための仕組みであり、値を保持するオブジェクトではありません。

9-3. static変数はいつ初期化される?

staticフィールドに初期値を書いた場合、型の初期化処理によって設定されます。

public class Sample{public static int Value = 10;}

静的コンストラクターを使うこともできます。

public class Sample{public static readonly int Value;

static Sample(){Value = LoadValue();}private static int LoadValue(){return 10;}

}

静的コンストラクターは、最初のインスタンスが作られる前、または静的メンバーが参照される前にランタイムから呼び出され、多くても1回実行されます。

ただし、明示的な静的コンストラクターがない型では、実際の初期化タイミングがランタイムによって最適化される場合があります。コードでは、必要な時点までに正しく初期化されることを前提にし、厳密な実行時刻へ依存しないようにしましょう。

9-4. static変数の値はいつまで保持される?

一般的なアプリでは、型が読み込まれてからプロセスが終了するまで保持されます。

ただし、次のような場合は値が失われます。

  • コンソールアプリやデスクトップアプリを終了した

  • Webアプリのプロセスが再起動した

  • サーバーが再起動した

  • アプリケーションが再デプロイされた

  • 対象の読み込みコンテキストがアンロードされた

永続的に保存したい値には、ファイル、データベース、クラウドストレージなどを使います。

9-5. static変数を初期値に戻す方法は?

自動的に初期状態へ戻す機能はないため、リセット用メソッドを用意します。

public static class GameState{public static int Score { get; private set; }public static bool IsGameOver { get; private set; }

public static void Reset(){Score = 0;IsGameOver = false;}

}

テストやゲームの再スタート時にResetを呼びます。

ただし、リセットを忘れやすい設計になっている場合は、状態を持つ通常クラスのインスタンスを新しく生成する方法も検討しましょう。

9-6. static変数は継承できる?

staticメンバーはインスタンスメンバーのようにオーバーライドされません。

基底クラスのstaticメンバーを派生クラス名から参照できる場合はありますが、メンバー自体は基底クラスに属したままです。

public class BaseClass{public static int Value = 10;}

public class DerivedClass : BaseClass{}

Console.WriteLine(DerivedClass.Value);

派生クラス側で同じ名前のstaticメンバーを宣言すると、オーバーライドではなく名前の隠蔽になります。

public class DerivedClass : BaseClass{public new static int Value = 20;}

また、static class自体はインスタンス化できず、通常のクラスのような継承元や派生クラスとしては利用できません。

9-7. staticとconstはどちらを使うべき?

staticconstは役割が異なります。

constは、コンパイル時に確定し、絶対に変わらない値へ使います。

public const int DaysInWeek = 7;

static readonlyは、実行時に決まり、その後は再代入しない値へ使います。

public static readonly DateTime StartedAt = DateTime.Now;

変更可能な共有状態には、staticフィールドやプロパティを使えます。

public static int CurrentCount { get; private set; }

判断基準は次のとおりです。

  • コンパイル時に値が決まるならconst

  • 実行時に値が決まり、再代入しないならstatic readonly

  • 実行中に変更する共有状態ならstatic

  • ユーザーや処理ごとに異なるなら通常のインスタンスやDI

9-8. グローバル変数を使わずに複数クラスで値を共有するには?

主な方法は、引数、同じインスタンス、DI、設定オブジェクト、データベースなどです。

小さな処理では、引数として渡す方法が最も分かりやすいでしょう。

var settings = new AppSettings();

var serviceA = new ServiceA(settings);var serviceB = new ServiceB(settings);

ASP.NET CoreではDIを利用し、必要なライフタイムでサービスを登録します。

アプリの再起動後も値を残す場合や、複数プロセスで共有する場合は、staticではなくデータベースや外部ストレージを利用します。

まとめ

C#には、クラスやメソッドに属さない言語仕様上のグローバル変数はありません。アプリケーション全体から値を参照したい場合は、staticフィールドやstaticプロパティを利用できます。

ただし、public staticフィールドをどこからでも変更できる設計には、意図しない書き換え、依存関係の複雑化、テストの難しさ、マルチスレッドでの競合といった問題があります。

安全に設計するには、フィールドをprivateにし、プロパティや専用メソッドで操作を制限しましょう。変更しない値にはconstまたはstatic readonlyを使い、並列処理ではlockInterlockedConcurrentDictionaryなどを検討します。

また、ログインユーザーや注文情報など、利用者や処理ごとに異なるデータはstaticへ保存してはいけません。通常クラス、引数、依存性注入、Scopedサービスなどを使い、適切な範囲で状態を管理します。

staticは便利な機能ですが、C#のグローバル変数として無条件に使うものではありません。「本当にアプリケーション全体で共有する必要があるか」「テスト時に差し替える必要があるか」「複数スレッドから更新されるか」を確認し、static、Singleton、DIを使い分けることが重要です。