static キーワードは「インスタンスに属さない、クラス自体に属するメンバーを定義する」ための仕組みです。Math.Sqrt() や Console.WriteLine() のような「インスタンスを作らずに呼べるメソッド」はすべて static として定義されています。
本記事では基本的な使い方にとどまらず、using static・拡張メソッド・スレッドセーフな初期化・テスタビリティの問題と解決策・const との使い分けまで体系的に解説します。
static メンバーの基本
通常のインスタンスメンバーはオブジェクトごとに独立していますが、static を付けるとすべてのインスタンスで共有されるクラスレベルのメンバーになります。
class AppStats
{
// ─── static フィールド:クラス全体で共有 ──────────
private static int _instanceCount = 0;
private static readonly object _lock = new();
public string Name { get; }
public AppStats(string name)
{
Name = name;
lock (_lock) _instanceCount++; // スレッドセーフなカウント
}
// ─── static プロパティ ────────────────────────────
public static int InstanceCount => _instanceCount;
// ─── static メソッド:インスタンスなしで呼べる ──
public static void Reset()
{
lock (_lock) _instanceCount = 0;
}
}
// 使い方
var a = new AppStats("A");
var b = new AppStats("B");
Console.WriteLine(AppStats.InstanceCount); // 2(クラス名でアクセス)
AppStats.Reset();
Console.WriteLine(AppStats.InstanceCount); // 0
| 種類 | 特性 | 主な用途 |
|---|---|---|
| static フィールド | クラスに1つ存在 | すべてのインスタンスで共有される値(カウンタ・キャッシュなど) |
| static プロパティ | クラスに1つ存在 | アプリ全体で共有する設定値・シングルトンインスタンス |
| static メソッド | インスタンス不要で呼べる | 純粋な計算・変換・ファクトリーメソッド |
| static クラス | インスタンス化不可 | ユーティリティ・拡張メソッドのコンテナ |
| static コンストラクタ | 型の初回使用前に1回だけ実行 | 静的フィールドの複雑な初期化・ファイル読み込み |
| static ローカル関数(C# 8+) | メソッド内のローカル関数 | キャプチャを防止し意図を明確化 |
static クラス:ユーティリティの設計
クラス全体に static を付けると、インスタンス化が禁止され、すべてのメンバーを static にすることが強制されます。
// ─── ユーティリティクラス ─────────────────────────
public static class StringUtils
{
// ユーティリティメソッドは副作用なし・純粋な変換が理想
public static string ToSnakeCase(string input)
{
if (string.IsNullOrEmpty(input)) return input;
return string.Concat(
input.Select((c, i) =>
i > 0 && char.IsUpper(c) ? "_" + char.ToLower(c) : char.ToLower(c).ToString()
));
}
public static string Truncate(string text, int maxLength, string ellipsis = "...")
{
if (text.Length <= maxLength) return text;
return text[..(maxLength - ellipsis.Length)] + ellipsis;
}
public static bool IsPalindrome(string s)
{
s = s.ToLower().Where(char.IsLetterOrDigit).Aggregate("", (acc, c) => acc + c);
return s == new string(s.Reverse().ToArray());
}
}
// 使い方
Console.WriteLine(StringUtils.ToSnakeCase("HelloWorld")); // hello_world
Console.WriteLine(StringUtils.Truncate("長いタイトルの記事", 8)); // 長いタイトル...
Console.WriteLine(StringUtils.IsPalindrome("racecar")); // True
using static:static メンバーの呼び出しを簡潔にする
C# 6 で導入された using static ディレクティブを使うと、クラス名を省略して static メンバーを呼び出せます。
using static System.Math; // Math クラスの static メンバーをインポート
using static System.Console; // Console クラスの static メンバーをインポート
using static System.Linq.Enumerable; // Range など
// ─── Math のメンバーをクラス名なしで使える ─────────
double area = PI * Pow(5.0, 2); // Math.PI * Math.Pow(...)
double root = Sqrt(16); // Math.Sqrt(16)
double maxVal = Max(10, 20); // Math.Max(10, 20)
// ─── Console も同様 ──────────────────────────────────
WriteLine("Hello, World!"); // Console.WriteLine(...)
Write("値: "); // Console.Write(...)
// ─── Enumerable.Range ─────────────────────────────
var nums = Range(1, 10).Where(n => n % 2 == 0).ToList();
// Enumerable.Range(1, 10) と同じ
// ─── 自作の static クラスにも使える ──────────────
using static MyApp.StringUtils;
string snake = ToSnakeCase("MyVariable"); // StringUtils. が不要に
using static はコードを簡潔にしますが、乱用するとどのクラスのメソッドかわかりにくくなります。Math・Console のような広く知られた型か、ファイル内で集中的に使う特定のユーティリティクラスに絞るのが実践的な指針です。拡張メソッド:既存の型を拡張する
static クラスの中に特定の構文で書いたメソッドは、既存の型にメソッドを追加したように呼び出せる拡張メソッドになります。this キーワードが付いた第1引数が拡張対象の型です。
// ─── 拡張メソッドは static クラスの中に定義 ────────
public static class StringExtensions
{
// string を拡張(第1引数に this を付ける)
public static bool IsNullOrWhiteSpaceEx(this string? s)
=> string.IsNullOrWhiteSpace(s);
public static string ToSnakeCase(this string s)
=> string.Concat(s.Select((c, i) =>
i > 0 && char.IsUpper(c) ? "_" + char.ToLower(c) : char.ToLower(c).ToString()));
public static string Repeat(this string s, int count)
=> string.Concat(Enumerable.Repeat(s, count));
public static string Left(this string s, int length)
=> s.Length <= length ? s : s[..length];
}
public static class IntExtensions
{
// int を拡張
public static bool IsEven(this int n) => n % 2 == 0;
public static bool IsInRange(this int n, int min, int max) => n >= min && n <= max;
public static IEnumerable<int> Times(this int n) => Enumerable.Range(0, n);
}
public static class IEnumerableExtensions
{
// IEnumerable<T?> から null を除いた IEnumerable<T> を返す
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : class
=> source.Where(x => x is not null).Select(x => x!);
// OfType<T>() でも同じ効果が得られる(型が違う要素もフィルタされる)
}
// ─── 使い方(インスタンスメソッドのように呼べる)──
string name = "HelloWorld";
Console.WriteLine(name.ToSnakeCase()); // hello_world
Console.WriteLine(name.Left(5)); // Hello
Console.WriteLine("ab".Repeat(3)); // ababab
int value = 42;
Console.WriteLine(value.IsEven()); // True
Console.WriteLine(value.IsInRange(1, 100)); // True
// 3.Times() で 3 回処理
foreach (var i in 3.Times())
Console.Write($"{i} "); // 0 1 2
// null フィルタ
string?[] items = { "a", null, "b", null, "c" };
var nonNull = items.WhereNotNull(); // "a", "b", "c"
Where・Select など)も IEnumerable<T> の拡張メソッドとして実装されています。既存クラスを継承できない場面・サードパーティライブラリへの機能追加・流暢な API 設計に特に有効です。static コンストラクタ:型の初期化タイミング
static コンストラクタ(型初期化子)は、型が初めて使われる直前に CLR によって1回だけ自動的に呼ばれます。手動で呼び出すことはできません。
class DatabaseConfig
{
public static readonly string ConnectionString;
public static readonly int MaxPoolSize;
// ─── static コンストラクタ:型の初回使用前に1回だけ実行 ──
static DatabaseConfig()
{
// 複雑な初期化ロジック(条件分岐・変換・ファイル読み込みなど)
string env = Environment.GetEnvironmentVariable("APP_ENV") ?? "development";
ConnectionString = env == "production"
? Environment.GetEnvironmentVariable("DB_URL") ?? throw new InvalidOperationException("DB_URL が未設定")
: "Server=localhost;Database=DevDb;";
MaxPoolSize = env == "production" ? 100 : 10;
Console.WriteLine($"DatabaseConfig 初期化: {env}");
}
}
// ─── 実行タイミングの保証 ────────────────────────────
// Console.WriteLine("開始");
// string cs = DatabaseConfig.ConnectionString; // ← ここで static コンストラクタが実行される
// Console.WriteLine($"接続文字列: {cs}");
// ─── TypeInitializationException ─────────────────
// static コンストラクタ内で例外が発生すると、
// その型は二度と使えなくなる(TypeInitializationException でラップされる)
class Broken
{
static Broken() => throw new Exception("初期化失敗!");
public static string Value = "never reached";
}
try
{
var _ = Broken.Value; // TypeInitializationException 発生
}
catch (TypeInitializationException ex)
{
Console.WriteLine($"型初期化エラー: {ex.InnerException?.Message}");
}
// 以降も Broken.Value へのアクセスはすべて TypeInitializationException になる
static ローカル関数(C# 8+)
C# 8 以降、ローカル関数に static を付けると、外側のスコープの変数を意図せずキャプチャすることを防げます。パフォーマンスとコードの意図の明確化に役立ちます。
public List<int> ProcessNumbers(List<int> numbers)
{
var result = new List<int>();
int threshold = 10; // 外側の変数
foreach (var n in numbers)
{
// 通常のローカル関数: threshold をキャプチャできる
bool isValid = NormalCheck(n);
// static ローカル関数: 外側の変数にアクセス不可(キャプチャしない)
int doubled = Double(n); // ← 外側の変数を使わないため static が使える
if (isValid) result.Add(doubled);
}
return result;
// 通常のローカル関数(外側の threshold にアクセスできる)
bool NormalCheck(int n) => n > threshold;
// static ローカル関数(外側の変数を参照すると「コンパイルエラー」になる)
// threshold をここで参照するとエラーになるため、設計ミスを早期発見できる
static int Double(int n) => n * 2;
// static int WrongDouble(int n) => n * threshold; // コンパイルエラー
}
static ローカル関数のメリット: ① 外側の変数のキャプチャが発生しないため、クロージャのアロケーションが避けられる ② 意図せず外側の変数を参照するバグをコンパイル時に防げる ③ 「この関数は外側の状態に依存しない純粋な計算」という意図を明示できるスレッドセーフな static フィールドの扱い
static フィールドはすべてのスレッドから共有されます。マルチスレッド環境で安全に使うためのパターンを押さえましょう。
// ─── Interlocked: 不可分操作(カウンタのインクリメント)
class RequestCounter
{
private static long _count = 0;
// BAD: _count++ はスレッドセーフでない(読み・加算・書き戻しが非原子的)
public static void BadIncrement() => _count++;
// GOOD: Interlocked で原子的に操作
public static void Increment() => Interlocked.Increment(ref _count);
public static void Add(int value) => Interlocked.Add(ref _count, value);
public static long Count => Interlocked.Read(ref _count);
}
// ─── volatile: 最新値を常に読む(キャッシュを使わない)
class WorkerFlag
{
private static volatile bool _running = false;
public static void Start() => _running = true;
public static void Stop() => _running = false;
// volatile なしだと、JIT がキャッシュを使い最新値が見えない場合がある
public static void Run()
{
while (_running) // volatile により常に最新値を読む
DoWork();
}
private static void DoWork() { /* ... */ }
}
// ─── Lazy<T>: スレッドセーフな遅延初期化 ─────────────
class ExpensiveResource
{
// LazyThreadSafetyMode.ExecutionAndPublication がデフォルト(スレッドセーフ)
private static readonly Lazy<ExpensiveResource> _instance
= new(() => new ExpensiveResource());
private ExpensiveResource()
{
Console.WriteLine("初期化(最初のアクセス時に1回だけ)");
// 重い初期化処理...
}
public static ExpensiveResource Instance => _instance.Value;
public bool IsInitialized => _instance.IsValueCreated;
}
// ─── ThreadLocal<T>: スレッドごとに独立した値 ────────
class ThreadLocalExample
{
// 各スレッドが独自の値を持つ
private static readonly ThreadLocal<int> _threadId
= new(() => Thread.CurrentThread.ManagedThreadId);
private static readonly ThreadLocal<List<string>> _log
= new(() => new List<string>());
public static void AddLog(string message)
{
_log.Value!.Add($"[Thread {_threadId.Value}] {message}");
}
public static IReadOnlyList<string> GetLog() => _log.Value!.AsReadOnly();
}
| 手法 | 保護範囲 | 特徴 | 適した用途 |
|---|---|---|---|
lock + Monitor |
任意のコードブロック | 汎用的。デッドロックに注意 | 排他制御 |
Interlocked |
インクリメント・加算・比較交換 | 高速・デッドロックなし | 数値カウンタ・フラグ |
volatile |
フィールドの読み書き順序保証 | 軽量だが保護は限定的 | シグナルフラグ(bool) |
Lazy<T> |
遅延初期化(生成は1回だけ) | シンプル・スレッドセーフ | 高コストな初期化 |
ThreadLocal<T> |
スレッドごとの独立した値 | スレッドローカルストレージ | スレッド別ログ・コンテキスト |
const・static readonly・static フィールドの使い分け
public class Constants
{
// ─── const: コンパイル時定数 ──────────────────────
public const double Pi = 3.14159265358979; // コンパイル時に値が埋め込まれる
public const int MaxRetry = 3; // プリミティブ型・string のみ使用可能
// public const DateTime Epoch = ...; // コンパイルエラー(DateTime は使えない)
// ─── static readonly: 実行時定数 ─────────────────
public static readonly DateTime AppStartTime = DateTime.UtcNow; // 実行時に初期化
public static readonly IReadOnlyList<string> Weekdays
= new[] { "月", "火", "水", "木", "金" };
// static コンストラクタで初期化することもできる
public static readonly string ConfigPath;
static Constants()
{
ConfigPath = Path.Combine(AppContext.BaseDirectory, "config.json");
}
// ─── static フィールド(可変)────────────────────
public static int CallCount = 0; // 変更可能(スレッドセーフではない)
}
| 種類 | 初期化タイミング | 可変性 | 使える型 | 使いどき |
|---|---|---|---|---|
const |
コンパイル時 | 変更不可 | プリミティブ・string のみ | 変更のない絶対的な定数(π・税率・最大値) |
static readonly |
実行時(型初期化時) | 変更不可 | あらゆる型 | 起動時に決まる定数(パス・設定値・不変コレクション) |
static(可変) |
実行時 | 変更可能 | あらゆる型 | カウンタ・キャッシュ(スレッドセーフに注意) |
const は参照しているアセンブリにコンパイル時に値が埋め込まれます。ライブラリとして公開する場合、const の値を変更してもライブラリを再コンパイルするだけではダメで、利用側も再コンパイルが必要になります。公開 API では static readonly を使うのが安全です。static の問題点:テスタビリティとグローバル状態
static メンバーは便利ですが、乱用すると「隠れたグローバル状態」を生み出し、テストが困難になります。
// ─── BAD: static に依存したコード(テスト困難)─────
public class OrderService
{
public decimal CalculatePrice(int productId, int quantity)
{
// static メソッドを直接呼ぶと、テスト時に差し替えられない
var product = Database.GetProduct(productId); // static DB アクセス
var rate = TaxCalculator.GetCurrentRate(); // static 税率取得(日付依存)
var discount = DateTime.Now.DayOfWeek == DayOfWeek.Friday ? 0.9m : 1.0m;
return product.Price * quantity * rate * discount;
}
}
// ─── GOOD: インターフェースを使って DI で差し替え可能にする ──
public interface IProductRepository { Product GetById(int id); }
public interface ITaxRateProvider { decimal GetCurrentRate(); }
public interface IClock { DateTime Now { get; } }
public class OrderServiceV2
{
private readonly IProductRepository _repo;
private readonly ITaxRateProvider _tax;
private readonly IClock _clock;
public OrderServiceV2(IProductRepository repo, ITaxRateProvider tax, IClock clock)
{
_repo = repo;
_tax = tax;
_clock = clock;
}
public decimal CalculatePrice(int productId, int quantity)
{
var product = _repo.GetById(productId);
var rate = _tax.GetCurrentRate();
var discount = _clock.Now.DayOfWeek == DayOfWeek.Friday ? 0.9m : 1.0m;
return product.Price * quantity * rate * discount;
}
}
// テストでは IClock をモックして特定の日付を設定できる
// new OrderServiceV2(mockRepo, mockTax, new FakeClock(DayOfWeek.Friday));
static abstract / static virtual(C# 11+)
C# 11 で導入されたインターフェースの static abstract メンバーにより、型レベルの契約をインターフェースで定義できるようになりました。ジェネリックな数値処理や演算子の抽象化に活用されています。
// ─── static abstract: インターフェースで型レベルの契約を定義 ─
public interface IAddable<T> where T : IAddable<T>
{
static abstract T operator +(T left, T right); // 演算子を抽象化
static abstract T Zero { get; } // ゼロ値を抽象化
}
// 実装クラス
public readonly struct Temperature : IAddable<Temperature>
{
public double Celsius { get; }
public Temperature(double celsius) => Celsius = celsius;
public static Temperature operator +(Temperature a, Temperature b)
=> new(a.Celsius + b.Celsius);
public static Temperature Zero => new(0);
public override string ToString() => $"{Celsius}°C";
}
// ─── ジェネリックメソッドで型に依存せず合計できる ──
static T Sum<T>(IEnumerable<T> items) where T : IAddable<T>
{
var total = T.Zero; // 型引数の静的メンバーにアクセス(C# 11+)
foreach (var item in items)
total = total + item;
return total;
}
var temps = new[] { new Temperature(20), new Temperature(5), new Temperature(-3) };
Console.WriteLine(Sum(temps)); // 22°C
// .NET 7+ では INumber<T> が標準ライブラリに追加され、
// int / double / decimal などで共通した数値演算が書けるようになった
static T Average<T>(IEnumerable<T> items) where T : System.Numerics.INumber<T>
{
T sum = T.Zero;
int count = 0;
foreach (var item in items) { sum += item; count++; }
return sum / T.CreateChecked(count);
}
Console.WriteLine(Average(new[] { 1.0, 2.0, 3.0 })); // 2.0
実践例
結果型(Result)ファクトリーと static メソッド
// static ファクトリーメソッドを使うと、複数の生成方法に名前を付けられる
public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? Error { get; }
private Result(bool success, T? value, string? error)
{
IsSuccess = success;
Value = value;
Error = error;
}
// static ファクトリー: 意図が名前から明確になる
public static Result<T> Ok(T value) => new(true, value, null);
public static Result<T> Fail(string e) => new(false, default, e);
// static ユーティリティ
public static Result<T> Try(Func<T> factory)
{
try { return Ok(factory()); }
catch (Exception ex) { return Fail(ex.Message); }
}
}
// 使い方
var success = Result<int>.Ok(42);
var failure = Result<int>.Fail("見つかりません");
var fromTry = Result<string>.Try(() => File.ReadAllText("config.txt"));
Console.WriteLine(success.IsSuccess ? $"成功: {success.Value}" : $"失敗: {success.Error}");
Console.WriteLine(fromTry.IsSuccess ? "ファイル読み込み成功" : $"エラー: {fromTry.Error}");
Fluent ビルダーパターンと static 初期化
public class QueryBuilder
{
private string _table = "";
private readonly List<string> _conditions = new();
private readonly List<string> _columns = new();
private int? _limit;
// static ファクトリー(エントリーポイント)
public static QueryBuilder From(string table) => new() { _table = table };
// メソッドチェーン(Fluent API)
public QueryBuilder Select(params string[] columns) { _columns.AddRange(columns); return this; }
public QueryBuilder Where(string condition) { _conditions.Add(condition); return this; }
public QueryBuilder Limit(int n) { _limit = n; return this; }
public string Build()
{
string cols = _columns.Count == 0 ? "*" : string.Join(", ", _columns);
string where = _conditions.Count == 0 ? "" : $" WHERE {string.Join(" AND ", _conditions)}";
string lim = _limit.HasValue ? $" LIMIT {_limit}" : "";
return $"SELECT {cols} FROM {_table}{where}{lim}";
}
}
// 使い方: static From() がエントリーポイント
string sql = QueryBuilder
.From("products")
.Select("id", "name", "price")
.Where("price > 100")
.Where("in_stock = 1")
.Limit(10)
.Build();
Console.WriteLine(sql);
// SELECT id, name, price FROM products WHERE price > 100 AND in_stock = 1 LIMIT 10
よくある質問
static readonly)にするか、Interlocked(数値操作)・lock(任意のブロック)・Lazy<T>(遅延初期化)でスレッドセーフを確保してください。static class はインスタンス化も継承もできず、すべてのメンバーが static であることを強制します。sealed class は継承を禁止しますが、インスタンス化は可能で、インスタンスメンバーを持てます。ユーティリティ・ヘルパーには static class、継承を禁止したいモデルクラス(record 含む)には sealed を使いましょう。static int X = 10;)は static コンストラクタの前に実行されます。詳細はコンストラクタ完全ガイドを参照してください。string・IEnumerable<T> のような既存型に業務ドメイン固有の操作を追加したい ③ メソッドチェーン(Fluent API)を作りたい、といった場面で特に有効です。ただし継承とは違い、virtual にできない・オーバーライドできないという制限があります。まとめ
| 機能 | ポイント |
|---|---|
| static フィールド・プロパティ | 全インスタンス共有。マルチスレッドでは Interlocked / lock / Lazy を使う |
| static メソッド | インスタンス不要。純粋関数(状態に依存しない)に適する |
| static クラス | インスタンス化不可。ユーティリティ・拡張メソッドのコンテナ |
| using static | クラス名を省略して static メンバーを呼べる(Math・Console など) |
| 拡張メソッド | 既存型にメソッド追加。LINQ もすべて拡張メソッド |
| static コンストラクタ | 型初回使用前に1回だけ実行。例外が出ると型が使用不可になる |
| static ローカル関数 | 外部変数キャプチャを禁止。意図の明確化とパフォーマンス向上 |
| const vs static readonly | const: コンパイル時埋め込み。static readonly: 実行時初期化(公開 API に安全) |
| テスタビリティ | 外部リソースに依存する static はインターフェース + DI に置き換える |
| static abstract(C# 11+) | インターフェースで型レベルの契約を定義。INumber<T> などに活用 |
static メンバーを多用している場合のリファクタリング方法は依存性注入(DI)の基本と実装例を、static コンストラクタの詳細はコンストラクタ完全ガイドを参照してください。