Что нового в F# 10

F# 10 позволяет улучшить язык F#, библиотеку FSharp.Core и инструменты. Эта версия — это выпуск уточнения, ориентированный на четкость, согласованность и производительность, с небольшими, но значимыми улучшениями, которые делают ваш ежедневный код более понятным и надежным. F# 10 поставляется с .NET 10 и Visual Studio 2026.

Скачать последний пакет SDK для .NET можно на странице загрузки .NET.

Начало работы

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 теперь можно применять различные модификаторы доступа к отдельным средствам доступа. Это позволяет указать различные уровни доступа для метода получения и задания свойства, что упрощает шаблон.

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 по умолчанию при использовании существующих вспомогательных параметров сопоставления шаблонов, необходимости семантики ссылок или при незначительной разнице производительности. Эта функция повышает паритет с другими конструкциями языка F#, которые уже поддерживают ValueOption.

Поддержка хвостовых вызовов в выражениях вычислений

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!, use! и and! с использованием того же синтаксиса, что и для обычных привязок let.

Ранее необходимо было использовать круглые скобки для аннотаций типов.

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 через связанные через let значения, функции, случаи объединений, неявные конструкторы, структуры и классы.

Ранее компилятор безмолвно позволял неправильно применять атрибуты к несовместимыми целевым объектам. Это вызвало тонкие ошибки, такие как игнорирование тестовых атрибутов, когда вы забываете () сделать функцию.

[<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 файл, чтобы удалить большие blob-объекты ресурсов метаданных F# (данные подписи и оптимизации, которые использует компилятор, но приложение не требуется во время выполнения).

При публикации с включенной функцией обрезки (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>

Параллельная компиляция может значительно сократить время компиляции для проектов с несколькими файлами и зависимостями.

Кэш обобщения типов

Компилятор теперь кэширует проверки связи типов, чтобы ускорить вывод типов и повысить производительность интегрированной среды разработки, особенно при работе с сложными иерархиями типов. Сохраняя и повторно используя результаты из предыдущих проверок подсоверения, компилятор избегает избыточных вычислений, которые ранее замедлили компиляцию и IntelliSense.

Управление кэшем:

В большинстве случаев кэш подсумпции типов повышает производительность без какой-либо конфигурации. Однако если вы наблюдаете увеличение использования памяти или процессора (из-за рабочих процессов обслуживания кэша), можно настроить поведение кэша:

  • Чтобы полностью отключить кэш, задайте <LangVersion>9</LangVersion> в файле проекта возврат к поведению F# 9.
  • Чтобы отключить асинхронное вытеснение кэша (что повышает давление потока) и использовать синхронное вытеснение, задайте FSharp_CacheEvictionImmediate=1 переменную среды.