您已经发现显示了该警告,因为它是第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");;
请注意,对于某些内置类型,例如float
或int
,此警告不会引发,因为编译器知道这些类型及其实现,并且知道它们会t变异(所有BCL方法都是安全的)。通常,不会为它们制作防御性副本。
还要注意,它不是特定于DateTimeOffset
的,例如DateTime
和Guid
的行为完全相同,几乎所有其他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