如何使用 DateOnly 和 TimeOnly 结构

DateOnlyTimeOnly 结构是随 .NET 6 引入的,分别表示一个特定的日期或时间。 在 .NET 6 之前,以及在 .NET Framework 中,开发人员使用 DateTime 类型(或某个其他替代项)来表示以下内容之一:

  • 一个完整的日期和时间。
  • 一个日期,不考虑时间。
  • 一个时间,不考虑日期。

DateOnlyTimeOnly 是表示 DateTime 类型的这些特定部分的类型。

重要

DateOnlyTimeOnly 类型在 .NET Framework 中不可用。

DateOnly 结构

DateOnly 结构表示一个不带时间的特定日期。 它不包含时间部分,因此表示一个从一天开始到一天结束的日期。 此结构非常适合用于存储特定日期,例如出生日期、周年日期或业务相关的日期。

虽然可以在使用 DateTime 的同时忽略时间部分,但与 DateTime 相比,使用 DateOnly 有一些好处:

  • 如果 DateTime 结构被时区偏移,它可能会滚动到上一天或第二天。 DateOnly 不能被时区偏移,并且始终表示所设置的日期。

  • 序列化 DateTime 结构包括时间部分,这可能会掩盖数据的意图。 此外,DateOnly 序列化的数据更少。

  • 当代码与数据库(如 SQL Server)交互时,整个日期通常存储为 date 数据类型,其中不包含时间。 DateOnly 与数据库类型更匹配。

DateOnly 的范围为 0001-01-01 到 9999-12-31,就像 DateTime 一样。 可以在 DateOnly 构造函数中指定特定的日历。 但是,DateOnly 对象始终表示前公历中的一个日期,不管是用哪个日历来构造它。 例如,可以从希伯来历生成日期,但日期将转换为公历:

var hebrewCalendar = new System.Globalization.HebrewCalendar();
var theDate = new DateOnly(5776, 2, 8, hebrewCalendar); // 8 Cheshvan 5776

Console.WriteLine(theDate);

/* This example produces the following output:
 *
 * 10/21/2015
*/
Dim hebrewCalendar = New System.Globalization.HebrewCalendar()
Dim theDate = New DateOnly(5776, 2, 8, hebrewCalendar) ' 8 Cheshvan 5776

Console.WriteLine(theDate)

' This example produces the following output
'
' 10/21/2015

DateOnly 示例

通过以下示例了解 DateOnly

将 DateTime 转换为 DateOnly

使用 DateOnly.FromDateTime 静态方法从 DateTime 类型创建 DateOnly 类型,如以下代码所示:

var today = DateOnly.FromDateTime(DateTime.Now);
Console.WriteLine($"Today is {today}");

/* This example produces output similar to the following:
 * 
 * Today is 12/28/2022
*/
Dim today = DateOnly.FromDateTime(DateTime.Now)
Console.WriteLine($"Today is {today}")

' This example produces output similar to the following
' 
' Today is 12/28/2022

加/减天数、月数、年数

有三种方法可以调整 DateOnly 结构:AddDaysAddMonthsAddYears。 每个方法采用一个整数参数,并按该度量值增加日期。 如果提供了负数,日期将按该度量值减少。 这些方法返回 DateOnly 的新实例,因为结构不可变。

var theDate = new DateOnly(2015, 10, 21);

var nextDay = theDate.AddDays(1);
var previousDay = theDate.AddDays(-1);
var decadeLater = theDate.AddYears(10);
var lastMonth = theDate.AddMonths(-1);

Console.WriteLine($"Date: {theDate}");
Console.WriteLine($" Next day: {nextDay}");
Console.WriteLine($" Previous day: {previousDay}");
Console.WriteLine($" Decade later: {decadeLater}");
Console.WriteLine($" Last month: {lastMonth}");

/* This example produces the following output:
 * 
 * Date: 10/21/2015
 *  Next day: 10/22/2015
 *  Previous day: 10/20/2015
 *  Decade later: 10/21/2025
 *  Last month: 9/21/2015
*/
Dim theDate = New DateOnly(2015, 10, 21)

