F# 10 中的新增功能

F# 10 提供了 F# 语言、FSharp.Core 库和工具的多项改进。 此版本是一个优化版本,侧重于清晰度、一致性和性能,具有少量但有意义的改进,使日常代码更清晰、更可靠。 F# 10 与 .NET 10Visual Studio 2026 一起发布。

可以从 .NET 下载页下载最新的 .NET SDK。

开始

F# 10 在所有 .NET Core 分发版和 Visual Studio 工具中都可用。 有关详细信息,请参阅 F# 入门

作用域内警告抑制

现在可以使用新 #warnon 指令取消代码的特定部分中的警告。 这与 现有 #nowarn 指令 配对,使你可以精确控制哪些警告在何处适用。

以前,使用 #nowarn 时,它会在文件的其余部分禁用警告,这可能会隐藏其他地方的实际问题。 让我们看看一个激励示例:

// We know f is never called with None.
let f (Some a) =    // creates warning 25, which we want to suppress
    // 2000 loc, where the incomplete match warning is beneficial

如果添加 #nowarn 25 在函数定义上方,则会对文件的其余部分禁用 FS0025。

使用 F# 10,现在可以标记要取消警告的确切部分:

#nowarn 25
let f (Some x) =    // FS0025 suppressed
#warnon 25
    // FS0025 enabled again

相反,如果全局禁用警告(例如,通过编译器标志),则可以通过 #warnon 在本地启用它。 此指令将应用,直至出现匹配的 #nowarn 或文件结束。

重要的兼容性说明:

此功能包括几个更改,用于提高 #nowarn/#warnon 指令的一致性。 以下是重大变更:

  • 编译器不再允许多行和空警告指令。
  • 编译器不再允许在两者之间#nowarn使用空格。
  • 不能对警告编号使用三引号、内插或逐字字符串。

脚本行为也已更改。 以前,在脚本中的任何位置添加 #nowarn 指令时,它会应用于整个编译。 现在,它在脚本中的 .fs 行为与文件中的行为匹配,仅应用于文件末尾或相应的 #warnon文件。

此功能实现 RFC FS-1146

自动属性访问器上的访问修饰符

面向对象的编程中的常见模式是创建可公开读取但私有可变状态。 在 F# 10 之前,为了实现这一点,需要使用显式的属性语法和后盾字段(存储实际属性值的隐藏变量),从而导致重复的代码。

type Ledger() =
    [<DefaultValue>] val mutable private _Balance: decimal
    member this.Balance with public get() = this._Balance and private set v = this._Balance <- v

使用 F# 10,现在可以对单个属性访问器应用不同的访问修饰符。 这样,可以为属性的 getter 和 setter 指定不同的访问级别,使模式更简单:

type Ledger() =
    member val Balance = 0m with public get, private set

可以在属性名称(同时应用于两个访问器)或单个访问器之前放置访问修饰符,但不能同时放置访问修饰符。

请注意,此功能不会扩展到签名 (.fsi) 文件。 上面的示例的正确签名 Ledger 是:

type Ledger() =
    member Balance : decimal
    member private Balance : decimal with set

此功能实现 RFC FS-1141

ValueOption 可选参数

现在可以对ValueOption<'T>使用基于结构的表示形式。 将[<Struct>]特性应用于可选参数时,编译器使用ValueOption<'T>类型,而不再是基于option的引用类型。 这可以避免对选项包装器(需要在垃圾回收的托管堆上分配的内存)进行堆分配,这对于性能关键型代码非常有用。

以前,F# 始终将堆分配 option 的类型用于可选参数,即使参数不存在:

// Prior to F# 10: always uses reference option
type X() =
    static member M(?x : string) =
        match x with
        | Some v -> printfn "Some %s" v
        | None -> printfn "None"

在 F# 10 中,可以使用 [<Struct>] 属性来利用结构体支持的 ValueOption

type X() =
    static member M([<Struct>] ?x : string) =
        match x with
        | ValueSome v -> printfn "ValueSome %s" v
        | ValueNone -> printfn "ValueNone"

当参数不存在时,这将消除堆分配,这对于性能关键型代码非常有用。

对于分配压力很重要的小型值或频繁构造的类型,请选择此基于结构的选项。 依赖于现有的模式匹配辅助工具、需要引用语义或性能差异可忽略时,请使用基于option的默认引用。 此功能加强了对其他已经支持ValueOption的F#语言构造的支持能力。

计算表达式中的结尾调用支持

F# 10 为计算表达式添加了尾调用优化。 计算表达式生成器现在可以通过实现特殊方法来选择加入这些优化。

