CLR 全面透彻解析

.NET Framework 4 中的时间间隔格式设置和分析

Ron Petrusha

在 Microsoft .NET Framework 4 中,TimeSpan 结构通过增加格式设置和分析支持得到改进,这种支持可与 DateTime 值的格式设置和分析支持相媲美。在本文中,我将探讨新增的格式设置和分析功能,并提供 TimeSpan 值使用方面的一些实用提示。

.NET Framework 3.5 及更早版本中的格式设置

在 Microsoft .NET Framework 3.5 及更早版本中,用于时间间隔的唯一格式设置方法是无参数 TimeSpan.ToString 方法。返回字符串的具体格式取决于 TimeSpan 值。其中至少包含 TimeSpan 值的小时、分钟和秒组成部分。如果是非零值,还包含日组成部分。此外,如果存在小数秒组成部分,则还将包含计时周期组成部分的所有七位数字。句点(“.”)用作日与小时之间以及秒与小数秒之间的分隔符。

.NET Framework 4 中的扩展格式设置支持

在 .NET Framework 4 中,默认 TimeSpan.ToString 方法的行为没有变化,但现在新增了两个重载。第一个重载只有一个参数,该参数可以是标准或自定义格式字符串,用于定义结果字符串的格式。第二个重载有两个参数:一个标准或自定义格式字符串和一个 IFormatProvider 实现,后者表示提供格式设置信息的区域性。顺便说一下,此方法为 TimeSpan 结构提供了 IFormattable 实现,它使 TimeSpan 值可用于支持复合格式设置的 String.Format 等方法。

除了包含标准或自定义格式字符串以及提供 IFormattable 实现之外,现在设置了格式的字符串还可以区分区域性。两个标准格式字符串“g”(一般短格式说明符)和“G”(一般长格式说明符)在结果字符串中使用当前区域性或特定区域性的格式设置约定。图 1 中的示例格式对此进行了演示,该示例显示的时间间隔结果字符串使用“G”格式字符串以及 en-US 和 fr-FR 区域性设置了格式。

图 1 使用“G”格式字符串设置格式的时间间隔 (VB)

Visual Basic
Imports System.Globalization

Module Example
   Public Sub Main()
      Dim interval As New TimeSpan(1, 12, 42, 30, 566)
      Dim cultures() As CultureInfo = { New CultureInfo("en-US"), 
                                        New CultureInfo("fr-FR") }
      For Each culture As CultureInfo In cultures
         Console.WriteLine("{0}: {1}", culture, interval.ToString(
            "G", culture))
      Next                                  
   End Sub
End Module

图 1 使用“G”格式字符串设置格式的时间间隔 (C#)

using System;
using System.Globalization;

public class Example
{
   public static void Main()
   {
      TimeSpan interval = new TimeSpan(1, 12, 42, 30, 566);
      CultureInfo[] cultures = { new CultureInfo("en-US"), 
                                 new CultureInfo(“"fr-FR") };
      foreach (CultureInfo culture in cultures)
         Console.WriteLine("{0}: {1}", culture, interval.ToString( _
            "G", culture));
   }
}

图 1 中的示例显示以下输出:

  en-US:1:12:42:30.5660000
  fr-FR:1:12:42:30,5660000

.NET Framework 3.5 及更早版本中的分析

在 .NET Framework 3.5 及更早版本中,由静态 System.TimeSpan.Parse 和 System.TimeSpan.TryParse 方法处理对时间间隔的分析支持,这两个方法支持数量有限的固定格式。图 2 中的示例分析某一时间间隔的字符串表示形式,该时间间隔采用了前述方法可识别的每种格式。

图 2 分析多种格式的时间间隔字符串 (VB)

Module Example
   Public Sub Main()
      Dim values() As String = {"12", "12.16:07", "12.16:07:32", _
                                "12.16:07:32.449", "12.16:07:32.4491522", 
_
                                "16:07", "16:07:32", "16:07:32.449" }
      
      For Each value In values
         Try
            Console.WriteLine("Converted {0} to {1}", _
                              value, TimeSpan.Parse(value))
         Catch e As OverflowException
            Console.WriteLine("Overflow: {0}", value)
         Catch e As FormatException
            Console.WriteLine("Bad Format: {0}", value)
         End Try
      Next
   End Sub

图 2 分析多种格式的时间间隔字符串 (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] values = { "12", "12.16:07", "12.16:07:32", 
                          "12.16:07:32.449", "12.16:07:32.4491522", 
                          "16:07", "16:07:32", "16:07:32.449" };
      
      foreach (var value in values)
         try {
            Console.WriteLine("Converted {0} to {1}", 
                              value, TimeSpan.Parse(value));}
         catch (OverflowException) {
            Console.WriteLine("Overflow: {0}", value); }
         catch (FormatException) {
            Console.WriteLine("Bad Format: {0}", value);
         }
   }
}