Dim nextDay = theDate.AddDays(1)
Dim previousDay = theDate.AddDays(-1)
Dim decadeLater = theDate.AddYears(10)
Dim lastMonth = theDate.AddMonths(-1)

Console.WriteLine($"Date: {theDate}")
Console.WriteLine($" Next day: {nextDay}")
Console.WriteLine($" Previous day: {previousDay}")
Console.WriteLine($" Decade later: {decadeLater}")
Console.WriteLine($" Last month: {lastMonth}")

' This example produces the following output
' 
' Date: 10/21/2015
'  Next day: 10/22/2015
'  Previous day: 10/20/2015
'  Decade later: 10/21/2025
'  Last month: 9/21/2015

分析 DateOnly 并设置其格式

DateOnly 可以从字符串进行分析,就像 DateTime 结构一样。 所有基于日期的标准 .NET 分析令牌都适用于 DateOnly。 将 DateOnly 类型转换为字符串时,也可以使用基于日期的标准 .NET 格式设置模式。 有关设置字符串格式的详细信息,请参阅标准日期和时间格式字符串

var theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture);  // Custom format
var theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture);

Console.WriteLine(theDate.ToString("m", CultureInfo.InvariantCulture));     // Month day pattern
Console.WriteLine(theDate2.ToString("o", CultureInfo.InvariantCulture));    // ISO 8601 format
Console.WriteLine(theDate2.ToLongDateString());

/* This example produces the following output:
 * 
 * October 21
 * 2015-10-21
 * Wednesday, October 21, 2015
*/
Dim theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture) ' Custom format
Dim theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture)

Console.WriteLine(theDate.ToString("m", CultureInfo.InvariantCulture))     ' Month day pattern
Console.WriteLine(theDate2.ToString("o", CultureInfo.InvariantCulture))    ' ISO 8601 format
Console.WriteLine(theDate2.ToLongDateString())

' This example produces the following output
' 
' October 21
' 2015-10-21
' Wednesday, October 21, 2015

比较 DateOnly

可以将 DateOnly 与其他实例进行比较。 例如,可以检查某个日期是早于还是晚于另一个日期,或者今天的日期是否与特定日期一致。

var theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture);  // Custom format
var theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture);
var dateLater = theDate.AddMonths(6);
var dateBefore = theDate.AddDays(-10);

Console.WriteLine($"Consider {theDate}...");
Console.WriteLine($" Is '{nameof(theDate2)}' equal? {theDate == theDate2}");
Console.WriteLine($" Is {dateLater} after? {dateLater > theDate} ");
Console.WriteLine($" Is {dateLater} before? {dateLater < theDate} ");
Console.WriteLine($" Is {dateBefore} after? {dateBefore > theDate} ");
Console.WriteLine($" Is {dateBefore} before? {dateBefore < theDate} ");

/* This example produces the following output:
 * 
 * Consider 10/21/2015
 *  Is 'theDate2' equal? True
 *  Is 4/21/2016 after? True
 *  Is 4/21/2016 before? False
 *  Is 10/11/2015 after? False
 *  Is 10/11/2015 before? True
*/
Dim theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture) ' Custom format
Dim theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture)
Dim dateLater = theDate.AddMonths(6)
Dim dateBefore = theDate.AddDays(-10)

Console.WriteLine($"Consider {theDate}...")
Console.WriteLine($" Is '{NameOf(theDate2)}' equal? {theDate = theDate2}")
Console.WriteLine($" Is {dateLater} after? {dateLater > theDate} ")
Console.WriteLine($" Is {dateLater} before? {dateLater < theDate} ")
Console.WriteLine($" Is {dateBefore} after? {dateBefore > theDate} ")
Console.WriteLine($" Is {dateBefore} before? {dateBefore < theDate} ")

' This example produces the following output
' 
' Consider 10/21/2015
'  Is 'theDate2' equal? True
'  Is 4/21/2016 after? True
'  Is 4/21/2016 before? False
'  Is 10/11/2015 after? False
'  Is 10/11/2015 before? True

