C#にグローバル関数はある?書き方・代替方法・staticとの違いを初心者向けに解説

はじめに

C#で共通処理を作ろうとしたとき、「クラスに属さないグローバル関数を定義できないのか」と疑問に思う人は少なくありません。C言語やJavaScript、Pythonなどの経験があると、ファイルの直下に関数を書き、どこからでも呼び出したくなることがあります。

しかし、C#にはC言語のようなグローバル関数はありません。基本的に、メソッドはクラスや構造体などの型に所属させて定義します。

その代わり、C#では次のような機能を利用できます。

  • staticメソッド

  • static class

  • using static

  • ローカル関数

  • 拡張メソッド

  • トップレベルステートメント

この記事では、C#におけるグローバル関数の考え方、代替方法、staticメソッドとの違いを初心者向けに解説します。実践的なコード例に加えて、設計上の注意点や使い分けも紹介します。

1. C#にグローバル関数はある?結論と基本ルール

1-1. C#にはC言語のようなグローバル関数は存在しない

結論として、C#には特定のクラスや構造体に所属しない、独立したグローバル関数は存在しません。

C言語では、次のように関数をファイルの直下へ定義できます。

C
int Add(int x, int y)
{
return x + y;
}

一方、従来のC#では、同じように名前空間や型の外側へメソッドだけを定義することはできません。

C#
// 通常のC#コードとしては定義できない
int Add(int x, int y)
{
return x + y;
}

C#のメソッドは、原則としてクラス、構造体、レコードなどの型に所属します。トップレベルステートメントでは一見するとファイル直下に関数を書けますが、後述するように、それはプログラム全体から利用できるグローバル関数ではなくローカル関数として扱われます。Microsoft Learn+2Microsoft Learn+2

1-2. C#のメソッドは原則として型の中に定義する

一般的なC#プログラムでは、メソッドを次のようにクラス内へ定義します。

C#
public class Calculator
{
public int Add(int x, int y)
{
return x + y;
}
}

このAddCalculatorクラスのインスタンスメソッドです。呼び出すには、基本的にインスタンスを作成します。

C#
var calculator = new Calculator();
int result = calculator.Add(10, 20);

Console.WriteLine(result);

一方、オブジェクトごとの状態を必要としない処理は、staticメソッドとして定義できます。

C#
public class Calculator
{
public static int Add(int x, int y)
{
return x + y;
}
}

staticメソッドは特定のインスタンスではなく型自体に所属するため、クラス名を使って呼び出します。Microsoft Learn+1

C#
int result = Calculator.Add(10, 20);

1-3. グローバル関数の代わりに使える主な書き方

C#でグローバル関数のような共通処理を実現する代表的な方法は、次のとおりです。

方法向いている用途
staticメソッド状態を持たない共通処理
static class関連するstaticメソッドの集約
using staticクラス名を省略した簡潔な呼び出し
ローカル関数特定のメソッド内だけで使う補助処理
拡張メソッド既存の型に操作を追加したように見せる
インスタンスメソッドオブジェクトの状態を利用する処理
DIを使ったサービス外部システムに依存する処理

最も単純な代替方法はstaticメソッドですが、すべての処理をstaticにすればよいわけではありません。処理が状態を持つか、外部サービスに依存するか、テスト時に差し替える必要があるかを考えて選ぶことが重要です。

2. C#でグローバル関数のように呼び出す方法

2-1. staticメソッドをクラスに定義する

グローバル関数に近い使い方をしたい場合は、メソッドをstaticとして定義します。

C#
public class MathHelper
{
public static int Add(int x, int y)
{
return x + y;
}
}

呼び出し側ではnewを使わず、クラス名から直接呼び出します。

C#
int result = MathHelper.Add(3, 5);

Console.WriteLine(result);

MathHelper.Addはどのインスタンスにも依存しません。引数として渡された値だけを使って結果を返すため、staticメソッドに適した処理です。

2-2. staticメソッドの基本的な書き方と呼び出し方

staticメソッドの基本構文は次のとおりです。

C#
アクセス修飾子 static 戻り値の型 メソッド名(引数)
{
// 処理
}

文字列を整形する例を見てみましょう。

C#
public class TextFormatter
{
public static string CreateGreeting(string name)
{
return $"こんにちは、{name}さん";
}
}

呼び出し方は次のとおりです。

C#
string message = TextFormatter.CreateGreeting("田中");

Console.WriteLine(message);

短い処理であれば、式形式のメソッドとして記述することもできます。

C#
public class MathHelper
{
public static int Add(int x, int y) => x + y;
}

staticメソッドは型自体に所属するメンバーであり、特定のオブジェクトを生成せずに利用できます。Microsoft Learn+1

2-3. static classを使って共通処理をまとめる

共通処理だけをまとめるクラスでは、クラス自体をstaticにできます。

