毫秒时间间隔的TimeSpan实现有问题吗?

33

我最近在 .NET 的 TimeSpan 实现中遇到了一些奇怪的行为。

TimeSpan test = TimeSpan.FromMilliseconds(0.5);
double ms = test.TotalMilliseconds; // Returns 0

FromMilliseconds 方法以 double 类型的参数为输入。然而,这个值在内部被四舍五入了。

如果我用 5000 个滴答(.5 毫秒)实例化一个新的 TimeSpan,则 TotalMilliseconds 的值是正确的。

通过反射查看 TimeSpan 的实现,发现输入实际上被强制转换为 long 类型。

为什么微软要设计 FromMilliseconds 方法以 double 类型的参数作为输入,而不是 long 类型(因为在此实现中使用 double 值是无效的)?


3
听起来设计不太好。@CodeNaked的回答指出它已经被记录下来,但这只是说明他们记录了这个糟糕的行为。我和你的想法一样:这是一个错误。如果你在Connect上写了它,请在这里发布链接,让人们投票支持。 - Joe White
2
https://connect.microsoft.com/VisualStudio/feedback/details/653782/timespan-frommilliseconds-incorrect-parsing#details - Kasper Holdum
5个回答

27
第一个考虑的问题是为什么他们选择一个double作为返回值。使用long会是一个显而易见的选择。尽管已经有一个完美的属性是long,Ticks带有100纳秒的单位是清晰明确的。但他们选择了double,可能是想返回一个小数值。
然而,这创造了一个新的问题,可能是后来才发现的。double只能存储15个有效数字。TimeSpan可以存储10000年。将TimeSpan转换为毫秒,再转换回TimeSpan并获得相同的值是非常理想的。
这对于double来说不可能。进行计算:10000年大约是10000 x 365.4 x 24 x 3600 x 1000 = 315,705,600,000,000毫秒。数出15位数,这是double所能做到的最好的,你得到恰好是一毫秒作为最小的可存储单位,没有舍入误差。任何额外的数字都将是随机噪声。
设计者(测试人员)陷入了困境,必须在从TimeSpan转换为毫秒时选择四舍五入值。或者在从毫秒到TimeSpan之后再做。他们选择早期处理它,这是一个勇敢的决定。
通过使用Ticks属性并乘以1E-4来获得毫秒,解决您的问题。

我仍然不明白为什么他们没有以保持精度的方式实现它。类似这样:=> this((long)milliseconds) + TimeSpan.FromTicks(1000*(milliseconds - ((long)milliseconds))) - Alain

4

显然这是有意设计的。根据文档描述:

值参数被转换为 刻度,并且使用该刻度数来初始化新的TimeSpan。 因此,只有最接近毫秒的值才会被认为是准确的。


4
该数值显然被转换为刻度值。但是,在将提供的数值转换为长整型之后才执行此操作。 - DEHAAS
@DEHAAS - 这是正确的。无论您使用什么“From”方法,它的精度都仅限于最近的毫秒。它们只是通过将其转换为长整型来截断小数值。 - CodeNaked
1
@CodeNaked 我明白这是这样的。我只是想知道为什么微软选择了这样的实现方式。特别是在选择这种设计时,为什么FromMilliseconds方法要使用double而不是long,因为在long变量中持有的任何附加信息都将永远丢失。 - DEHAAS
@CodeNaked 感谢您指出这一点,这非常令人困惑。我曾经遇到过FromMilliseconds的奇怪行为,但我不知道其他的From方法也会表现出相同的行为。 - Ilya Kogan
@DEHAAS - 是的,我觉得其他的 From 方法也接受 double 类型的参数。 - CodeNaked
显示剩余2条评论

2

接受一个double是一个逻辑设计。你可以有毫秒的分数。

内部发生的事情是实现设计。即使所有当前的CLI实现先舍入它,也不一定在未来如此。


3
使用双精度值确实有意义。然而,这似乎会对TimeSpan的实现造成这种“突破性”改变。 - DEHAAS

1
您的代码问题实际上在第一行,即您调用 FromMilliseconds。 正如先前提到的,文档中的备注说明如下:

value 参数将转换为滴答数,并使用该数字初始化新的 TimeSpan。因此,value 只考虑最接近的毫秒精度。

事实上,这种说法既不正确也不合乎逻辑。 按相反的顺序:
  • 滴答声被定义为“一百纳秒”。根据这个定义,文档应该写成:

    因此,value 只会被认为是精确到最近的滴答声,或者说十千万分之一秒

  • 由于一个错误或疏忽,在初始化新的 TimeSpan 实例之前,value 参数没有直接转换为滴答声。在 TimeSpan 的参考源代码中 可以看到, millis 值在转换为滴答声之前被四舍五入,而不是之后。如果要保留最大的精度,这行代码应该改为以下内容(并且 3 行之前的 0.5 毫秒调整将被删除):

    return new TimeSpan((long)(millis * TicksPerMillisecond));
    

概述:

各种TimeSpan.From*的文档,除了FromTicks之外,应更新为指出参数四舍五入到最近的毫秒(不包括对刻度的引用)。


0

或者,你可以这样做:

double x = 0.4;

TimeSpan t = TimeSpan.FromTicks((long)(TimeSpan.TicksPerMillisecond * x)); // where x can be a double
double ms = t.TotalMilliseconds; //return 0.4

--讽刺

TimeSpan将毫秒的双精度转换为滴答声,因此“显然”您可以拥有小于1ms的TimeSpan粒度。

-/讽刺

--这一点真的不明显... 为什么这不在.FromMilliseconds方法内完成,对我来说是个谜。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接