图 2 中的示例显示以下输出:

  Converted 12 to 12.00:00:00
  Converted 12.16:07 to 12.16:07:00
  Converted 12.16:07:32 to 12.16:07:32
  Converted 12.16:07:32.449 to 12.16:07:32.4490000
  Converted 12.16:07:32.4491522 to 12.16:07:32.4491522
  Converted 16:07 to 16:07:00
  Converted 16:07:32 to 16:07:32
  Converted 16:07:32.449 to 16:07:32.4490000

如输出所示,该方法可以分析单个整数,将该整数解释为时间间隔中的天数(稍后将对此进行详细的说明)。若非单个整数,则要分析的字符串至少须包含小时值和分钟值。

.NET Framework 4 中的扩展分析支持

在 .NET Framework 4 和 Silverlight 4 中,对时间间隔字符串表示形式的分析支持已得到增强,现可与日期和时间字符串的分析支持相媲美。TimeSpan 结构现在为 Parse 和 TryParse 方法提供了新的重载,并提供了全新的 ParseExact 和 TryParseExact 方法,后两个方法各有四个重载。这些分析方法支持标准和自定义格式字符串,并为区分区域性的格式设置提供一定的支持。两个标准格式字符串(“g”和“G”)是区分区域性的,而其余的标准格式字符串(“c”、“t”和“T”)以及所有自定义格式字符串都是固定的。在 .NET Framework 的未来版本中,将会进一步增强对时间间隔的分析和格式设置支持。

图 3 中的示例演示了在 .NET Framework 4 中如何使用 ParseExact 方法分析时间间隔数据。它定义了一个数组,其中包含七个自定义格式字符串;如果要分析的时间间隔字符串表示形式与其中所有格式都不相符,该方法就会失败并引发异常。

图 3 使用 ParseExact 方法分析时间间隔数据 (VB)

Module modMain
   Public Sub Main()
      Dim formats() As String = { "hh", "%h", "h\:mm", "hh\:mm",
                                  "d\.hh\:mm\:ss", "fffff", "hhmm" }
      Dim values() As String = { '16", "1", "16:03", "1:12", 
                                 "1.13:34:15", "41237", "0609" }
      Dim interval As TimeSpan
      
      For Each value In values
         Try
            interval = TimeSpan.ParseExact(value, formats, Nothing)
            Console.WriteLine("Converted '{0}' to {1}", 
                              value, interval)
         Catch e As FormatException
            Console.WriteLine("Invalid format: {0}", value)
         Catch e As OverflowException
            Console.WriteLine("Overflow: {0}", value)
         Catch e As ArgumentNullException
            Console.WriteLine("No string to parse")
         End Try         
      Next
   End Sub
End Module

图 3 使用 ParseExact 方法分析时间间隔数据 (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] formats = { "hh", "%h", @"h\:mm", @"hh\:mm", 
                           @"d\.hh\:mm\:ss", "fffff", "hhmm" };
      string[] values = { "16", "1", "16:03", '1:12', 
                          "1.13:34:15", "41237", "0609" };
      TimeSpan interval;
      
      foreach (var value in values)
      {
         try {
            interval = TimeSpan.ParseExact(value, formats, null);
            Console.WriteLine("Converted '{0}' to {1}", value, 
                              interval); }
         catch (FormatException) {
            Console.WriteLine("Invalid format: {0}", value); }
         catch (OverflowException) {
            Console.WriteLine("Overflow: {0}", value); }
         catch (ArgumentNullException) {
            Console.WriteLine("No string to parse");
         }         
      }
   }
}