C#
public static class MathHelper
{
public static int Add(int x, int y)
{
return x + y;
}

public static int Subtract(int x, int y)
{
return x - y;
}
}

利用方法は通常のstaticメソッドと同じです。

C#
int total = MathHelper.Add(10, 5);
int difference = MathHelper.Subtract(10, 5);

static classにはインスタンスメンバーを定義できず、newによるインスタンス化もできません。そのため、「このクラスは状態を持たない共通処理だけを提供する」という意図を明確にできます。Microsoft Learn+2Microsoft Learn+2

C#
// コンパイルエラー
var helper = new MathHelper();

2-4. using staticでクラス名を省略して呼び出す

using staticを使うと、指定した型のstaticメンバーをクラス名なしで呼び出せます。

例えば、次のクラスがあるとします。

C#
namespace MyApp.Utilities;

public static class MathHelper
{
public static int Add(int x, int y)
{
return x + y;
}
}

呼び出し側でusing staticを指定します。

C#
using static MyApp.Utilities.MathHelper;

int result = Add(10, 20);

Console.WriteLine(result);

見た目だけなら、Addがグローバル関数であるように感じられます。しかし、実際にはMathHelperクラスのstaticメソッドです。

using staticは型のstaticメンバーを現在のスコープへ取り込み、型名による修飾を省略できる機能です。本当のグローバル関数を作る機能ではありません。Microsoft Learn+2Microsoft Learn+2

2-5. 名前空間を使ってメソッドの衝突を防ぐ

複数のクラスに同名のメソッドがあると、using staticによって呼び出しが曖昧になることがあります。

C#
namespace Sales.Utilities;

public static class PriceCalculator
{
public static decimal Calculate(decimal price)
{
return price * 1.1m;
}
}
C#
namespace Shipping.Utilities;

public static class PriceCalculator
{
public static decimal Calculate(decimal price)
{
return price + 500m;
}
}

クラス名まで指定すれば、どちらの処理か明確です。

C#
decimal salesPrice =
Sales.Utilities.PriceCalculator.Calculate(1000m);

decimal shippingPrice =
Shipping.Utilities.PriceCalculator.Calculate(1000m);

名前空間には、型を分類し、同じ名前を持つ型の衝突を避ける役割があります。共通処理を作るときは、単にUtilityへ集めるのではなく、MyApp.TextMyApp.Pricingのように責務ごとに名前空間を分けると管理しやすくなります。

3. C#のグローバル関数とstaticメソッドの違い

3-1. グローバル関数とstaticメソッドの定義上の違い

一般的なグローバル関数は、特定の型に所属せず、プログラムやモジュールの広い範囲から呼び出せる関数を指します。

一方、C#のstaticメソッドは必ず型に所属します。

C#
public static class GlobalFunctions
{
public static void PrintMessage(string message)
{
Console.WriteLine(message);
}
}

呼び出し方は次のとおりです。

C#
GlobalFunctions.PrintMessage("Hello");

using staticを使えばクラス名を省略できますが、メソッドがGlobalFunctions型に所属している事実は変わりません。

C#
using static GlobalFunctions;

PrintMessage("Hello");

したがって、C#で「グローバル関数」と呼ばれているものの多くは、実際にはstaticメソッド、ローカル関数、または拡張メソッドです。

3-2. インスタンスメソッドとstaticメソッドの違い

インスタンスメソッドとstaticメソッドの主な違いは、オブジェクトの状態に所属するかどうかです。

C#
public class BankAccount
{
public decimal Balance { get; private set; }

public void Deposit(decimal amount)
{
Balance += amount;
}

public static bool IsValidAmount(decimal amount)
{
return amount > 0;
}
}

Depositは特定の口座のBalanceを変更するため、インスタンスメソッドです。

C#
var account = new BankAccount();
account.Deposit(1000m);

IsValidAmountは特定の口座状態を必要とせず、渡された金額だけで判定できるため、staticメソッドにできます。

C#
bool isValid = BankAccount.IsValidAmount(1000m);

整理すると、次のようになります。

項目staticメソッドインスタンスメソッド
所属先型自体個々のオブジェクト
インスタンス生成不要原則として必要
インスタンス状態へのアクセス直接はできないできる
呼び出し方型名.メソッド名()変数名.メソッド名()
向いている処理状態を持たない計算や変換オブジェクトの状態を扱う処理

3-3. staticメソッドからインスタンスメンバーを直接使えない理由

staticメソッドには、対象となるインスタンスがありません。そのため、インスタンスフィールドやインスタンスプロパティを直接参照できません。

C#
public class User
{
public string Name { get; set; } = "";

public static void PrintName()
{
// コンパイルエラー
Console.WriteLine(Name);
}
}

Nameが「どのUserオブジェクトの名前なのか」を特定できないためです。

インスタンスを引数として受け取ればアクセスできます。

C#
public class User
{
public string Name { get; set; } = "";

public static void PrintName(User user)
{
Console.WriteLine(user.Name);
}
}
C#
var user = new User
{
Name = "佐藤"
};

