F#制作不必要的DateTimeOffset副本

在这里肯定有一个初学者的问题,为什么F#编译器制作不必要的DateTimeOffset副本,以及如何停止它?我不记得这是个问题,但是自从我在F#中使用DateTimeOffset以来,也许已经太久了:

let now = DateTimeOffset.Now
now.AddDays(30.0).ToString("yyyy-MM-dd")

在第2行上,编译器引发错误,指出“已复制该值以确保该操作不会使原始变量发生突变,或者因为从一个成员返回结构并寻址另一个成员时该副本是隐式的”。我该如何抢Now并添加几天?

iCMS 回答:F#制作不必要的DateTimeOffset副本

您已经发现显示了该警告,因为它是第5级警告的一部分。但是您可能仍然想知道该警告的实际含义。

在警告本身中已经有一个提示。当您以struct类型调用实例方法时,其中包括ToString()之类的虚拟方法,编译器无法确定基础struct保持不变。这是F#的关键点,它会尽力确保您原来的let绑定保持不变。

F#编译器中有几种优化方法,它们试图最大程度地减少防御性复制的数量。但是仍然有很多情况无法确定值不会改变。这对于任何虚拟调用都是正确的(您可能会说虚拟调用不能从结构中覆盖,但是对当前结构的覆盖可以被覆盖,并且可以访问字段,因此可以可以对其数据进行更改),更普遍的是,对于任何实例成员。

如果我采用您的代码,并将其传递给FSI(在设置warn:5之后),它将正确报告两个警告:

> let now = DateTimeOffset.Now
now.AddDays(30.0).ToString("yyyy-MM-dd");;

  now.AddDays(30.0).ToString("yyyy-MM-dd");;
  ^^^^^^^^^^^^^^^^^

stdin(3,1): warning FS0052: The value has been copied to ensure the original is not mutated by this operation or because the copy is implicit when returning a struct from a member and another member is then accessed


  now.AddDays(30.0).ToString("yyyy-MM-dd");;
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

stdin(3,1): warning FS0052: The value has been copied to ensure the original is not mutated by this operation or because the copy is implicit when returning a struct from a member and another member is then accessed

val now : DateTimeOffset = 16-7-2020 0:21:21 +02:00
val it : string = "2020-08-15"

通常,JIT可以优化这些内容,但就像F#一样,JIT也不能总是确定是否需要防御性副本。在这种情况下,复制仍将进行。我已经看到这种行为对于不同的JIT是不同的(甚至对于x86和x64之间的同一JIT甚至可以更改)。

那么您将如何防止这种复制发生?这并不总是那么容易,如果您不能更改类型的实现,当然也就不容易。有点反常理,如果您告诉F#您不在乎它是否已突变,它将停止为您复制struct

let mutable now = DateTimeOffset.Now
now <- now.AddDays(30.)
now.ToString("yyyy-MM-dd");;

请注意,对于某些内置类型,例如floatint,此警告不会引发,因为编译器知道这些类型及其实现,并且知道它们会t变异(所有BCL方法都是安全的)。通常,不会为它们制作防御性副本。

还要注意,它不是特定于DateTimeOffset的,例如DateTimeGuid的行为完全相同,几乎所有其他struct也会这样做。 t部分原始类型。

编辑:Tomas的回答也很有价值,他解释了为什么在这种情况下实际上需要AddDays的副本。但这是结果的中间副本,而不是防御性副本,在这种情况下最终还是一样(我知道这令人困惑)。即使结果不需要中间副本(例如ToString),也会发出警告。

,

您得到的警告的完整措辞是:

警告FS0052:已复制该值,以确保此操作不会更改原始值,或者因为从成员返回结构并随后访问另一个成员时该副本是隐式的

在这种情况下,我认为在消息的后半部分解释了警告的原因,即“因为从成员返回结构时,该副本是隐式的,然后可以访问另一个成员”。

如果您查看生成的IL代码,则会发现编译器确实生成了一个局部变量,将AddDays的结果分配给该局部变量,然后获取了该变量的地址,并使用此地址调用ToString(为了进行比较,C#编译器为相同代码段生成的代码完全相同):

call valuetype [mscorlib]System.DateTimeOffset 
  [mscorlib]System.DateTimeOffset::get_Now()
stloc.0       // Store the result of 'Now' in local variable #0
ldloca.s 0    // Load the address of local variable #0 to call 'AddDays'
ldc.r8 30
call instance valuetype [mscorlib]System.DateTimeOffset 
  [mscorlib]System.DateTimeOffset::AddDays(float64)
stloc.2       // Store the result of 'AddDays' in local variable #2
ldloca.s 2    // Load the address of local variable #2 to call 'ToString'
ldstr "yyyy-MM-dd"
call instance string [mscorlib]System.DateTimeOffset::ToString(string)
stloc.1

我不是IL专家,但是我认为编译器必须执行此处的操作-值类型可以是可变的,因此结果需要存储在局部变量中(以便它可以随后调用操作)使用其地址)。如果不是通过地址,则该方法将无法更改(可能是可变的)值类型。

因此,编译器会警告您它创建的本地变量在代码中看不到的事实。如果您这样写,这将很有用:

someValueType.MutateOne().MutateTwo()

如果您认为这两种变异方法会变异someValueType变量,则警告会告诉您这不是正在发生的事情! (因为第二种方法是对隐藏的隐式变量进行突变。)在这种情况下,您可以放心地忽略警告。

本文链接:https://www.f2er.com/1928190.html

大家都在问