什么是 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.System 属性提供 TimeProvider 实现,具有以下特征:
- 日期和时间是用 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 或库必须引用 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)