User.PrintName(user);

ただし、処理が常にUserの状態を使うなら、インスタンスメソッドにしたほうが自然です。

C#
public class User
{
public string Name { get; set; } = "";

public void PrintName()
{
Console.WriteLine(Name);
}
}

3-4. staticメソッドをグローバル関数の代替にするメリット

staticメソッドには、次のようなメリットがあります。

まず、インスタンス生成が不要です。

C#
int result = MathHelper.Add(1, 2);

次に、どのクラスが処理を提供しているかが明確です。

C#
PasswordValidator.IsValid(password);
DateFormatter.ToJapaneseDate(date);
PriceCalculator.CalculateTax(price);

さらに、入力だけで結果が決まり、副作用を持たないstaticメソッドは動作を理解しやすく、単体テストも書きやすい傾向があります。

C#
public static decimal CalculateTax(decimal price, decimal rate)
{
return price * rate;
}

このような処理は、現在時刻、ファイル、データベース、共有状態などに依存しません。入力と期待結果をそのままテストできます。

3-5. staticメソッドを増やしすぎるデメリット

staticメソッドは便利ですが、増やしすぎると設計上の問題が生じます。

例えば、次のstaticメソッドは内部で現在時刻を直接取得しています。

C#
public static bool IsCampaignActive()
{
return DateTime.Now.Month == 12;
}

この処理はテストを実行する日時によって結果が変わります。また、別の時計実装へ簡単に差し替えられません。

データベースや外部APIへのアクセスをstaticメソッドの内部へ埋め込むと、依存関係が見えにくくなります。

C#
public static User FindUser(int id)
{
// 内部でデータベースへ接続する処理
}

さらに、staticフィールドで状態を共有すると、どの処理がいつ値を変更したのか追跡しにくくなります。

C#
public static class ApplicationState
{
public static int CurrentUserId;
}

staticメソッドが問題なのではなく、変更可能な共有状態や外部依存を隠す使い方が問題です。

4. トップレベルステートメントはグローバル関数として使える?

4-1. トップレベルステートメントとは

トップレベルステートメントは、ProgramクラスやMainメソッドを明示せず、ファイルの直下へ実行コードを書ける機能です。

C#
Console.WriteLine("Hello, C#!");

従来は、同じ処理を次のように記述していました。

C#
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello, C#!");
}
}

トップレベルステートメントを使うと、特に小規模なコンソールアプリケーションで定型コードを減らせます。現在の.NETのC#コンソールアプリ用テンプレートでも利用されている書き方です。Microsoft Learn+2Microsoft Learn+2

4-2. ProgramクラスとMainメソッドを省略できる仕組み

トップレベルステートメントを使っても、プログラムのエントリーポイントという仕組みがなくなるわけではありません。

次のコードがあるとします。

C#
Console.WriteLine("Start");

概念的には、コンパイラによってエントリーポイントを持つコードとして扱われます。

C#
internal class Program
{
private static void Main(string[] args)
{
Console.WriteLine("Start");
}
}

実際に生成される型名やメソッドの詳細をアプリケーション側から前提にするべきではありませんが、「ファイル直下のコードが完全なグローバル領域で実行されているわけではない」と理解することが重要です。

MicrosoftのC#ドキュメントでも、トップレベルステートメントを使用するアプリケーションでは、コンパイラがMainメソッドを生成し、その中にトップレベルの処理を含めると説明されています。Microsoft Learn+1

4-3. トップレベルステートメント内で関数を定義する方法

トップレベルステートメントでは、次のように関数のような処理を定義できます。

C#
int Add(int x, int y)
{
return x + y;
}

int result = Add(10, 20);

Console.WriteLine(result);

短く書くことも可能です。

C#
int Add(int x, int y) => x + y;

Console.WriteLine(Add(10, 20));

見た目はグローバル関数ですが、別のクラスから自由に呼び出せるわけではありません。

C#
public class Sample
{
public void Execute()
{
// トップレベルで定義したAddはここから直接呼び出せない
// int result = Add(1, 2);
}
}

4-4. トップレベルに書いた関数がローカル関数として扱われる理由

トップレベルステートメント内で定義した関数は、生成されるエントリーポイント内のローカル関数として扱われます。

ローカル関数とは、別のメソッドやメンバーの内部に定義され、その包含範囲からだけ呼び出せる関数です。Microsoft Learn+1

例えば、次のコードにおけるAddは、トップレベルの実行コンテキスト内でのみ使用できます。

C#
int Add(int x, int y)
{
return x + y;
}

Console.WriteLine(Add(1, 2));

アプリケーション内の複数のクラスから共通利用したい場合は、staticメソッドとして別の型へ定義するのが適切です。

C#
public static class MathHelper
{
public static int Add(int x, int y)
{
return x + y;
}
}

4-5. トップレベルステートメントを使う際の制限と注意点

