Nouveautés de F# 11

Bienvenue à F# 11 ! Cette version vous permet de conserver les modèles de domaine distincts, de réutiliser des algorithmes génériques, de coordonner le travail asynchrone et de réduire l’utilisation de la mémoire du compilateur.

Merci à la communauté F# pour les propositions, les implémentations, les révisions et les tests derrière ces changements. Les liens des contributeurs accompagnent les fonctionnalités ci-dessous.

Note

Les exemples nécessitent un compilateur F# 11 et FSharp.Core 11 avec les fonctionnalités affichées. Dans le SDK .NET 11.0.100, les fonctionnalités de F# 11.2 nécessitent <LangVersion>preview</LangVersion> ; consultez les paramètres de langage et la prise en charge prévue dans le SDK. L’exemple runtime-async comporte des exigences supplémentaires concernant l’environnement d’exécution ; un SDK .NET 11 antérieur peut ne pas inclure toutes les modifications.

Écarts d’enregistrements : modèles distincts sans mappages répétitifs

Pour que les modèles de stockage et d’application restent distincts, les expansions d’enregistrements réutilisent leurs champs partagés sans répéter les déclarations. Ici, les modèles partagent les informations client, mais utilisent différents types d’identifiants :

open System

type CustomerId = CustomerId of Guid

type CustomerDetails = { Name: string; PreferredLanguage: string }

type CustomerRow =
    { ...CustomerDetails
      Id: string
      InternalNote: string }

type Customer =
    { ...CustomerDetails
      Id: CustomerId }

...CustomerDetails contribue aux déclarations de champs. À la frontière, ...row copie les valeurs pendant que la conversion analyse l’identificateur :

let toCustomer (row: CustomerRow) =
    match Guid.TryParse row.Id with
    | true, id -> Ok ({ ...row; Id = CustomerId id }: Customer)
    | false, _ -> Error "Invalid customer identifier."

La destination conserve les détails partagés, remplace Id et exclut InternalNote. Guid.TryParse, et non le spread, valide l’identificateur ; une entrée malformée produit Error.

Cela traite le problème de correspondance de la couche applicative. Les spreads de valeur combinent également des enregistrements nommés et anonymes ; les spreads de type créent uniquement des enregistrements nommés. Les champs définis ultérieurement prévalent, et les sources s’exécutent une seule fois, dans l’ordre. Consultez les opérateurs de décomposition d’enregistrement pour des exemples de composition et les risques liés à l’évolution partagée.

Merci à @brianrourkeboll pour l’affichage étendu des enregistrements.

Membres d’extension pour les algorithmes génériques, les opérateurs et les tuples

Pour partager un algorithme entre les types existants, F# 11.2 permet aux membres d’extension de satisfaire les contraintes de paramètre de type résolu statiquement (SRTP). Par exemple, une transformation peut gérer des listes et des options sans types wrapper.

Note

Dans le SDK .NET 11.0.100, activez <LangVersion>preview</LangVersion> lors de la résolution des extensions et ouvrez leur module. Consultez les prérequis de la bibliothèque.

Partager une transformation entre les listes et les options

Avec le map de la bibliothèque dans la portée, l’application peut nettoyer l’une ou l’autre représentation, modifier les types d’éléments et composer des transformations :

let inline cleanNames values =
    map (fun (name: string) -> name.Trim().ToUpperInvariant()) values

let batch: string list = cleanNames [ " Ada "; " grace " ]
let optional: string option = cleanNames (Some " linus ")
let missing: string option = cleanNames (None: string option)
let lengths: int list = map String.length batch
let nested: int option list = map (map String.length) [ Some "abc"; None ]

printfn "%A; %A; %A" batch optional missing
printfn "%A; %A" lengths nested
// ["ADA"; "GRACE"]; Some "LINUS"; None
// [3; 5]; [Some 3; None]

Les adaptateurs dans Structural.Adapters utilisent des opérations familières, plutôt que d’imposer de nouveaux conteneurs :