TimeOnly 结构

TimeOnly 结构表示一天中的时间值,例如每日闹钟或每天吃午餐的时间。 TimeOnly 的范围限制为 00:00:00.0000000 - 23:59:59.9999999,即一天中的特定时间。

在引入 TimeOnly 类型之前,程序员通常使用 DateTime 类型或 TimeSpan 类型来表示特定时间。 但是,使用这些结构来模拟没有日期的时间可能会导致一些问题,而 TimeOnly 可以解决这些问题:

  • TimeSpan 表示经过的时间,例如使用秒表测量的时间。 上限范围超过 29,000 年,它的值可以是负值,表示在时间上向后移动。 负的 TimeSpan 并不表示一天中的特定时间。

  • 如果将 TimeSpan 用作一天中的某个时间,则存在可能将其操作为 24 小时以外的值的风险。 TimeOnly 就没有这种风险。 例如,如果一个员工的工作班次从 18:00 开始且持续 8 小时,那么,向 TimeOnly 结构加上 8 小时将滚动到 2:00

  • 要将 DateTime 用于一天中的某个时间,需要将任意日期与该时间相关联,然后被忽略。 通常的做法是选择 DateTime.MinValue (0001-01-01) 作为日期,但如果从 DateTime 值中减去小时数,可能会发生 OutOfRange 异常。 TimeOnly 就没有这个问题,因为时间在 24 小时的时间范围内前后滚动。

  • 序列化 DateTime 结构包括日期部分,这可能会掩盖数据的意图。 此外,TimeOnly 序列化的数据更少。

TimeOnly 示例

通过以下示例了解 TimeOnly

将 DateTime 转换为 TimeOnly

使用 TimeOnly.FromDateTime 静态方法从 DateTime 类型创建 TimeOnly 类型,如以下代码所示:

var now = TimeOnly.FromDateTime(DateTime.Now);
Console.WriteLine($"It is {now} right now");

/* This example produces output similar to the following:
 * 
 * It is 2:01 PM right now
*/
Dim now = TimeOnly.FromDateTime(DateTime.Now)
Console.WriteLine($"It is {now} right now")

' This example produces output similar to the following
' 
' It is 2:01 PM right now

加/减时间

有三种方法可以调整 TimeOnly 结构:AddHoursAddMinutesAddAddHoursAddMinutes 都采用整数参数,并相应地调整值。 你可以使用负值进行减法运算,使用正值进行加法运算。 这些方法返回 TimeOnly 的新实例,因为结构不可变。 Add 方法采用 TimeSpan 参数,并从 TimeOnly 值中加上或减去值。

由于 TimeOnly 只表示 24 小时的时段,因此在向这三种方法加上提供的值时,它会相应地向前或向后滚动。 例如,如果使用 01:30:00 值表示凌晨 1:30,然后从该时间段开始加上 -4 小时,则会向后滚动到 21:30:00,即晚上 9:30。 针对 AddHoursAddMinutesAdd 的方法重载会捕获滚动的天数。

var theTime = new TimeOnly(7, 23, 11);

var hourLater = theTime.AddHours(1);
var minutesBefore = theTime.AddMinutes(-12);
var secondsAfter = theTime.Add(TimeSpan.FromSeconds(10));
var daysLater = theTime.Add(new TimeSpan(hours: 21, minutes: 200, seconds: 83), out int wrappedDays);
var daysBehind = theTime.AddHours(-222, out int wrappedDaysFromHours);

Console.WriteLine($"Time: {theTime}");
Console.WriteLine($" Hours later: {hourLater}");
Console.WriteLine($" Minutes before: {minutesBefore}");
Console.WriteLine($" Seconds after: {secondsAfter}");
Console.WriteLine($" {daysLater} is the time, which is {wrappedDays} days later");
Console.WriteLine($" {daysBehind} is the time, which is {wrappedDaysFromHours} days prior");

/* This example produces the following output:
 * 
 * Time: 7:23 AM
 *  Hours later: 8:23 AM
 *  Minutes before: 7:11 AM
 *  Seconds after: 7:23 AM
 *  7:44 AM is the time, which is 1 days later 
 *  1:23 AM is the time, which is -9 days prior
*/
Dim wrappedDays As Integer
Dim wrappedDaysFromHours As Integer