图 3 中的示例显示以下输出:

  Converted ‘16’ to 16:00:00
 Converted ‘1’ to 01:00:00
 Converted ‘16:03’ to 16:03:00
 Converted ‘1:12’ to 01:12:00
 Converted ‘1.13:34:15’ to 1.13:34:15
 Converted ‘41237’ to 00:00:00.4123700
 Converted ‘0609’ to 06:09:00

使用单个数值实例化 TimeSpan

有趣的是,如果在 .NET Framework 的任何版本中向 TimeSpan.Parse(String) 方法传递这七个时间间隔字符串,则这些字符串的分析将全部成功,但其中四个字符串会返回不同的结果。对这些字符串调用 TimeSpan.Parse(String) 将产生以下输出:

  Converted ‘16’ to 16.00:00:00
  Converted ‘1’ to 1.00:00:00
  Converted ‘16:03’ to 16:03:00
  Converted ‘1:12’ to 01:12:00
  Converted ‘1.13:34:15’ to 1.13:34:15
  Converted ‘41237’ to 41237.00:00:00
  Converted ‘0609’ to 609.00:00:00

TimeSpan.Parse(String) 与 TimeSpan.ParseExact(String, String[], IFormatProvider) 方法调用的主要区别在于对表示整数值的字符串的处理方式。TimeSpan.Parse(String) 方法将此类字符串解释为日。TimeSpan.ParseExact(String, String[], IFormatProvider) 方法对整数的解释取决于在字符串数组参数中提供的自定义格式字符串。在此示例中,只有一个或两个整数位的字符串将解释为小时数,具有四个整数位的字符串将解释为小时数和分钟数,而具有五个整数位的字符串将解释为秒的小数。

在许多情况下,.NET Framework 应用程序以任意格式接收包含时间间隔数据的字符串(如表示毫秒数的整数或表示小时数的整数)。在以前的 .NET Framework 版本中,须将这样的数据处理成可接受的格式,然后才能传递给 TimeSpan.Parse 方法。在 .NET Framework 4 中,可以使用自定义格式字符串定义对只包含整数的时间间隔字符串的解释,而无需预先处理字符串数据。图 4 中的示例为一到五位的整数提供不同的表示形式,从而演示这一点。

图 4 1 到 5 位整数表示形式 (VB)

Module Example
   Public Sub Main()
      Dim formats() As String = { "%h", "hh", "fff", "ffff', 'fffff' }
      Dim values() As String = { "3", "17", "192", "3451", 
                                 "79123", "01233" }

      For Each value In values
         Dim interval As TimeSpan
         If TimeSpan.TryParseExact(value, formats, Nothing, interval) Then
            Console.WriteLine("Converted '{0}' to {1}",  
                              value, interval.ToString())
         Else
            Console.WriteLine("Unable to parse {0}.", value)
         End If       
      Next
   End Sub
End Module

图 4 1 到 5 位整数表示形式 (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] formats = { "%h", "hh", "fff", "ffff", "fffff" };
      string[] values = { "3", "17", "192", "3451", "79123", "01233" };

      foreach (var value in values)
      {
         TimeSpan interval;
         if (TimeSpan.TryParseExact(value, formats, null, out interval))
            Console.WriteLine("Converted '{0}' to {1}", 
                              value, interval.ToString());
         else
            Console.WriteLine("Unable to parse {0}.", value);    
      }
   }
}

图 4 中的示例显示以下输出:

  Converted ‘3’ to 03:00:00
  Converted ‘17’ to 17:00:00
  Converted ‘192’ to 00:00:00.1920000
  Converted ‘3451’ to 00:00:00.3451000
  Converted ‘79123’ to 00:00:00.7912300
  Converted ‘01233’ to 00:00:00.0123300

处理分析时间间隔时产生的 OverflowException