トップレベルステートメントには、いくつかの注意点があります。

1つのプロジェクトでトップレベルステートメントを記述できるソースファイルは1つだけです。複数のファイルにトップレベルの実行コードを書くことはできません。

また、トップレベルステートメントと明示的なMainメソッドが同じプロジェクトにある場合、トップレベルステートメント側がエントリーポイントとして扱われ、明示的なMainはエントリーポイントとして使用されません。Microsoft Learn+1

トップレベルステートメントは次の用途に向いています。

  • 学習用の短いコード

  • 小規模なコンソールアプリ

  • 動作確認用のプログラム

  • 簡単なバッチ処理

一方、大規模なアプリケーションで共通関数を管理する仕組みとして使うものではありません。再利用する処理は、役割に応じたクラスへ分離するほうが保守しやすくなります。

5. C#でグローバル関数を代替する4つの方法

5-1. staticメソッドを使う

最も基本的な代替方法は、通常のクラスへstaticメソッドを定義することです。

C#
public class TemperatureConverter
{
public static double CelsiusToFahrenheit(double celsius)
{
return celsius * 9 / 5 + 32;
}
}
C#
double fahrenheit =
TemperatureConverter.CelsiusToFahrenheit(25);

同じクラス内にインスタンスメソッドも必要になる可能性がある場合は、通常のクラスにstaticメソッドを定義できます。

5-2. static classのユーティリティクラスを作る

状態を持たない共通処理だけをまとめるなら、static classが適しています。

C#
public static class FileNameHelper
{
public static string RemoveExtension(string fileName)
{
return Path.GetFileNameWithoutExtension(fileName);
}

public static bool HasCsvExtension(string fileName)
{
return string.Equals(
Path.GetExtension(fileName),
".csv",
StringComparison.OrdinalIgnoreCase);
}
}

static classはインスタンス化できず、含められる独自メンバーもstaticに限られます。そのため、共通処理を提供する型であることがコード上で明確になります。Microsoft Learn+1

ただし、無関係な処理をすべて1つのUtilityクラスに集めるのは避けましょう。

C#
// 責務が広すぎる例
public static class Utility
{
public static decimal CalculateTax(decimal price) => price * 0.1m;

public static string NormalizeEmail(string email) =>
email.Trim().ToLowerInvariant();

public static void SaveLog(string message)
{
// ファイル保存
}
}

次のように、目的別に分割したほうが分かりやすくなります。

C#
public static class TaxCalculator
{
}

public static class EmailNormalizer
{
}

public class FileLogger
{
}

5-3. ローカル関数を使って処理範囲を限定する

特定のメソッド内だけで使用する処理は、ローカル関数として定義できます。

C#
public static int CalculateTotal(int price, int quantity)
{
bool IsValid(int value)
{
return value >= 0;
}

if (!IsValid(price) || !IsValid(quantity))
{
throw new ArgumentOutOfRangeException();
}

return price * quantity;
}

IsValidCalculateTotalの内部だけで使用できます。

ローカル関数には、次のメリットがあります。

  • 利用範囲を限定できる

  • クラスの公開メソッドを増やさずに済む

  • 長いメソッドの処理を分割できる

  • 外側のメソッドにある変数を参照できる

ローカル関数は包含するメンバーからのみ呼び出せるため、再利用する必要がない補助処理に適しています。Microsoft Learn

5-4. 拡張メソッドを使って既存の型に処理を追加する

拡張メソッドを使うと、既存の型へメソッドを追加したような形式で呼び出せます。

C#
namespace MyApp.Extensions;

public static class StringExtensions
{
public static bool IsBlank(this string? value)
{
return string.IsNullOrWhiteSpace(value);
}
}

呼び出し側で名前空間を取り込みます。

C#
using MyApp.Extensions;

string? name = " ";

bool result = name.IsBlank();

実際には、string型そのものを書き換えているわけではありません。staticメソッドをインスタンスメソッドのような構文で呼び出せる仕組みです。

従来形式の拡張メソッドは、入れ子ではないstatic class内のstaticメソッドとして定義し、最初の引数にthisを付けます。C# 14では、新しいextensionブロックによる拡張メンバー構文も追加されていますが、既存のthis形式も引き続き利用できます。Microsoft Learn+3Microsoft Learn+3Microsoft Learn+3

5-5. 目的別に適切な代替方法を選ぶ基準

代替方法は、処理の利用範囲と依存関係に応じて選びます。

目的適した方法
単純な計算や値の変換staticメソッド
関連する共通処理をまとめるstatic class
クラス名を省略して簡潔に呼ぶusing static
1つのメソッド内でのみ使うローカル関数
既存型の操作として自然に見せる拡張メソッド
オブジェクトの状態を変更するインスタンスメソッド
DBやAPIなどを利用するDI可能なサービスクラス

「どこからでも呼べるようにしたい」という理由だけでstaticにするのではなく、その処理がどの責務に属するのかを考えることが重要です。