当编译器将计算表达式转换为正则 F# 代码(称为 desugaring 的进程)时,它会识别表达式(例如 return!yield!do! 出现在尾位置)。 如果生成器提供以下方法,编译器会将这些调用路由到优化的入口点:

  • ReturnFromFinal - 调用尾部 return! (如果不存在则回退到 ReturnFrom
  • YieldFromFinal - 调用尾部 yield! (如果缺失,则回退到 YieldFrom
  • 对于终端 do!,编译器优先选择 ReturnFromFinal,然后选择 YieldFromFinal,最后才回退到正常 Bind 路径。

这些 *Final 成员是可选的,只是为了启用优化而存在。 未提供这些成员的生成器保持其现有语义不变。

例如:

coroutine {
    yield! subRoutine() // tail position -> YieldFromFinal if available
}

但是,在非尾位置:

coroutine {
    try
        yield! subRoutine() // not tail -> normal YieldFrom
    finally ()
}

重要兼容性说明:

如果计算表达式生成器已定义具有这些名称的成员,则此更改可能会中断。 在大多数情况下,使用 F# 10 编译时,现有生成器将继续工作,而无需修改。 较旧的编译器将忽略新 *Final 方法,因此必须与早期编译器版本保持兼容的生成器不应假定编译器将调用这些方法。

此功能实现 RFC FS-1330

计算表达式中没有括号的类型化绑定

F# 10 消除了向计算表达式绑定添加类型注释时对括号的要求。 现在,可以使用与普通let绑定相同的语法在let!use!and!绑定上添加类型注释。

以前,必须对类型批注使用括号:

async {
    let! (a: int) = fetchA()
    and! (b: int) = fetchB()
    use! (d: MyDisposable) = acquireAsync()
    return a + b
}

在 F# 10 中,可以编写没有括号的类型批注:

async {
    let! a: int = fetchA()
    and! b: int = fetchB()
    use! d: MyDisposable = acquireAsync()
    return a + b
}

允许在use!绑定中使用_

现在可以在计算表达式中的use!绑定中使用丢弃模式(_)。 这将use!的行为与常规use绑定保持一致。

以前,编译器会拒绝在 use! 绑定中使用丢弃模式,从而迫使你创建临时标识符:

counterDisposable {
    use! _ignored = new Disposable()
    // logic
}

在 F# 10 中,可以直接使用丢弃模式:

counterDisposable {
    use! _ = new Disposable()
    // logic
}

这样可以阐明在绑定那些其值仅用于生命周期管理的异步资源时的目的。

拒绝类型中的伪嵌套模块

在类型定义内将 module 声明置于相同结构级别时,编译器现在会引发错误。 加强结构验证,以防止在类型中错误地放置模块。

以前,编译器接受的类型 module 定义内缩进的声明,但它实际上将这些模块创建为该类型的同级模块,而不是将其嵌套在其中:

type U =
    | A
    | B
    module M = // Silently created a sibling module, not nested
        let f () = ()

使用 F# 10 时,此模式会引发错误 FS0058,强制你通过适当的模块放置来阐明意向:

type U =
    | A
    | B

module M =
    let f () = ()

功能弃用警告:省略了 seq

编译器现在会警告你关于省略seq 生成器的裸序列表达式。 使用裸范围大括号 { 1..10 } 时,会出现弃用警告,建议你使用显式 seq { ... } 形式。

一直以来,F# 允许一种特殊情况的“序列推导简化”语法,其中可以省去 seq 关键字:

{ 1..10 } |> List.ofSeq  // implicit sequence, warning FS3873 in F# 10

在 F# 10 中,编译器会警告此模式并鼓励显式形式:

seq { 1..10 } |> List.ofSeq

这当前是一条警告,而不是一个错误,可让你有时间更新代码库。 如果要禁止显示此警告,请在本地使用 NoWarn 项目文件或 #nowarn 指令中的属性,并向其传递警告号:3873。

显式 seq 形式可提高与其他计算表达式的代码清晰度和一致性。 将来的 F# 版本可能会使此错误,因此我们建议在更新代码时采用显式语法。

此功能实现 RFC FS-1033

属性目标执行

F# 10 在所有语言构造中强制实施属性目标验证。 编译器现在通过跨允许绑定值、函数、联合事例、隐式构造函数、结构和类检查 AttributeTargets ,验证特性是否仅应用于其预期目标。

以前,编译器以无提示方式允许你错误地将属性应用于不兼容的目标。 这导致了一些细微的 bug,例如在你忘记使用 () 生成函数时,测试属性被忽略了:

[<Fact>]
let ``this is not a function`` = // Silently ignored in F# 9, not a test!
    Assert.True(false)

在 F# 10 中,编译器强制实施属性目标,并在属性被误用时引发警告:

[<Fact>]
//^^^^ - warning FS0842: This attribute cannot be applied to property, field, return value. Valid targets are: method
let ``this is not a function`` =
    Assert.True(false)

重要兼容性说明:

这是一项重大变更,可能会揭示代码库中以前未显现的问题。 早期错误可防止测试发现问题,并确保分析器和修饰器等属性按预期生效。

支持 and! 在任务表达式中

现在,您可以在任务表达式and!中同时等待多个任务。 使用 task 是使用 F# 中的异步工作流的常用方法,尤其是在需要与 C# 的互作性时。 但是,到目前为止,还没有一种简洁的方法在计算表达式中同时等待多个任务。

也许你从按顺序等待计算的代码开始:

// Awaiting sequentially
task {
    let! a = fetchA()
    let! b = fetchB()
    return combineAB a b
}

如果想要更改它以同时等待它们,通常使用 Task.WhenAll

// Use explicit Task combinator to await concurrently
task {
    let ta = fetchA()
    let tb = fetchB()
    let! results = Task.WhenAll([| ta; tb |])
    return combineAB ta.Result tb.Result
}

在 F# 10 中,你可以使用 and! 来实现更符合习惯用法的方法。

task {
    let! a = fetchA()
    and! b = fetchB()
    return combineAB a b
}

这结合了并发版本的语义和顺序版本的简单性。

此功能实施了 F# 语言建议 #1363,并作为对 FSharp.Core 库的补充进行实现。 大多数项目会自动从编译器获取最新版本 FSharp.Core ,除非它们显式指定版本。 在这种情况下,需要对其进行更新才能使用此功能。

默认情况下,更佳的裁剪

F# 10 解决了与 F# 程序集裁剪相关的长期存在的问题。 剪裁是从已发布的应用程序中删除未使用的代码以减小其大小的过程。 您不再需要手动维护 ILLink.Substitutions.xml 文件来去除大型 F# 元数据资源 blob(这些是编译器使用的签名和优化数据,而您的应用程序在运行时并不需要它们)。

在启用剪裁(PublishTrimmed=true)时进行发布,F# 生成现在会自动创建一个嵌入的替换文件,该文件专为这些仅限工具的 F# 资源设计。

以前,必须手动维护此文件以去除元数据。 这增加了维护负担,很容易忘记。

默认情况下,结果是较小的输出、要维护的重复代码更少,维护危害更少。 如果需要完全手动控制,仍可以添加自己的替换文件。 可以通过 <DisableILLinkSubstitutions>false</DisableILLinkSubstitutions> 属性关闭自动生成。

预览版中的并行编译

对于希望减少编译时间的 F# 用户来说,这是一项令人兴奋的更新:并行编译功能正在稳定。 从 .NET 10 开始,三个功能:基于图形的类型检查、并行 IL 代码生成和并行优化,在项目属性下 ParallelCompilation 组合在一起。

默认情况下,F# 10 对使用 LangVersion=Preview的项目启用此设置。 我们计划为 .NET 11 中的所有项目启用它。

请务必尝试一下,看看它是否加快了编译速度。 若要在 F# 10 中启用并行编译,请执行:

<PropertyGroup>
    <ParallelCompilation>true</ParallelCompilation>
    <Deterministic>false</Deterministic> <!-- Note: deterministic builds don't get the benefits of parallel compilation -->
</PropertyGroup>

如果要选择退出,同时仍享受其他预览功能,请设置为 ParallelCompilation false:

<PropertyGroup>
    <LangVersion>Preview</LangVersion>
    <ParallelCompilation>false</ParallelCompilation>
</PropertyGroup>

并行编译可以显著减少具有多个文件和依赖项的项目的编译时间。

类型归纳缓存

编译器现在缓存类型关系检查,以加快类型推理并提高 IDE 性能,尤其是在处理复杂类型层次结构时。 通过存储和重用以前的子建议检查的结果,编译器可以避免先前减缓编译和 IntelliSense 的冗余计算。

管理缓存:

在大多数情况下,类型子建议缓存可提高性能,无需进行任何配置。 但是,如果由于缓存维护线程导致内存占用量增加或 CPU 使用率上升,您可以调整缓存行为:

  • 若要完全禁用缓存,请在项目文件中设置 <LangVersion>9</LangVersion> 回退到 F# 9 行为。
  • 若要关闭异步缓存逐出(这会增加线程压力),并改用同步逐出,请设置 FSharp_CacheEvictionImmediate=1 环境变量。