C#を使用してアプリケーションを開発する際、処理にかかった時間や特定のイベント間の間隔、いわゆる「Duration(期間)」を扱う場面は非常に多く存在します。
単なる経過時間の表示から、マイクロ秒単位の精密なパフォーマンス計測、さらには分散システムにおけるタイムアウト制御まで、その用途は多岐にわたります。
2026年現在の.NET環境において、期間を扱うための手段は単一ではありません。
古くからあるTimeSpan構造体から、モダンな開発で推奨されるTimeProvider、さらには高精度な計測を可能にするStopwatchまで、最適なツールを選択することがコードの品質と実行速度を左右します。
本記事では、C#におけるDurationの扱いについて、基本から応用、そして最新のベストプラクティスまでを詳しく解説します。
C#における期間管理の基本:TimeSpan構造体
C#で「期間」を表現する最も基本的な型は、TimeSpan構造体です。
これは、日、時間、分、秒、およびミリ秒の単位で経過時間を保持するイミュータブル(不変)な構造体です。
TimeSpanの生成方法
TimeSpanを生成するには、コンストラクタを使用する方法と、静的メソッドを使用する方法があります。
一般的には、単位が明確で読みやすい静的メソッド(Fromメソッド群)の使用が推奨されます。
// 静的メソッドによる生成
TimeSpan durationFromMinutes = TimeSpan.FromMinutes(90); // 1時間30分
TimeSpan durationFromSeconds = TimeSpan.FromSeconds(45);
// コンストラクタによる生成(時, 分, 秒)
TimeSpan durationExplicit = new TimeSpan(1, 30, 0);
// 現在時刻の差分から生成
DateTime start = DateTime.Now;
// ... 何らかの処理 ...
DateTime end = DateTime.Now;
TimeSpan elapsed = end - start;
プロパティの使い分け:Totalとそれ以外
TimeSpanを使用する際に最も注意すべき点は、「Total」で始まるプロパティと、そうでないプロパティの違いを正しく理解することです。
| プロパティ名 | 説明 | 例(90分の場合) |
|---|---|---|
Hours | 期間のうち「時」の部分(0~23) | 1 |
TotalHours | 期間全体を「時」単位で換算した値 | 1.5 |
Minutes | 期間のうち「分」の部分(0~59) | 30 |
TotalMinutes | 期間全体を「分」単位で換算した値 | 90.0 |
この違いを誤ると、ログ出力や計算処理で意図しないバグを引き起こす原因となります。
例えば、25時間の期間に対してHoursプロパティを参照すると、値は「1」となります。
期間全体を把握したい場合は、必ずTotal...プロパティを使用するようにしましょう。
高精度な計測を実現するStopwatchクラス
コードの実行速度を計測する場合、DateTime.Nowの差分を利用する方法では精度が不十分な場合があります。
OSのシステムクロックの解像度に依存するため、ミリ秒以下の正確な計測には向きません。
そこで利用するのがSystem.Diagnostics.Stopwatchクラスです。
Stopwatchの基本的な使い方
Stopwatchは、高解像度のパフォーマンスカウンタが存在する場合はそれを利用し、ナノ秒レベルに近い精度での計測を可能にします。
using System.Diagnostics;
// 計測開始
Stopwatch sw = Stopwatch.StartNew();
// 計測対象の処理
DoHeavyProcess();
// 計測停止
sw.Stop();
Console.WriteLine($"経過時間: {sw.Elapsed.TotalMilliseconds} ms");
Console.WriteLine($"高精度チック数: {sw.ElapsedTicks}");
.NET 7以降における高精度計測の最適化
近年の.NETアップデートにより、Stopwatchにはさらに効率的な静的メソッドが追加されています。
特にStopwatch.GetTimestamp()とStopwatch.GetElapsedTime()を組み合わせる手法は、インスタンスの生成を抑えられるため、高頻度なループ内での計測に適しています。
// 開始時点のタイムスタンプを取得(long型)
long startTime = Stopwatch.GetTimestamp();
// 処理実行
DoSomething();
// 経過時間を直接TimeSpanとして取得
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
Console.WriteLine($"実行時間: {elapsed.TotalMicroseconds} μs");
実行時間: 124.5 μs
この方法は、ガベージコレクション(GC)への負荷を軽減できるため、パフォーマンスが極めて重要なパスにおいて非常に有効なテクニックです。
モダン開発の必須知識:TimeProviderによる抽象化
2026年現在のC#開発において、テストの容易性(テスタビリティ)を確保するために欠かせないのがSystem.TimeProviderです。
これまで、DateTime.NowやTask.Delayといった「時間」に依存する処理は、ユニットテストで実行時間を制御することが困難でした。
TimeProviderの導入メリット
TimeProviderを使用すると、本番環境では実際のシステム時間を参照し、テスト環境では「仮想の時間」を注入することができます。
これにより、「1時間待つ」という処理のテストを、実際には0秒で完了させるといった制御が可能になります。
public class ProcessManager
{
private readonly TimeProvider _timeProvider;
public ProcessManager(TimeProvider timeProvider = null)
{
// 指定がなければシステム標準のプロバイダーを使用
_timeProvider = timeProvider ?? TimeProvider.System;
}
public void ExecuteWithDurationLog()
{
long start = _timeProvider.GetTimestamp();
// 業務ロジック
PerformWork();
TimeSpan duration = _timeProvider.GetElapsedTime(start);
Console.WriteLine($"処理時間: {duration}");
}
}
このように、直接StopwatchやDateTimeを叩くのではなく、抽象化されたプロバイダーを経由することが、保守性の高いコードを書くためのスタンダードとなっています。
文字列からの期間解析(Parsing)とフォーマット
外部APIからのレスポンスや設定ファイルにおいて、期間が文字列で表現されていることも珍しくありません。
C#でこれらを扱う方法を整理します。
標準的な書式設定
TimeSpan.ParseやTryParseを使用することで、"01:30:00"(1時間30分)のような形式の文字列をオブジェクトに変換できます。
また、出力の際もカスタム書式指定子を利用して柔軟にフォーマット可能です。
TimeSpan interval = TimeSpan.FromHours(2.5);
// 標準的な文字列表現
Console.WriteLine(interval.ToString(@"hh\:mm\:ss"));
// 補完文字列での利用
Console.WriteLine($"経過時間: {interval.Hours}時間{interval.Minutes}分");
ISO 8601 Duration形式の扱い
Web APIの世界では、ISO 8601規格に基づく"PT1H30M"(1時間30分の期間)といった形式がよく使われます。
標準のTimeSpan.Parseはこれに対応していませんが、System.Xml.XmlConvert.ToTimeSpanを使用することで簡単に変換できます。
using System.Xml;
string isoDuration = "PT1H30M";
TimeSpan converted = XmlConvert.ToTimeSpan(isoDuration);
Console.WriteLine($"総分母: {converted.TotalMinutes}分");
総分母: 90分
非同期処理におけるDurationとタイムアウト
Duration(期間)を扱う上で避けて通れないのが、非同期処理におけるタイムアウト設定です。
CancellationTokenSourceに期間を渡すことで、一定時間後に処理を自動的にキャンセルさせることができます。
CancelAfterによる制御
using var cts = new CancellationTokenSource();
TimeSpan timeout = TimeSpan.FromSeconds(5);
// 5秒後にキャンセルされるよう設定
cts.CancelAfter(timeout);
try
{
await DoAsyncWork(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がタイムアウトしました。");
}
また、.NET 8以降では、WaitAsyncメソッドに直接TimeSpanを渡すことができるようになり、より直感的にタイムアウトを記述できるようになりました。
パフォーマンスを最大化するための注意点
Durationを扱う処理がアプリケーションのホットパス(頻繁に実行される箇所)にある場合、以下の点に留意してください。
1. 構造体のメリットを活かす
TimeSpanは構造体(値型)であるため、ヒープ領域を汚染しません。
しかし、object型へのボックス化が発生するような実装(非効率な文字列結合やインターフェース経由の呼び出しなど)は避けるべきです。
2. 精度の限界を知る
TimeSpan.Ticksは100ナノ秒単位を最小単位としています。
これよりも細かいナノ秒単位の厳密な計測が必要な場合は、ネイティブAPIの呼び出しや特定のハードウェアカウンタを直接制御する必要がありますが、通常の業務アプリケーションではStopwatchの精度で十分事足ります。
3. DateTime.Nowの多用を避ける
期間を算出するためにDateTime.Nowを2回呼び出す手法は、システム時刻の変更(NTP同期など)によって、稀に負の値が算出されるリスクがあります。
経過時間の純粋な計測には、時刻変更の影響を受けないStopwatchまたはTimeProvider.GetTimestamp()を使用するのが鉄則です。
まとめ
C#でDuration(期間)を効率的に扱うには、用途に応じたツールの使い分けが重要です。
- TimeSpan:期間の保持、基本的な加減算、API連携などのデータ表現に使用。
- Stopwatch:ミリ秒・マイクロ秒単位の精密なパフォーマンス計測に使用。
- TimeProvider:モダンな設計における時間の抽象化とテスト容易性の向上に使用。
- XmlConvert:ISO 8601などの特殊なフォーマット解析に使用。
特に、「時刻(When)」と「期間(How long)」を明確に区別して設計することが、バグの少ないクリーンなコードへの第一歩となります。
2026年現在の開発シーンでは、単に動くだけでなく、計測の正確性とテストのしやすさを両立させることが求められています。
今回解説した手法を組み合わせ、効率的な時間管理を実装に取り入れてみてください。