6. グローバル関数の代替方法を使った実践コード例

6-1. 計算処理をstaticメソッドとして共通化する例

商品の税込価格を計算する処理をstaticメソッドにします。

C#
public static class PriceCalculator
{
public static decimal AddTax(
decimal price,
decimal taxRate)
{
if (price < 0)
{
throw new ArgumentOutOfRangeException(
nameof(price),
"価格は0以上で指定してください。");
}

if (taxRate < 0)
{
throw new ArgumentOutOfRangeException(
nameof(taxRate),
"税率は0以上で指定してください。");
}

return price * (1 + taxRate);
}
}

呼び出し側は次のようになります。

C#
decimal total =
PriceCalculator.AddTax(1000m, 0.1m);

Console.WriteLine(total);

この処理には次の特徴があります。

  • オブジェクトの状態を必要としない

  • 引数だけで結果が決まる

  • 外部サービスを利用しない

  • 呼び出すたびに同じ入力から同じ結果が得られる

このような計算処理はstaticメソッドに向いています。

6-2. 文字列処理をユーティリティクラスにまとめる例

文字列の整形処理を目的別のstatic classへまとめます。

C#
public static class TextNormalizer
{
public static string NormalizeName(string value)
{
ArgumentNullException.ThrowIfNull(value);

return value.Trim();
}

public static string NormalizeEmail(string value)
{
ArgumentNullException.ThrowIfNull(value);

return value
.Trim()
.ToLowerInvariant();
}

public static string RemoveSpaces(string value)
{
ArgumentNullException.ThrowIfNull(value);

return value.Replace(" ", "");
}
}

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

C#
string name =
TextNormalizer.NormalizeName(" 山田 太郎 ");

string email =
TextNormalizer.NormalizeEmail(
" TARO@EXAMPLE.COM ");

Console.WriteLine(name);
Console.WriteLine(email);

クラス名をUtilityではなくTextNormalizerとすることで、何を担当するクラスなのかが明確になります。

6-3. using staticで関数のように呼び出す例

数学処理を提供するstatic classを作ります。

C#
namespace SampleApp.Mathematics;

public static class NumberFunctions
{
public static int Square(int value)
{
return value * value;
}

public static bool IsEven(int value)
{
return value % 2 == 0;
}
}

呼び出し側でusing staticを記述します。

C#
using static SampleApp.Mathematics.NumberFunctions;

int square = Square(5);
bool isEven = IsEven(10);

Console.WriteLine(square);
Console.WriteLine(isEven);

関数のように短く呼び出せますが、メソッド数が多い場合は、どの型に定義された処理なのか分かりにくくなる可能性があります。

業務上重要な処理では、あえてクラス名を残したほうが読みやすい場合があります。

C#
int square = NumberFunctions.Square(5);

6-4. ローカル関数でメソッド内の処理を分割する例

注文金額を計算する処理を、ローカル関数で分割します。

C#
public static decimal CalculateOrderTotal(
decimal unitPrice,
int quantity,
decimal discountRate)
{
ValidateArguments();

decimal subtotal = unitPrice * quantity;
decimal discount = CalculateDiscount(subtotal);

return subtotal - discount;

void ValidateArguments()
{
if (unitPrice < 0)
{
throw new ArgumentOutOfRangeException(
nameof(unitPrice));
}

if (quantity < 0)
{
throw new ArgumentOutOfRangeException(
nameof(quantity));
}

if (discountRate is < 0 or > 1)
{
throw new ArgumentOutOfRangeException(
nameof(discountRate));
}
}

decimal CalculateDiscount(decimal subtotal)
{
return subtotal * discountRate;
}
}

ValidateArgumentsCalculateDiscountは、外部から利用する必要がありません。そのため、クラスのpublicメソッドやprivateメソッドとして増やさず、ローカル関数にする選択ができます。

6-5. 拡張メソッドで文字列やコレクションを操作する例

文字列の拡張メソッドを作ります。

C#
namespace SampleApp.Extensions;

public static class StringExtensions
{
public static string Truncate(
this string value,
int maxLength)
{
ArgumentNullException.ThrowIfNull(value);

if (maxLength < 0)
{
throw new ArgumentOutOfRangeException(
nameof(maxLength));
}

return value.Length <= maxLength
? value
: value[..maxLength];
}
}

呼び出し例は次のとおりです。

C#
using SampleApp.Extensions;

string text = "C#のグローバル関数を解説します";
string shortened = text.Truncate(10);

Console.WriteLine(shortened);

コレクション向けの拡張メソッドも定義できます。

C#
namespace SampleApp.Extensions;

public static class EnumerableExtensions
{
public static bool HasAny<T>(
this IEnumerable<T>? source)
{
return source is not null && source.Any();
}
}
C#
using SampleApp.Extensions;

var numbers = new List<int> { 1, 2, 3 };

