什麼是 TimeProvider?
System.TimeProvider 是時間的一種抽象化,它提供了一個作為 DateTimeOffset 類型的時間點。 藉由使用 TimeProvider
,您可確保程式代碼可測試且可預測。
TimeProvider
是在 .NET 8 中引進的,也適用於 .NET Framework 4.7+ 和 .NET Standard 2.0 作為 NuGet 套件。
TimeProvider 類別會定義下列功能:
- 透過 TimeProvider.GetUtcNow() 和 TimeProvider.GetLocalNow()提供日期和時間的存取權。
- 具有 TimeProvider.GetTimestamp()的高頻率時間戳。
- 使用 TimeProvider.GetElapsedTime測量兩個時間戳之間的時間。
- 具有 TimeProvider.CreateTimer(TimerCallback, Object, TimeSpan, TimeSpan)的高解析度定時器。
- 使用 TimeProvider.LocalTimeZone取得目前的時區。
預設實現
.NET 透過 TimeProvider 屬性提供 TimeProvider.System 實作,具有下列特性:
- 日期和時間是使用 DateTimeOffset.UtcNow 和 TimeZoneInfo.Local來計算。
- 時間戳是由 System.Diagnostics.Stopwatch提供。
- 定時器是透過內部類別實作,並公開為 System.Threading.ITimer。
下列範例示範如何使用 TimeProvider 來取得目前的日期和時間:
Console.WriteLine($"Local: {TimeProvider.System.GetLocalNow()}");
Console.WriteLine($"Utc: {TimeProvider.System.GetUtcNow()}");
/* This example produces output similar to the following:
*
* Local: 12/5/2024 10:41:14 AM -08:00
* Utc: 12/5/2024 6:41:14 PM +00:00
*/
Console.WriteLine($"Local: {TimeProvider.System.GetLocalNow()}")
Console.WriteLine($"Utc: {TimeProvider.System.GetUtcNow()}")
' This example produces output similar to the following
'
' Local: 12/5/2024 10:41:14 AM -08:00
' Utc: 12/5/2024 6:41:14 PM +00:00
下列範例示範使用 TimeProvider.GetTimestamp()擷取經過的時間:
long stampStart = TimeProvider.System.GetTimestamp();
Console.WriteLine($"Starting timestamp: {stampStart}");
long stampEnd = TimeProvider.System.GetTimestamp();
Console.WriteLine($"Ending timestamp: {stampEnd}");
Console.WriteLine($"Elapsed time: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd)}");
Console.WriteLine($"Nanoseconds: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd).TotalNanoseconds}");
/* This example produces output similar to the following:
*
* Starting timestamp: 55185546133
* Ending timestamp: 55185549929
* Elapsed time: 00:00:00.0003796
* Nanoseconds: 379600
*/
Dim stampStart As Long = TimeProvider.System.GetTimestamp()
Console.WriteLine($"Starting timestamp: {stampStart}")
Dim stampEnd As Long = TimeProvider.System.GetTimestamp()
Console.WriteLine($"Ending timestamp: {stampEnd}")
Console.WriteLine($"Elapsed time: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd)}")
Console.WriteLine($"Nanoseconds: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd).TotalNanoseconds}")
' This example produces output similar to the following:
'
' Starting timestamp: 55185546133
' Ending timestamp: 55185549929
' Elapsed time: 00:00:00.0003796
' Nanoseconds: 379600
FakeTimeProvider 實作
Microsoft.Extensions.TimeProvider.Testing NuGet 套件 提供專為單元測試設計的可控制 TimeProvider
實作。
下列清單描述 FakeTimeProvider 類別的一些功能:
- 設定特定的日期和時間。
- 每當讀取日期和時間時,自動將日期和時間前移指定的數量。
- 手動前移日期和時間。
自定義實作
雖然 FakeTimeProvider 應該涵蓋大部分需要時間可預測性的案例,但您仍然可以提供自己的實作。 建立一個從 TimeProvider 衍生的新類別,並覆寫其成員,以控制時間的提供方式。 例如,下列類別只提供單一日期,月亮登陸的日期:
public class MoonLandingTimeProviderPST: TimeProvider
{
// July 20, 1969, at 20:17:40 UTC
private readonly DateTimeOffset _specificDateTime = new(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset);
public override DateTimeOffset GetUtcNow() => _specificDateTime;
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.FindSystemTimeZoneById("PST");
}
Public Class MoonLandingTimeProviderPST
Inherits TimeProvider
'July 20, 1969, at 20:17:40 UTC
Private ReadOnly _specificDateTime As New DateTimeOffset(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset)
Public Overrides Function GetUtcNow() As DateTimeOffset
Return _specificDateTime
End Function
Public Overrides ReadOnly Property LocalTimeZone As TimeZoneInfo
Get
Return TimeZoneInfo.FindSystemTimeZoneById("PST")
End Get
End Property
End Class
如果使用這個類別的程式代碼會呼叫 MoonLandingTimeProviderPST.GetUtcNow
,則會傳回 UTC 登月日期。 如果呼叫 MoonLandingTimeProviderPST.GetLocalNow
,基類會將 MoonLandingTimeProviderPST.LocalTimeZone
套用至 GetUtcNow
,並傳回 PST 時區中的月球登陸日期和時間。
若要示範控制時間的實用性,請考慮下列範例。 假設您正在撰寫行事曆應用程式,以在每天第一次開啟應用程式時傳送問候語給使用者。 應用程式在特定的日子提供特別的問候,例如登月周年紀念日。
public static class CalendarHelper
{
static readonly DateTimeOffset MoonLandingDateTime = new(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset);
public static void SendGreeting(TimeProvider currentTime, string name)
{
DateTimeOffset localTime = currentTime.GetLocalNow();
Console.WriteLine($"Good morning, {name}!");
Console.WriteLine($"The date is {localTime.Date:d} and the day is {localTime.Date.DayOfWeek}.");
if (localTime.Date.Month == MoonLandingDateTime.Date.Month
&& localTime.Date.Day == MoonLandingDateTime.Date.Day)
{
Console.WriteLine("Did you know that on this day in 1969 humans landed on the Moon?");
}
Console.WriteLine($"I hope you enjoy your day!");
}
}
Public Module CalendarHelper
ReadOnly MoonLandingDateTime As DateTimeOffset = #7/20/1969 20:17:40#
Public Sub SendGreeting(currentTime As TimeProvider, name As String)
Dim localTime As DateTimeOffset = currentTime.GetLocalNow()
Console.WriteLine($"Good morning, {name}!")
Console.WriteLine($"The date is {localTime.Date:d} and the day is {localTime.Date.DayOfWeek}.")
If (localTime.Date.Month = MoonLandingDateTime.Date.Month _
And localTime.Date.Day = MoonLandingDateTime.Date.Day) Then
Console.WriteLine("Did you know that on this day in 1969 humans landed on the Moon?")
End If
Console.WriteLine($"I hope you enjoy your day!")
End Sub
End Module
您可能傾向於使用 DateTime 或 DateTimeOffset 撰寫先前的程式代碼,以取得目前的日期和時間,而不是 TimeProvider。 但是,使用單元測試,很難直接解決 DateTime 或 DateTimeOffset。 您必須在登月日和月執行測試,或將程式代碼進一步抽象化為更小但可測試的單位。
應用程式的一般作業會使用 TimeProvider.System
來擷取目前的日期和時間:
CalendarHelper.SendGreeting(TimeProvider.System, "Eric Solomon");
/* This example produces output similar to the following:
*
* Good morning, Eric Solomon!
* The date is 12/5/2024 and the day is Thursday.
* I hope you enjoy your day!
*/
CalendarHelper.SendGreeting(TimeProvider.System, "Eric Solomon")
' This example produces output similar to the following:
'
' Good morning, Eric Solomon!
' The date is 12/5/2024 and the day is Thursday.
' I hope you enjoy your day!
而且單元測試可以撰寫來測試特定案例,例如測試月球登陸周年紀念日:
CalendarHelper.SendGreeting(new MoonLandingTimeProviderPST(), "Eric Solomon");
/* This example produces output similar to the following:
*
* Good morning, Eric Solomon!
* The date is 7/20/1969 and the day is Sunday.
* Did you know that on this day in 1969 humans landed on the Moon?
* I hope you enjoy your day!
*/
CalendarHelper.SendGreeting(New MoonLandingTimeProviderPST(), "Eric Solomon")
' This example produces output similar to the following:
'
' Good morning, Eric Solomon!
' The date is 7/20/1969 and the day is Sunday.
' Did you know that on this day in 1969 humans landed on the Moon?
' I hope you enjoy your day!
與 .NET 搭配使用
從 .NET 8 開始,執行階段庫會提供 TimeProvider 類別。 以 .NET Standard 2.0 為目標的舊版 .NET 或使用 .NET Standard 2.0 作為目標的函式庫,必須參考 Microsoft.Bcl.TimeProvider NuGet 套件。
下列與異步程序設計相關的方法可與 TimeProvider
搭配使用:
- CancellationTokenSource(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider, CancellationToken)
- Task.WaitAsync(TimeSpan, TimeProvider)
- Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken)
搭配 .NET Framework 使用
TimeProvider 是由 Microsoft.Bcl.TimeProvider NuGet 套件實作。
透過下列擴充方法新增了在異步程序設計案例中使用 TimeProvider
的支援:
- TimeProviderTaskExtensions.CreateCancellationTokenSource(TimeProvider, TimeSpan)
- TimeProviderTaskExtensions.Delay(TimeProvider, TimeSpan, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync(Task, TimeSpan, TimeProvider, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync<TResult>(Task<TResult>, TimeSpan, TimeProvider, CancellationToken)