Dim theTime = New TimeOnly(7, 23, 11)

Dim hourLater = theTime.AddHours(1)
Dim minutesBefore = theTime.AddMinutes(-12)
Dim secondsAfter = theTime.Add(TimeSpan.FromSeconds(10))
Dim daysLater = theTime.Add(New TimeSpan(hours:=21, minutes:=200, seconds:=83), wrappedDays)
Dim daysBehind = theTime.AddHours(-222, wrappedDaysFromHours)

Console.WriteLine($"Time: {theTime}")
Console.WriteLine($" Hours later: {hourLater}")
Console.WriteLine($" Minutes before: {minutesBefore}")
Console.WriteLine($" Seconds after: {secondsAfter}")
Console.WriteLine($" {daysLater} is the time, which is {wrappedDays} days later")
Console.WriteLine($" {daysBehind} is the time, which is {wrappedDaysFromHours} days prior")

' This example produces the following output
' 
' Time: 7:23 AM
'  Hours later: 8:23 AM
'  Minutes before: 7:11 AM
'  Seconds after: 7:23 AM
'  7:44 AM is the time, which is 1 days later 
'  1:23 AM is the time, which is -9 days prior

分析 TimeOnly 并设置其格式

TimeOnly 可以从字符串进行分析,就像 DateTime 结构一样。 所有基于时间的标准 .NET 分析令牌都适用于 TimeOnly。 将 TimeOnly 类型转换为字符串时,也可以使用基于日期的标准 .NET 格式设置模式。 有关设置字符串格式的详细信息,请参阅标准日期和时间格式字符串

var theTime = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture);  // Custom format
var theTime2 = TimeOnly.Parse("17:30:25", CultureInfo.InvariantCulture);

Console.WriteLine(theTime.ToString("o", CultureInfo.InvariantCulture));     // Round-trip pattern.
Console.WriteLine(theTime2.ToString("t", CultureInfo.InvariantCulture));    // Long time format
Console.WriteLine(theTime2.ToLongTimeString());

/* This example produces the following output:
 * 
 * 17:00:00.0000000
 * 17:30
 * 5:30:25 PM
*/
Dim theTime = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture) ' Custom format
Dim theTime2 = TimeOnly.Parse("17:30:25", CultureInfo.InvariantCulture)

Console.WriteLine(theTime.ToString("o", CultureInfo.InvariantCulture))     ' Round-trip pattern.
Console.WriteLine(theTime2.ToString("t", CultureInfo.InvariantCulture))    ' Long time format
Console.WriteLine(theTime2.ToLongTimeString())

' This example produces the following output
' 
' 17:00:00.0000000
' 17:30
' 5:30:25 PM

将 DateOnly 和 TimeOnly 类型序列化

在 .NET 7+ 中,System.Text.Json 支持将 DateOnlyTimeOnly 类型序列化和反序列化。 请考虑以下对象:

sealed file record Appointment(
    Guid Id,
    string Description,
    DateOnly Date,
    TimeOnly StartTime,
    TimeOnly EndTime);
Public NotInheritable Class Appointment
    Public Property Id As Guid
    Public Property Description As String
    Public Property DateValue As DateOnly?
    Public Property StartTime As TimeOnly?
    Public Property EndTime As TimeOnly?
End Class

以下示例序列化 Appointment 对象,显示生成的 JSON,然后将其反序列化为 Appointment 类型的新实例。 最后,对比原始实例与反序列化后的新实例是否相同,并将结果写入控制台:

Appointment originalAppointment = new(
    Id: Guid.NewGuid(),
    Description: "Take dog to veterinarian.",
    Date: new DateOnly(2002, 1, 13),
    StartTime: new TimeOnly(5,15),
    EndTime: new TimeOnly(5, 45));
string serialized = JsonSerializer.Serialize(originalAppointment);

Console.WriteLine($"Resulting JSON: {serialized}");