if (numbers.HasAny())
{
Console.WriteLine("要素があります");
}

拡張メソッドを利用するには、そのメソッドが定義された名前空間をusingで取り込む必要があります。Microsoft Learn+1

ただし、一般的な操作まで独自の拡張メソッドにすると、標準APIとの区別がつきにくくなります。対象型にとって自然な操作であり、複数箇所で再利用される場合に限定するとよいでしょう。

7. グローバル関数の代替設計でよくある失敗と注意点

7-1. 何でもstaticにしてしまう

staticメソッドはインスタンス生成が不要なので、初心者には便利に見えます。しかし、状態を持つ処理までstaticにすると、設計が不自然になります。

C#
public static class UserManager
{
public static string CurrentName = "";

public static void ChangeName(string name)
{
CurrentName = name;
}
}

この設計では、アプリケーション全体で1つのCurrentNameを共有します。複数ユーザーを同時に扱うことが難しく、どこから値が変更されたかも追跡しにくくなります。

ユーザーごとに状態を持たせるなら、インスタンスを使うべきです。

C#
public class User
{
public string Name { get; private set; }

public User(string name)
{
Name = name;
}

public void ChangeName(string name)
{
Name = name;
}
}

7-2. Utilityクラスに無関係な処理を詰め込む

次のようなUtilityクラスは、規模が大きくなるほど管理が難しくなります。

C#
public static class Utility
{
public static string FormatDate(DateTime date)
{
return date.ToString("yyyy/MM/dd");
}

public static decimal CalculateTax(decimal price)
{
return price * 0.1m;
}

public static bool ValidateEmail(string email)
{
return email.Contains('@');
}

public static void WriteFile(
string path,
string content)
{
File.WriteAllText(path, content);
}
}

日付、税金、メール、ファイル処理という無関係な責務が混在しています。

次のように分割すると、変更理由が明確になります。

C#
public static class DateFormatter
{
}

public static class TaxCalculator
{
}

public static class EmailValidator
{
}

public class FileWriter
{
}

クラスを分割するときは、「同じ理由で変更される処理か」を基準にすると判断しやすくなります。

7-3. グローバルな状態をstaticフィールドで共有する

変更可能なstaticフィールドは、実質的なグローバル変数として動作します。

C#
public static class GlobalState
{
public static int Counter;
public static string? CurrentUserName;
}

どこからでも変更できるため、次の問題が起こりやすくなります。

  • 値を変更した場所が分かりにくい

  • テスト間で状態が残る

  • 並行処理で競合する

  • 初期化順序に依存する

  • メソッドの結果が共有状態によって変わる

定数であれば、staticメンバーでも問題になりにくい場合があります。

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

一方、実行中に変化する値は、必要なオブジェクトへ明示的に保持させるほうが安全です。

7-4. メソッド名やクラス名の衝突を招く

using staticを多用すると、同名メソッドが衝突する可能性があります。

C#
using static FirstLibrary.MathFunctions;
using static SecondLibrary.MathFunctions;

// どちらのCalculateか判断できない可能性がある
// int result = Calculate(10);

衝突を避けるには、型名を明示します。

C#
int first =
FirstLibrary.MathFunctions.Calculate(10);

int second =
SecondLibrary.MathFunctions.Calculate(10);

また、クラス名やメソッド名は、処理内容が分かる具体的な名前にします。

C#
// 抽象的
Helper.Execute();

// 具体的
InvoiceCalculator.CalculateTotal();

7-5. テストしにくいstaticメソッドを作る

staticメソッド自体がテストしにくいとは限りません。次のような純粋な計算処理は簡単にテストできます。

C#
public static decimal CalculateDiscount(
decimal price,
decimal rate)
{
return price * rate;
}

問題になるのは、差し替えたい依存関係を内部に隠したstaticメソッドです。

C#
public static bool IsExpired(DateTime expiresAt)
{
return expiresAt < DateTime.Now;
}

現在時刻を外から渡せば、テストしやすくなります。

C#
public static bool IsExpired(
DateTime expiresAt,
DateTime currentTime)
{
return expiresAt < currentTime;
}
C#
bool result = IsExpired(
new DateTime(2026, 1, 1),
new DateTime(2026, 2, 1));

単純な値を引数として渡せるなら、staticメソッドのままでも十分にテストできます。

7-6. 依存性注入を使うべき処理までstaticにする

データベース、外部API、ファイル、メール送信などに依存する処理は、実装を差し替えられる設計が適しています。

例えば、メール送信処理をstaticにするとします。

C#
public static class MailSender
{
public static void Send(
string address,
string message)
{
// 外部メールサービスへ接続
}
}

この実装を利用するクラスは、本物のメール送信処理に強く依存します。

インターフェースを使えば、テスト用実装へ差し替えられます。