.NET Framework 4 中所引入的这些新 TimeSpan 格式设置和分析功能保留了一个可能会对某些用户造成不便的行为。为了向后兼容,在以下情况下 TimeSpan parsing 方法会引发 OverflowException:

  • 如果小时组成部分的值超过 23。
  • 如果分钟组成部分的值超过 59。
  • 如果秒组成部分的值超过 59。

可以通过多种方式来处理此异常。可以使用 Int32.Parse 方法(而不是调用 TimeSpan.Parse 方法)将各字符串组成部分转换为整数值,然后将其传递给某一 TimeSpan 类构造函数。与 TimeSpan 分析方法不同,TimeSpan 构造函数不会在传递给该构造函数的小时、分钟或秒值超出范围时引发 OverflowException。

这是一种可接受的解决方法,但它也存在一个限制:它要求在调用 TimeSpan 构造函数之前分析所有字符串并将其转换为整数。如果要分析的大部分数据在分析操作期间都不会溢出,则此解决方法会导致不必要的处理。

另一种解决方法是先尝试分析数据,然后再处理在时间间隔的各组成部分超出范围时引发的 OverflowException。这也是一种可接受的解决方法,但应用程序中的不必要异常处理可能会产生昂贵的开销。

最佳解决方法是先使用 TimeSpan.TryParse 方法分析数据,然后仅当该方法返回 false 时再处理时间间隔的各组成部分。如果分析操作失败,则可以使用 String.Split 方法将时间间隔的字符串表示形式拆分为各组成部分,然后将其传递给 TimeSpan(Int32, Int32, Int32, Int32, Int32) 构造函数。图 5 中的示例提供了一个简单实现:

图 5 处理非标准时间间隔字符串 (VB)

Module Example
   Public Sub Main()
      Dim values() As String = { "37:16:45.33", "0:128:16.324", 
                                 "120:08" }
      Dim interval As TimeSpan
      For Each value In values
         Try
            interval = ParseIntervalWithoutOverflow(value)
            Console.WriteLine("'{0}' --> {1}", value, interval)
         Catch e As FormatException
            Console.WriteLine("Unable to parse {0}.", value)
         End Try   
      Next
   End Sub
   
   Private Function ParseIntervalWithoutOverflow(value As String) 
                    As TimeSpan   
      Dim interval As TimeSpan
      If Not TimeSpan.TryParse(value, interval) Then
         Try 
            ‘ Handle failure by breaking string into components.
            Dim components() As String = value.Split( {"."c, ":"c } )
            Dim offset As Integer = 0
            Dim days, hours, minutes, seconds, milliseconds As Integer
            ‘ Test whether days are present.
            If value.IndexOf(".") >= 0 AndAlso 
                     value.IndexOf(".") < value.IndexOf(":") Then 
               offset = 1
               days = Int32.Parse(components(0))
            End If
            ‘ Call TryParse to parse values so no exceptions result.
            hours = Int32.Parse(components(offset))
            minutes = Int32.Parse(components(offset + 1))
            If components.Length >= offset + 3 Then
               seconds = Int32.Parse(components(offset + 2))
            End If
            If components.Length >= offset + 4 Then
               milliseconds = Int32.Parse(components(offset + 3))                              
            End If
            ‘ Call constructor.
            interval = New TimeSpan(days, hours, minutes, 
                                    seconds, milliseconds)
         Catch e As FormatException
            Throw New FormatException(
                      String.Format("Unable to parse '{0}'"), e)
         Catch e As ArgumentOutOfRangeException
            Throw New FormatException(
                      String.Format("Unable to parse '{0}'"), e)
         Catch e As OverflowException
            Throw New FormatException(
                      String.Format("Unable to parse '{0}'"), e)
         Catch e As ArgumentNullException
            Throw New ArgumentNullException("value cannot be null.",
                                            e)
         End Try      
      End If         
      Return interval
   End Function
End Module

