前几天我在 F#(+ .NET Core 3.1)中遇到了一些关于 let
的意外情况。绑定(bind)初始化(变量)并不总是发生,具体取决于程序编译器的配置:调试或发布。
好的,问题出在这些方面(我故意简化了代码,并且仍然可以重现行为),我创建了一个项目,它是一个带有单个文件的控制台,如下所示:Program.fs
:
open System
open ClassLibrary1
open Flurl.Http
[<RequireQualifiedAccess>]
module Console =
let private init =
printfn "Console: A"
FlurlHttp.Configure(fun settings ->
printfn "Console: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Console: C"
[<EntryPoint>]
let main _ =
Console.doStuff()
Library.doStuff()
0
ClassLibrary1
namespace 实际上是一个引用到控制台项目的库项目。该库项目也由单个文件组成:
Library.fs
:namespace ClassLibrary1
open System
open Flurl.Http
[<RequireQualifiedAccess>]
module Library =
let private init =
printfn "Library: A"
FlurlHttp.Configure(fun settings ->
printfn "Library: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Library: C"
运行控制台项目时的区别
释放输出:
Console: A
Console: B
Console: C
Library: C
调试输出:
Console: A
Console: B
Console: C
Library: A
Library: B
Library: C
这有点令人不安,我和我的同事花了很多时间试图弄清楚发生了什么。
所以我想在这个上下文中确认编译器优化规则。
我的理解 atm 是:
我想知道我的理解是否正确。
[编辑]
Bent Tranberg 建议我的帖子与以下内容重复:Module values in F# don't get initialized. Why?
所以我检查了那个帖子中给出的答案:
Brian pointed me to this part of the spec, which indicates that this is the expected behavior.
It looks like one workaround would be to provide an explicit entry point, like this:
[<EntryPoint>] let main _ = 0
所以我确实在库项目中添加了一个入口点
Library.fs
module ClassLibrary1
open System
open Flurl.Http
[<RequireQualifiedAccess>]
module Library =
let private init =
printfn "Library: A"
FlurlHttp.Configure(fun settings ->
printfn "Library: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Library: C"
[<EntryPoint>]
let callMe _ =
Library.doStuff ()
0
并将可执行程序更改如下:
open System
open ClassLibrary1
open Flurl.Http
[<RequireQualifiedAccess>]
module Console =
let private init =
printfn "Console: A"
FlurlHttp.Configure(fun settings ->
printfn "Console: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Console: C"
[<EntryPoint>]
let main _ =
Console.doStuff()
callMe [||] |> ignore
0
同样的事情发生了,就像以前一样。
我什至将库项目类型更改为可执行项目,但也没有任何变化......
最佳答案
这个需要一些挖掘。这是两个不同问题的结果:
启动码
这是因为fsc
选择为模块生成 IL。一个模块的所有初始化代码 is bundled into a separate class在 StartupCode$
命名空间。
所以模块的静态构造函数实际上是 exists in a another class命名 <StartupCode$Assembly>.$ClassLibrary1
.也许你可以开始看到这个问题——如果这个类从未被引用,静态构造函数将永远不会运行。
积极优化
在 Release
模式,F# 将积极地内联短方法、文字,并将忽略其值被丢弃的属性访问。
module Library =
let private init =
printfn "In init"
0
let doStuff () =
init |> ignore //<-- will be thrown away
printfn "%s" "doStuff"
更清楚地说,这就是
init
好像: static class ClassLibrary1 {
static Unit init { get { return <StartupCode$Assembly>.$ClassLibrary1.init; } }
}
因此,如果没有引用启动类上的该字段的属性访问,则模块中没有使用启动代码类的任何部分,因此静态构造函数将不会运行。
module Library =
let private init =
printfn "In init"
0
let doStuff () =
init |> printfn "%d" // init is accessed
printfn "%s" "doStuff"
上面的代码有效是因为
init
不能扔掉。最后,为了证明任何字段或属性访问都可以,我们编写了一个确保属性访问的示例 - 可变变量将阻止任何优化。
module Library =
let mutable str = "Anything will do"
let private init =
printfn "In init"
let doStuff () =
printfn "%s" str
您可以看到 init 代码仍然会运行。
关于.net-core - 积极的 F# 编译器优化是否只发生在引用的依赖项 + 发布配置上?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61937274/