C#
public interface IMailSender
{
Task SendAsync(
string address,
string message);
}
C#
public class OrderService
{
private readonly IMailSender _mailSender;

public OrderService(IMailSender mailSender)
{
_mailSender = mailSender;
}

public async Task CompleteOrderAsync(
string address)
{
// 注文処理

await _mailSender.SendAsync(
address,
"注文が完了しました。");
}
}

外部環境に依存する処理や、実装を交換する可能性がある処理は、staticメソッドよりサービスオブジェクトと依存性注入が向いています。

8. staticメソッドとインスタンスメソッドの使い分け

8-1. 状態を持たない処理はstaticメソッドが向いている

次の条件を満たす処理は、staticメソッドの候補です。

  • 引数だけで計算できる

  • インスタンスのフィールドやプロパティを使わない

  • 外部サービスに依存しない

  • 共有状態を変更しない

  • 処理の意味が特定の型に自然に属する

例えば、単位変換はstaticメソッドに向いています。

C#
public static class LengthConverter
{
public static double KilometersToMeters(
double kilometers)
{
return kilometers * 1000;
}
}

文字列の検証も、状態を必要としなければstaticメソッドにできます。

C#
public static class PasswordValidator
{
public static bool IsValid(string password)
{
return password.Length >= 8;
}
}

8-2. オブジェクトの状態を扱う処理はインスタンスメソッドを使う

オブジェクトが保持する値を参照または変更する処理は、インスタンスメソッドにします。

C#
public class ShoppingCart
{
private readonly List<decimal> _prices = [];

public void AddItem(decimal price)
{
_prices.Add(price);
}

public decimal CalculateTotal()
{
return _prices.Sum();
}
}

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

C#
var cart1 = new ShoppingCart();
cart1.AddItem(1000m);

var cart2 = new ShoppingCart();
cart2.AddItem(2000m);

Console.WriteLine(cart1.CalculateTotal());
Console.WriteLine(cart2.CalculateTotal());

それぞれのカートが別の状態を持ちます。このような処理をstaticで表現すると、複数のカートを自然に扱えません。

8-3. 外部サービスやデータベースに依存する処理の設計

外部サービスに依存する処理は、インターフェースを介したサービスクラスとして設計すると管理しやすくなります。

C#
public interface IWeatherApi
{
Task<double> GetTemperatureAsync(
string city);
}
C#
public class WeatherService
{
private readonly IWeatherApi _weatherApi;

public WeatherService(IWeatherApi weatherApi)
{
_weatherApi = weatherApi;
}

public async Task<string> CreateReportAsync(
string city)
{
double temperature =
await _weatherApi.GetTemperatureAsync(city);

return $"{city}の気温は{temperature}度です。";
}
}

この設計では、テスト時にIWeatherApiの偽物を渡せます。

外部依存をstaticメソッド内へ直接書くより、次の点で有利です。

  • 依存関係がコンストラクターから分かる

  • テスト用実装へ差し替えられる

  • 接続先の変更に対応しやすい

  • クラスごとの責務を分けやすい

8-4. 保守性とテスト容易性を高める判断基準

staticメソッドにするか迷ったら、次の質問で判断します。

特定のオブジェクトの状態を使うか

使う場合は、インスタンスメソッドが自然です。

入力値だけで結果が決まるか

決まる場合は、staticメソッドが候補です。

外部APIやデータベースを使うか

使う場合は、DI可能なサービスクラスを検討します。

実装をテスト時に差し替えたいか

差し替えたい場合は、インターフェースとインスタンスを利用します。

処理を使う範囲はどこか

1つのメソッド内だけなら、ローカル関数が適しています。

既存型の操作として表現すると自然か

自然であれば、拡張メソッドを検討できます。

staticを選ぶ基準は「簡単に呼び出せるから」ではなく、「インスタンス状態を必要とせず、その型に属する処理だから」と考えるとよいでしょう。

9. C#のグローバル関数に関するよくある質問

9-1. クラスの外側にメソッドを直接書ける?

通常の型宣言として、クラスや構造体の外側に独立したメソッドを定義することはできません。

ただし、トップレベルステートメントを使うファイルでは、次のようなコードを書けます。

C#
void PrintMessage(string message)
{
Console.WriteLine(message);
}

PrintMessage("Hello");

このPrintMessageはグローバル関数ではなく、トップレベルの実行コンテキスト内にあるローカル関数です。別のクラスから共通利用したい場合は、staticメソッドとして型の中へ定義します。Microsoft Learn+1

9-2. Mainメソッドの外に関数を定義できる?

Programクラスの中であれば、Mainメソッドの外側に別のメソッドを定義できます。

C#
public class Program
{
public static void Main(string[] args)
{
int result = Add(1, 2);
Console.WriteLine(result);
}

private static int Add(int x, int y)
{
return x + y;
}
}

ここでAddMainの外側にありますが、Programクラスの内側です。クラスから独立したグローバル関数ではありません。

9-3. using staticを使えば本当のグローバル関数になる?