图 5 处理非标准时间间隔字符串 (C#)

using System;

public class Example
{
   public static void Main()
   {
      string[] values = { "37:16:45.33", "0:128:16.324", "120:08" };
      TimeSpan interval;
      foreach (var value in values)
      {
         try {
            interval = ParseIntervalWithoutOverflow(value);
            Console.WriteLine("'{0}' --> {1}", value, interval);
         }
         catch (FormatException) {  
            Console.WriteLine("Unable to parse {0}.", value);
         }
      }   
   }

   private static TimeSpan ParseIntervalWithoutOverflow(string value)
   {   
      TimeSpan interval;
      if (! TimeSpan.TryParse(value, out interval))
      {
         try {   
            // Handle failure by breaking string into components.
            string[] components = value.Split( 
                                  new Char[] {'.', ':' } );
   
            int offset = 0;
            int days = 0;
            int hours = 0;
            int minutes = 0;
            int seconds = 0;
            int milliseconds = 0;
            // Test whether days are present.
            if (value.IndexOf(".") >= 0 &&  
                     value.IndexOf(".") < value.IndexOf(":")) 
            {
               offset = 1;
               days = Int32.Parse(components[0]);
            }
            // Call TryParse to parse values so no exceptions result.
            hours = Int32.Parse(components[offset]);
            minutes = Int32.Parse(components[offset + 1]);
            if (components.Length >= offset + 3)
               seconds = Int32.Parse(components[offset + 2]);
   
            if (components.Length >= offset + 4)
               milliseconds = Int32.Parse(components[offset + 3]);                              
   
            // Call constructor.
            interval = new TimeSpan(days, hours, minutes, 
                                    seconds, milliseconds);
         }
         catch (FormatException e) {
            throw new FormatException(
                      String.Format("Unable to parse '{0}'"), e);
         }   
         catch (ArgumentOutOfRangeException e) {
            throw new FormatException(
                      String.Format("Unable to parse '{0}'"), e);
         }
         catch (OverflowException e)
         {
            throw new FormatException(
                      String.Format("Unable to parse '{0}'"), e);
         }   
         catch (ArgumentNullException e)
         {
            throw new ArgumentNullException("value cannot be null.",
                                            e);
         }      
      }         
      return interval;
   }   
}

如以下输出所示,图 5 中的示例成功处理了四个大于 23 的值,以及大于 59 的分钟值和秒值:

  ‘37:16:45.33’ --> 1.13:16:45.0330000
  ‘0:128:16.324’ --> 02:08:16.3240000
  ‘120:08’ --> 5.00:08:00

应用程序兼容性

矛盾的是,在 .NET Framework 4 中对 TimeSpan 值的增强格式设置支持破坏了在 .NET Framework 早期版本中对 TimeSpan 值做过格式设置的一些应用程序。例如,下面的代码在 .NET Framework 3.5 中可正常执行,但在 .NET Framework 4 中执行时会引发 FormatException:

string result = String.Format("{0:r}", new TimeSpan(4, 23, 17));

为了对参数列表中的每个参数进行格式设置,String.Format 方法将确定该对象是否实现了 IFormattable。如果实现,则会调用该对象的 IFormattable.ToString 实现。如果未实现,则会放弃索引项中提供的任何格式字符串并调用该对象的无参数 ToString 方法。

在 .NET Framework 3.5 及更早版本中,TimeSpan 未实现 IFormattable,也不支持格式字符串。因此,将忽略“r”格式字符串,并将调用无参数 TimeSpan.ToString 方法。而在 .NET Framework 4 中,将调用 TimeSpan.ToString(String, IFormatProvider) 并传递不受支持的格式字符串,从而导致该异常。

如果可能,应修改此代码使之调用无参数 TimeSpan.ToString 方法,或向格式设置方法传递有效的格式字符串。但是,如果这样做不可行,可向应用程序的配置文件中添加一个 <TimeSpan_LegacyFormatMode> 元素,类似如下所示:

<?xml version ="1.0"?>
<configuration>
   <runtime>
      <TimeSpan_LegacyFormatMode enabled="true"/>
   </runtime>
</configuration>

通过将其 enabled 特性设置为 true,可确保 TimeSpan 使用旧式格式设置行为。

Ron Petrusha 是 .NET Framework 基类库团队的程序员。他编写了许多编程书籍和文章,著作内容涉及 Windows 编程、Web 编程及 VB.NET 编程。

衷心感谢以下技术专家,感谢他们审阅了本文:Melitta Andersen 和 Josh Free