Appointment deserializedAppointment =
    JsonSerializer.Deserialize<Appointment>(serialized)!;

bool valuesAreTheSame = originalAppointment == deserializedAppointment;
Console.WriteLine($"""
    Original record has the same values as the deserialized record: {valuesAreTheSame}
    """);
        Dim originalAppointment As New Appointment With {
            .Id = Guid.NewGuid(),
            .Description = "Take dog to veterinarian.",
            .DateValue = New DateOnly(2002, 1, 13),
            .StartTime = New TimeOnly(5, 3, 1),
            .EndTime = New TimeOnly(5, 3, 1)
}
        Dim serialized As String = JsonSerializer.Serialize(originalAppointment)

        Console.WriteLine($"Resulting JSON: {serialized}")

        Dim deserializedAppointment As Appointment =
            JsonSerializer.Deserialize(Of Appointment)(serialized)

        Dim valuesAreTheSame As Boolean =
            (originalAppointment.DateValue = deserializedAppointment.DateValue AndAlso
            originalAppointment.StartTime = deserializedAppointment.StartTime AndAlso
            originalAppointment.EndTime = deserializedAppointment.EndTime AndAlso
            originalAppointment.Id = deserializedAppointment.Id AndAlso
            originalAppointment.Description = deserializedAppointment.Description)

        Console.WriteLine(
            $"Original object has the same values as the deserialized object: {valuesAreTheSame}")

在上述代码中:

  • Appointment 对象实例化并分配给 appointment 变量。
  • 使用 JsonSerializer.Serializeappointment 实例序列化为 JSON。
  • 将生成的 JSON 写入控制台。
  • 使用 JsonSerializer.Deserialize 将 JSON 反序列化为 Appointment 类型的新实例。
  • 对比原始实例与反序列化后的新实例是否相同。
  • 将对比结果写入控制台。

有关详细信息,请参阅如何在 .NET 中序列化和反序列化 JSON

使用 TimeSpan 和 DateTime

TimeOnly 可以创建自以及转换为 TimeSpan。 此外,TimeOnly 可与 DateTime 一起使用,既可以创建 TimeOnly 实例,也可以创建 DateTime 实例,只要提供一个日期即可。

以下示例从 TimeSpan 创建 TimeOnly 对象,然后将其转换回:

// TimeSpan must in the range of 00:00:00.0000000 to 23:59:59.9999999
var theTime = TimeOnly.FromTimeSpan(new TimeSpan(23, 59, 59));
var theTimeSpan = theTime.ToTimeSpan();

Console.WriteLine($"Variable '{nameof(theTime)}' is {theTime}");
Console.WriteLine($"Variable '{nameof(theTimeSpan)}' is {theTimeSpan}");

/* This example produces the following output:
 * 
 * Variable 'theTime' is 11:59 PM
 * Variable 'theTimeSpan' is 23:59:59
*/
' TimeSpan must in the range of 00:00:00.0000000 to 23:59:59.9999999
Dim theTime = TimeOnly.FromTimeSpan(New TimeSpan(23, 59, 59))
Dim theTimeSpan = theTime.ToTimeSpan()

Console.WriteLine($"Variable '{NameOf(theTime)}' is {theTime}")
Console.WriteLine($"Variable '{NameOf(theTimeSpan)}' is {theTimeSpan}")

' This example produces the following output
' 
' Variable 'theTime' is 11:59 PM
' Variable 'theTimeSpan' is 23:59:59

以下示例从 TimeOnly 对象创建 DateTime,其中选择了任意日期:

var theTime = new TimeOnly(11, 25, 46);   // 11:25 AM and 46 seconds
var theDate = new DateOnly(2015, 10, 21); // October 21, 2015
var theDateTime = theDate.ToDateTime(theTime);
var reverseTime = TimeOnly.FromDateTime(theDateTime);

Console.WriteLine($"Date only is {theDate}");
Console.WriteLine($"Time only is {theTime}");
Console.WriteLine();
Console.WriteLine($"Combined to a DateTime type, the value is {theDateTime}");
Console.WriteLine($"Converted back from DateTime, the time is {reverseTime}");