type List<'T> with
    static member Map(source: 'T list, mapping: 'T -> 'U) : 'U list =
        List.map mapping source

type Option<'T> with
    static member Map(source: 'T option, mapping: 'T -> 'U) : 'U option =
        Option.map mapping source

La fonction partagée dans Structural.Algorithms demande au type d’entrée un membre statique Map :

let inline map (mapping: 'T -> 'U) (source: ^Input) : ^Output =
    (^Input :
        (static member Map: ^Input * ('T -> 'U) -> ^Output)
        (source, mapping))

^Input et ^Output sont des types résolus statiquement. À chaque appel concret, le compilateur sélectionne le membre avec la signature requise. Les extensions n’ajoutent pas d’implémentation d’interface. L’application complète ouvre les deux modules.

Opérateurs, contexte et types de sortie

Le même mécanisme prend en charge les opérateurs de tuple et la propagation du contexte et la construction dirigée par la sortie. Les en-têtes de tuples natifs nécessitent l’activation de la fonctionnalité F# 11.2 sur leurs déclarations, ainsi que sur les sites d’appel de résolution d’extensions.

Un ajout distinct , AllowOverloadOnReturnTypepermet à une fabrique de déclarer des surcharges distinguées par leurs retours :

type Inject =
    [<AllowOverloadOnReturnType>]
    static member Pure(value: 'T) : 'T list = [value]

    [<AllowOverloadOnReturnType>]
    static member Pure(value: 'T) : 'T option = Some value

let batch: string list = Inject.Pure "ready"
let optional: string option = Inject.Pure "ready"
// ["ready"] and Some "ready"

Cet attribut Core 11 ne nécessite pas d’aperçu. L’annotation de résultat sélectionne une surcharge ; sans elle l’appel reste ambigu. Voir Sélectionner par type de retour.

Merci à @gusty pour avoir étendu la prise en charge de la programmation générique.

intégration .NET et l’expérience de développement

Les délégués directs préservent l’interface effective du handler

Pour exposer aux infrastructures les noms des gestionnaires et les attributs de liaison, F# 11 peut cibler directement une fonction nommée compatible au lieu d’une fonction wrapper. Les API minimales d’ASP.NET Core peuvent ensuite lire le contrat de route, de service et d’en-tête HTTP :

open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.DependencyInjection

type ShipmentStore() =
    member _.Status trackingId =
        if trackingId = "SHP-1042" then "Dispatched" else "Unknown"

let status
    (trackingId: string)
    ([<FromServices>] store: ShipmentStore)
    ([<FromHeader(Name = "X-Customer")>] customer: string) =
    $"{customer}: {trackingId} is {store.Status trackingId}"

let handler = Func<string, ShipmentStore, string, string>(status)

Consultez le projet exécutable et les règles de génération/de compatibilité. L’adaptation des captures et des arguments peut toujours nécessiter des wrappers.

Merci à @kerams pour les delegates directs.

Réutiliser la documentation XML

Pour gérer les remarques d’API partagées à un seul endroit, placez-les dans shared.xml:

<docs>
  <tracking>
    <remarks>An unrecognized tracking identifier has status Unknown.</remarks>
  </tracking>
</docs>

Avec GenerateDocumentationFile activé, F# 11 développe <include> en entrées de membres émises :

/// <summary>Find the latest status of a shipment.</summary>
/// <include file="shared.xml" path="/docs/tracking/*"/>
let status trackingId =
    if trackingId = "SHP-1042" then "Dispatched" else "Unknown"

/// <summary>Find the latest statuses of several shipments.</summary>
/// <include file="shared.xml" path="/docs/tracking/*"/>
let statuses trackingIds =
    trackingIds |> List.map (fun id -> id, status id)

L’implémentation d’inclusion met à jour les deux descriptions à partir d’un fragment. Séparément, le résolveur d’outillage expose les descriptions de base pour <inheritdoc/>; le compilateur laisse cette balise pour les consommateurs de documentation. Consultez la documentation XML pour des exemples et les règles de résolution.

Merci à @T-Gro d’avoir partagé la documentation XML dans FSharp.Core.

Déboguer des fonctions en ligne dans des versions non optimisées

Pour inspecter les arguments d’assistance inline, définissez un point d’arrêt sur let total dans une build non optimisée et entrez dans addFee. F# 11 préfère désormais les méthodes appelantes dans ces builds :

let inline addFee (fee: decimal) (amount: decimal) =
    let total = amount + fee
    total

let invoiceTotal = addFee 2m 40m
printfn "%M" invoiceTotal
// 42

Les compilations optimisées conservent l’expansion en ligne. Certaines structures doivent encore être développées. Consultez Déboguer les fonctions inline pour connaître ces limites et la commande --always-inline.

Coordination asynchrone et coordination des tâches

Pour connecter les workflows F# aux API basées sur les tâches, FSharp.Core 11 ajoute des passerelles sensibles à l’annulation, des lots de taille limitée et des transformations de résultats.

Limiter les E/S en attente, pas seulement la collecte des résultats

Un lot important d’opérations de lecture peut déclencher plus de travail qu’une application ne devrait en avoir en cours. Pour décider quand le travail basé sur les tâches commence, un coordinateur a besoin de factories, et non de tâches qui ont déjà commencé :

open System.IO
open System.Threading

let readFiles (paths: seq<string>) (ct: CancellationToken) =
    paths
    |> Seq.map (fun path ->
        fun token -> File.ReadAllTextAsync(path, token))
    |> Task.parallelLimit 4 ct

Au maximum, quatre opérations de lecture s’exécutent simultanément ; les résultats conservent l’ordre des entrées. Une défaillance demande l’annulation de l’opération sœur via les jetons fournis. Cela limite la simultanéité, et non le nombre de requêtes par seconde. Consultez les fonctions asynchrones et les fonctions Task pour les variantes séquentielles et retournant une valeur unitaire.

Conserver l’annulation et l’exception sous-jacente à travers la limite

Lorsqu’un flux de travail lance un traitement externe, son jeton doit être transmis avec celui-ci. Async.StartTaskImmediate transmet le jeton du workflow en cours d’exécution à la fabrique de tâches et attend la fin :

open System.IO

let tryReadFile path =
    async {
        try
            let! contents =
                Async.StartTaskImmediate(fun ct -> File.ReadAllTextAsync(path, ct))
            return Some contents
        with :? FileNotFoundException ->
            return None
    }

Un échec d’une seule tâche est traité par ce gestionnaire comme FileNotFoundException sous-jacent. Pour une Task ou une ValueTask déjà créée, utilisez Async.Await ; l’ancienne Async.AwaitTask conserve son comportement d’encapsulation des exceptions.

Pour les appelants orientés tâches, Task.startAsyncImmediate token workflow fournit le pont de retour. Il démarre sur le thread appelant jusqu’à la première suspension asynchrone.

Utiliser des helpers correspondants pour les transformations courtes

Pour transformer un résultat sans expression de calcul complète, utilisez les helpers correspondants dans Microsoft.FSharp.Control. Par exemple, lisez un fichier, obtenez sa longueur et retournez un échec ordinaire sous la forme de Error :

open System.IO
open System.Threading

let fileLength (path: string) (ct: CancellationToken) =
    File.ReadAllTextAsync(path, ct)
    |> Task.map String.length
    |> Task.catch

Un fichier contenant Report 1 donne Ok 8 ; un fichier manquant donne un Error contenant FileNotFoundException. L’annulation se propage toujours. Async et ValueTask offrent des équivalents correspondants map, bind et des helpers d’erreur ; consultez les fonctions asynchrones et de tâches. Utilisez une expression de calcul pour les branches ou plusieurs valeurs dépendantes.

Les helpers ValueTask nécessitent la génération de Core net10.0 ou et une netstandard2.1.

Merci à @bartelink pour ses contributions à l’API async/task.

Relancer après le signalement asynchrone d’un échec

La journalisation opérationnelle doit ajouter des informations sans modifier ce que l’appelant intercepte ou masquer où l’échec a commencé. Les gestionnaires d’expressions de calcul peuvent désormais utiliser reraise () même après avoir attendu le logger :

Note

La relance d’exception dans une expression de calcul est une fonctionnalité de F# 11.2. Dans le sdk .NET 11.0.100, activez <LangVersion>preview</LangVersion>.

open System
open System.IO
open System.Threading.Tasks

let readReport (logFailure: exn -> Task) path =
    task {
        try
            return! File.ReadAllTextAsync(path)
        with :? IOException as error ->
            do! logFailure error
            return reraise ()
    }

Une fois la connexion réussie, les mêmes informations d’exception et de site de levée d’origine se propagent. Une défaillance du journaliseur peut remplacer l’original. Cela répond à la demande d’observabilité asynchrone sans asynchronisme à l’exécution. Consultez Relancer une exception à partir d’une expression de calcul.

Merci à @xperiandri pour ce support.

FSharp.Core peut être compilé pour des versions modernes de .NET

Pour utiliser des API modernes sans abandonner la prise en charge des anciennes cibles, FSharp.Core ajoute net10.0 aux côtés de netstandard2.0 et de netstandard2.1. NuGet sélectionne la build compatible. L’empaquetage d’outils mis à jour permet également une suppression asynchrone basée sur les tâches dans F# Interactive, comme dans les projets compilés. Consultez les frameworks cibles FSharp.Core.

Merci à pour le @T-Gro et le packaging des outils SDK.

Runtime async pour les auteurs de builder

Les auteurs du générateur peuvent laisser le runtime suspendre et reprendre les méthodes marquées au lieu d’utiliser des machines d’état générées par le compilateur. Un builder expérimental défini par la bibliothèque expose le mécanisme en tant qu’expression de calcul :

let readManifest (path: string) =
    runtimeTask {
        let! text = System.IO.File.ReadAllTextAsync path
        return text.Length
    }

Note

Ce builder expérimental utilise une fonctionnalité de F# 11.2 dans laquelle son implémentation inline est définie ou étendue ; dans le SDK .NET 11.0.100, activez les fonctionnalités en préversion. Elle nécessite également la build et les références de Core 11 net10.0, ainsi qu’un environnement d’exécution prenant en charge runtime-async. La combinaison runtime/référence vérifiée ici est .NET 11 RC1. Le ciblage de .NET 10 à lui seul n'est pas suffisant. Consultez les expériences Runtime async.

FSharp.Core n’inclut pas cet exemple en tant que builder à usage général. Le code task ordinaire ne bascule pas automatiquement async vers celui-ci, et les appelants d’une API ordinaire déjà compilée qui retourne des tâches n’ont pas intrinsèquement besoin de la préversion.

Merci à @majocha pour l’async runtime.

Texte natif compatible avec l’AOT et tableaux à deux dimensions

Pour prendre en charge AOT natif, F# 11 supprime, pour les opérations de texte et de tableau les plus courantes, les exigences de mise en forme basées sur la réflexion et d’allocation dynamique.

Lorsque vous avez déjà une chaîne, vous n’avez pas besoin d’un format "%s" pour l’écrire. Nouvelles fonctions print principales et printn acceptent des valeurs par le biais de la conversion de string F# ; printn ajoute une nouvelle ligne :

let report (processed: int) (total: decimal) (elapsed: TimeSpan) : string =
    $"{processed} items, total {total:F2}, elapsed {elapsed.TotalMilliseconds:F0} ms"

printn (report 12 145.50m (TimeSpan.FromMilliseconds 250.0))
// 12 items, total 145.50, elapsed 250 ms

L’interpolation de chaîne utilise la conversion directe et String.Concat dans davantage de cas. Les espaces réservés au format .NET tels que {total:F2} utilisent un formatage invariant et encapsulent la valeur ; %A et les formats printf avec précision peuvent toujours nécessiter un formatage printf. Sélectionnez une culture explicite pour la sortie localisée.

Avec --reflectionfree, les records et les unions obtiennent également une sortie générée ToString. Consultez Interpolation et AOT natif pour obtenir des exemples de culture et des chemins pris en charge.

Merci à @bbatsov pour les fonctions d’impression et @charlesroddie pour l’implémentation d’interpolation.

Conserver les opérations ordinaires de grille basée sur zéro

Une grille de base zéro ne doit pas nécessiter de module Array2D de remplacement simplement pour éviter un chemin d’allocation de code dynamique. Les implémentations principales mises à jour prennent en charge l’initialisation et la transformation familières :

let grid = Array2D.init 3 4 (fun row column -> row * 4 + column)
let doubled = grid |> Array2D.map (fun value -> value * 2)
let snapshot = Array2D.copy doubled

printn $"{snapshot.GetLength(0)} x {snapshot.GetLength(1)}; last = {snapshot[2, 3]}"
// 3 x 4; last = 22

Array2D.create, init, et rebase utilisent le chemin d’allocation indexé à partir de zéro dans toutes les builds Core. mappréservant les limites, mapi et copy peuvent utiliser une API d’allocation .NET plus récente dans la version net10.0 ; les versions .NET Standard conservent leur mécanisme de repli. Les tableaux non basés sur zéro ne sont pas dépréciés, mais leurs restrictions d’allocation natives restent.

Consultez les tableaux à deux dimensions et AOT native pour connaître les chemins d’allocation et les restrictions restantes. Vérifiez les diagnostics de publication de l’application complète.

Merci à @T-Gro pour les améliorations array2D.

Moins de mémoire conservée et moins de travail répété

F# 11 réduit l’analyse répétée du compilateur et certaines allocations d’application. Celles-ci affectent différents workloads.

Partager des bibliothèques référencées entre des projets

Pour éviter les copies répétées de dépendances binaires partagées, le service linguistique peut partager des graphes de bibliothèques importées admissibles entre les projets.

Dans l’expérience de mémoire conservée de la PR d’implémentation, la solution ReSharper.FSharp de dix projets est passée de 1 547,2 Mo à 1 002,2 Mo avec le partage activé. Il s’agit d’une mesure binaire marche/arrêt pour cette charge de travail, et non d’une réduction générale de F# 10 à 11. Les solutions dominées par les références de projet plutôt que les références binaires partagées peuvent voir des gains beaucoup plus petits.

La lecture des métadonnées et la spécialisation inline réduisent également les allocations temporaires, une métrique distincte. Consultez les performances des outils pour connaître la portée et les contrôles.

Merci à @auduchinok pour les importations de bibliothèques partagées.

Compiler le travail indépendant en parallèle par défaut

La compilation parallèle est désormais activée par défaut, avec des règles de sortie déterministes et d’ordre source conservées. Les améliorations apportées à la mise en cache et au modèle protégé évitent d’autres tâches répétées. Visual Studio/FCS réduit également l’analyse des documents inchangés et les vérifications simultanées liées ; consultez les performances des outils.

Éviter la surcharge d’allocation de rappel et de résultat vide

Les fonctions sélectionnées List et Array les fonctions sont désormais des rappels inline. Pour vos propres assistances inline, OptimizeClosureIfNotInlined adapte un rappel curried opaque une fois :

let inline foldItems
    ([<InlineIfLambda; OptimizeClosureIfNotInlined>] folder)
    state
    (items: 'T[]) =
    let mutable total = state
    for item in items do
        total <- folder total item
    total

D’autres allocations peuvent rester. Consultez Optimiser les fonctions de rappel curryfiées pour connaître les exigences.

L’absence de résultats est un autre cas courant de fonctionnement réussi. La charge de travail qui a motivé FSharpLint a combiné plusieurs fois des règles qui n’ont trouvé aucun avertissement. Core réutilise désormais les tableaux vides partagés dans un plus grand nombre d’opérations :

let checkTitle (title: string) =
    if String.IsNullOrWhiteSpace title then [|"A title is required."|]
    else Array.empty

let warnings = ["Introduction"; "Installation"] |> List.toArray |> Array.collect checkTitle
let sharedEmpty = obj.ReferenceEquals(warnings, Array.empty<string>)
// true: no new empty result array

La modification supprime les objets tableau vide redondants, pas toutes les allocations dans le pipeline. Ne vous appuyez pas sur des résultats vides séparés ayant des identités de référence distinctes.

Merci pour @T-Groles rappels de collection inline et les tableaux vides partagés.

Appels d’API plus précis et fonctionnalités pratiques au quotidien

Exiger des noms lorsque l’ordre de paramètre généré peut changer

Pour protéger les API générées contre les modifications silencieuses de l’ordre des paramètres, un auteur peut exiger des arguments nommés :

Note

L’application des règles est une fonctionnalité de F# 11.2 ; dans le SDK .NET 11.0.100, activez la préversion. Une définition disponible System.Diagnostics.CodeAnalysis.RequireNamedArgumentsAttribute est également requise. L’exemple complet fournit une définition de compatibilité pour les références qui ne l’ont pas.

open System.Diagnostics.CodeAnalysis

type Query =
    [<RequireNamedArguments>]
    static member Execute(numerator: decimal, denominator: decimal) =
        numerator / denominator

let ratio = Query.Execute(numerator = 3m, denominator = 4m)
// 0.75M; Query.Execute(3m, 4m) is rejected.

La réorganisation de la déclaration conserve le résultat de l’appel nommé ; 0.75Mun appel positionnel est rejeté. Pour le cas d’usage de l’API générée, le provider doit émettre l’annotation.

Reconnaître un argument générique déjà encapsulé

Pour éviter d’encapsuler une tâche existante dans Task<'T> en tant que simple valeur de résultat, une nouvelle règle de surcharge de dernier recours préfère Task<'T> à 'T seul :

Note

Le comportement le plus concret du saut d’attache et de la priorité de surcharge sont des caractéristiques F# 11.2 ; dans le sdk .NET 11.0.100, activez la préversion.

open System.Threading.Tasks

let pending = Task.FromResult 42
let wrapped = ValueTask<_>(pending)
let answer: int = wrapped.Result
// ValueTask<int>, not ValueTask<Task<int>>

Le résultat est ValueTask<int>, pas ValueTask<Task<int>>. Consultez Overload selection pour connaître les limites de la règle.

Préférer une nouvelle surcharge sans supprimer l’ancien point d’entrée

Une bibliothèque peut conserver une ancienne méthode de diagnostic pour les fichiers binaires existants tout en ajoutant une surcharge de fonction qui inclut l’emplacement source de l’appelant. OverloadResolutionPriorityAttribute rend cette préférence explicite pour les appelants nouvellement compilés :

open System.Runtime.CompilerServices

type Check =
    static member That(condition: bool) =
        if condition then "OK" else "Check failed."

    [<OverloadResolutionPriority(1)>]
    static member That(condition: bool, [<CallerLineNumber>] ?line: int) =
        if condition then "OK" else $"Check failed at line {defaultArg line 0}."

let stock = 0
let diagnostic = Check.That(stock > 0)
// The diagnostic includes the caller's source line.

L’appel inchangé sélectionne la surcharge sensible aux lignes. Consultez la sélection de surcharge pour connaître les règles d’étendue et de compatibilité.

Utiliser les promesses de nullabilité conditionnelle de .NET

Si votre chemin d'accès de fichier n'est pas null, le résultat nullable d'une API Path ne doit pas forcer une vérification que son propre contrat exclut. F# 11 s’appuie sur la garantie .NET existante : une entrée nommée non null implique un résultat non null.

open System.IO

let fileName: string = Path.GetFileName("reports/annual.csv")
let extension: string = Path.GetExtension(fileName)
printn (extension.ToUpperInvariant())
// .CSV

let fileNameOf (path: string | null) : string | null =
    Path.GetFileName path

Activez <Nullable>enable</Nullable>. Les paramètres acceptant la valeur null ont toujours besoin d’une gestion des valeurs nulles ; consultez les contrats de retour conditionnels. Pour les API C# T? non contraintes, utilisez l’assistance distincteUnchecked.withNull.

Merci à @kerams pour la prise en charge conditionnelle de la nullabilité.

Améliorations mineures des langues

Pour appeler un type d’enregistrement avec la syntaxe de constructeur .NET, utilisez les constructeurs d’enregistrements F# 11.2 :

type Person = { Name: string; Age: int }
let person = Person(Name = "Ada", Age = 36)

Les arguments nommés évitent la dépendance à l’ordre des champs. Merci à @charlesroddie pour cette fonctionnalité.

Pour la source conditionnelle, #elif évite les groupes #if imbriqués :

#if EU_WAREHOUSE
let dispatchTimeZone = "Europe/Paris"
#elif US_WAREHOUSE
let dispatchTimeZone = "America/New_York"
#else
let dispatchTimeZone = "UTC"
#endif

System.Console.WriteLine(dispatchTimeZone)

La première branche correspondante gagne. L’exemple de projet définit EU_WAREHOUSE et imprime Europe/Paris; avec aucun symbole défini, la sortie est UTC.

Pour les outils associés et les cas d’interopérabilité :

Avant la mise à niveau

Gardez ces changements conséquents à l’esprit lors de la mise à niveau :

Code affecté Action
Bibliothèques contenant des fichiers .fsi Copiez les attributs de contrat appliqués aux signatures ; les omissions sont désormais des erreurs FS3888.
Bibliothèques inline prises en charge par une extension Passez en revue les signatures déduites et les exigences de producteur/consommateur. L’inférence F# 11.2 peut conserver des contraintes plus générales.
Délégués de méthodes d’instance Validez les receveurs avant la construction directe, lorsque des erreurs dues à un receveur nul se produisent désormais.
Sortie interpolée localisée Les trous de format .NET utilisent la culture invariante. Choisissez une culture explicite si une sortie localisée est attendue.
Builders personnalisés avec final do! Fournir la liaison de nom et l’achèvement de l’unité ; final do! ne sélectionne plus les membres de transfert marqués final.
Non terminé let! Ajoutez le corps de calcul suivant ; une liaison incomplète a désormais un diagnostic plus direct.
Anciens paramètres de langue Les modes numérotés pris en charge commencent à F# 8 ; Le mode de retrait non strict est supprimé. Consultez la version linguistique et l’indentation.
Opérations bit à bit sur les énumérations Utiliser des énumérations avec type sous-jacent entier ; Les types de stockage non valides sont rejetés.