本当のグローバル関数にはなりません。

C#
using static MyApp.MathHelper;

int result = Add(1, 2);

Addをクラス名なしで呼び出せますが、実体はMyApp.MathHelper型に所属するstaticメソッドです。

using staticが影響するのは名前の解決方法であり、メソッドの所属先やアクセス範囲をグローバルへ変更するわけではありません。Microsoft Learn+1

9-4. static classと通常のclassは何が違う?

static classはインスタンス化できず、独自のインスタンスメンバーも持てません。

C#
public static class MathHelper
{
public static int Add(int x, int y)
{
return x + y;
}
}
C#
// インスタンス化できない
// var helper = new MathHelper();

通常のclassはインスタンス化でき、インスタンスごとの状態を持てます。

C#
public class Counter
{
public int Value { get; private set; }

public void Increment()
{
Value++;
}
}
C#
var counter1 = new Counter();
var counter2 = new Counter();

counter1.Increment();

Console.WriteLine(counter1.Value);
Console.WriteLine(counter2.Value);

通常のclassにもstaticメソッドを定義できます。

C#
public class Counter
{
public static bool IsValid(int value)
{
return value >= 0;
}

public int Value { get; private set; }
}

クラス内の処理をすべてstaticに限定したい場合はstatic class、状態を持つ可能性がある場合は通常のclassを使用します。Microsoft Learn+1

9-5. PythonやJavaScriptの関数と同じ書き方はできる?

トップレベルステートメントを使えば、見た目が似たコードは書けます。

C#
int Add(int x, int y)
{
return x + y;
}

Console.WriteLine(Add(1, 2));

ただし、C#ではこのAddがアプリケーション全体のグローバル関数になるわけではありません。トップレベルの実行コンテキスト内にあるローカル関数として扱われます。

複数のクラスやファイルから利用する場合は、staticメソッドなどへ分離します。

9-6. Unityでもグローバル関数の代わりにstaticを使える?

UnityでもC#を使うため、状態を持たない共通処理はstaticメソッドとして定義できます。

C#
public static class DamageCalculator
{
public static int Calculate(
int attack,
int defense)
{
return Math.Max(attack - defense, 0);
}
}
C#
int damage =
DamageCalculator.Calculate(100, 40);

ただし、GameObjectやコンポーネントごとの状態を扱う処理までstaticにするのは適切ではありません。

C#
public class PlayerHealth : MonoBehaviour
{
public int CurrentHp { get; private set; } = 100;

public void TakeDamage(int damage)
{
CurrentHp = Math.Max(CurrentHp - damage, 0);
}
}

ダメージの計算はstatic、各プレイヤーのHP変更はインスタンスメソッドというように分けると、責務が明確になります。

9-7. グローバル変数もC#には存在しない?

C#では、名前空間の直下に独立した変数を宣言することはできません。変数を広い範囲で共有したい場合、staticフィールドやstaticプロパティを使うことはできます。

C#
public static class AppSettings
{
public static string ApplicationName { get; set; }
= "SampleApp";
}
C#
Console.WriteLine(AppSettings.ApplicationName);

ただし、これは型に所属するstaticメンバーであり、厳密には型から独立したグローバル変数ではありません。

定数や変更されない設定値であれば比較的扱いやすいものの、変更可能なstaticフィールドを多用すると、実質的なグローバル状態になります。

C#
public static class GlobalState
{
public static int CurrentUserId;
}

変更可能な共有状態は、不具合やテストの不安定化につながりやすいため、必要なオブジェクトへ値を渡す設計を優先しましょう。

まとめ

C#には、C言語のように型へ所属しないグローバル関数はありません。C#のメソッドは、原則としてクラス、構造体、レコードなどの型に定義します。

グローバル関数の代わりとして最も一般的なのはstaticメソッドです。

C#
public static class MathHelper
{
public static int Add(int x, int y)
{
return x + y;
}
}
C#
int result = MathHelper.Add(1, 2);

using staticを使えば、クラス名を省略して関数のように呼び出せます。

C#
using static MathHelper;

int result = Add(1, 2);

ただし、実体はあくまで型に所属するstaticメソッドです。

また、トップレベルステートメント内に定義した関数は、プログラム全体から呼び出せるグローバル関数ではなく、トップレベルの実行コンテキスト内で使うローカル関数です。

代替方法を選ぶ際は、次の基準で考えます。

  • 状態を持たない計算や変換はstaticメソッド

  • 関連する共通処理の集約はstatic class

  • 特定メソッド内の補助処理はローカル関数

  • 既存型の操作として表現したい処理は拡張メソッド

  • オブジェクトの状態を扱う処理はインスタンスメソッド

  • APIやデータベースに依存する処理はDI可能なサービス

「どこからでも簡単に呼び出せるか」だけで決めるのではなく、処理の責務、状態、依存関係、テストのしやすさを基準に設計することが、保守しやすいC#コードにつながります。