/* This example produces the following output:
 * 
 * Date only is 10/21/2015
 * Time only is 11:25 AM
 * 
 * Combined to a DateTime type, the value is 10/21/2015 11:25:46 AM
 * Converted back from DateTime, the time is 11:25 AM
*/
Dim theTime = New TimeOnly(11, 25, 46) ' 11:   25 PM And 46 seconds
Dim theDate = New DateOnly(2015, 10, 21) ' October 21, 2015
Dim theDateTime = theDate.ToDateTime(theTime)
Dim reverseTime = TimeOnly.FromDateTime(theDateTime)

Console.WriteLine($"Date only is {theDate}")
Console.WriteLine($"Time only is {theTime}")
Console.WriteLine()
Console.WriteLine($"Combined to a DateTime type, the value is {theDateTime}")
Console.WriteLine($"Converted back from DateTime, the time is {reverseTime}")

' This example produces the following output
' 
' Date only is 10/21/2015
' Time only is 11:25 AM
' 
' Combined to a DateTime type, the value is 10/21/2015 11:25:46 AM
' Converted back from DateTime, the time is 11:25 AM

算术运算符和比较 TimeOnly

两个 TimeOnly 实例可以相互比较,你可以使用 IsBetween 方法来检查某个时间是否介于另外两个时间之间。 对 TimeOnly 使用加法或减法运算符时,会返回 TimeSpan,表示一个时间长度。

var start = new TimeOnly(10, 12, 01); // 10:12:01 AM
var end = new TimeOnly(14, 00, 53); // 02:00:53 PM

var outside = start.AddMinutes(-3);
var inside = start.AddMinutes(120);

Console.WriteLine($"Time starts at {start} and ends at {end}");
Console.WriteLine($" Is {outside} between the start and end? {outside.IsBetween(start, end)}");
Console.WriteLine($" Is {inside} between the start and end? {inside.IsBetween(start, end)}");
Console.WriteLine($" Is {start} less than {end}? {start < end}");
Console.WriteLine($" Is {start} greater than {end}? {start > end}");
Console.WriteLine($" Does {start} equal {end}? {start == end}");
Console.WriteLine($" The time between {start} and {end} is {end - start}");

/* This example produces the following output:
 * 
 * Time starts at 10:12 AM and ends at 2:00 PM
 *  Is 10:09 AM between the start and end? False
 *  Is 12:12 PM between the start and end? True
 *  Is 10:12 AM less than 2:00 PM? True
 *  Is 10:12 AM greater than 2:00 PM? False
 *  Does 10:12 AM equal 2:00 PM? False
 *  The time between 10:12 AM and 2:00 PM is 03:48:52
*/
Dim startDate = New TimeOnly(10, 12, 1) ' 10:12:01 AM
Dim endDate = New TimeOnly(14, 0, 53) ' 02:00:53 PM

Dim outside = startDate.AddMinutes(-3)
Dim inside = startDate.AddMinutes(120)

Console.WriteLine($"Time starts at {startDate} and ends at {endDate}")
Console.WriteLine($" Is {outside} between the start and end? {outside.IsBetween(startDate, endDate)}")
Console.WriteLine($" Is {inside} between the start and end? {inside.IsBetween(startDate, endDate)}")
Console.WriteLine($" Is {startDate} less than {endDate}? {startDate < endDate}")
Console.WriteLine($" Is {startDate} greater than {endDate}? {startDate > endDate}")
Console.WriteLine($" Does {startDate} equal {endDate}? {startDate = endDate}")
Console.WriteLine($" The time between {startDate} and {endDate} is {endDate - startDate}")

' This example produces the following output
' 
' Time starts at 10:12 AM And ends at 2:00 PM
'  Is 10:09 AM between the start And end? False
'  Is 12:12 PM between the start And end? True
'  Is 10:12 AM less than 2:00 PM? True
'  Is 10:12 AM greater than 2:00 PM? False
'  Does 10:12 AM equal 2:00 PM? False
'  The time between 10:12 AM and 2:00 PM is 03:48:52