Novedades de F# 11

Le damos la bienvenida a F# 11 Esta versión le permite mantener diferenciados los modelos de dominio, reutilizar algoritmos genéricos, coordinar operaciones asincrónicas y reducir el uso de memoria del compilador.

Gracias a la comunidad de F# por las propuestas, implementaciones, revisiones y pruebas que han hecho posibles estos cambios. Las siguientes características incluyen vínculos a colaboradores.

Note

Los ejemplos requieren un compilador de F# 11 y FSharp.Core 11 con las características que se muestran. En el SDK de .NET 11.0.100, las características de F# 11.2 requieren <LangVersion>preview</LangVersion>; consulte la configuración de idioma y la compatibilidad planeada del SDK. El ejemplo en tiempo de ejecución asincrónico tiene requisitos adicionales de tiempo de ejecución; es posible que un SDK de .NET 11 anterior no contenga todos los cambios.

Propagación de registros: modelos distintos sin asignaciones repetitivas

Para mantener diferenciados los modelos de almacenamiento y de aplicación, las propagaciones de registros permiten reutilizar los campos que comparten sin necesidad de declararlos repetidamente. Aquí los modelos comparten los detalles del cliente, pero usan tipos de identificador diferentes:

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 contribuye a las declaraciones de campos. En el límite, ...row copia los valores mientras la conversión analiza el identificador:

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

El destino mantiene los detalles compartidos, reemplaza Id y excluye InternalNote. Guid.TryParse, no la propagación, valida el identificador; La entrada con formato incorrecto genera Error.

Esto resuelve el problema de asignación de la capa de aplicación. Las propagaciones de valores también permiten combinar registros con nombre y anónimos, mientras que las propagaciones de tipos solo permiten crear registros con nombre. Los campos posteriores tienen prioridad y los orígenes se evalúan una sola vez, en orden. Consulte Operadores de propagación de registros para ver ejemplos de composición y riesgos de evolución compartida.

Gracias a @brianrourkeboll por las propagaciones de registros.

Miembros de extensión para algoritmos genéricos, operadores y tuplas

Para compartir un algoritmo entre los tipos existentes, F# 11.2 permite a los miembros de extensión satisfacer las restricciones de parámetros de tipo (SRTP) resueltas estáticamente. Por ejemplo, una transformación puede gestionar listas y opciones sin tipos de contenedor.

Note

En el SDK de .NET 11.0.100, habilite <LangVersion>preview</LangVersion> dónde se resuelven las extensiones y abra el módulo correspondiente. Consulte los requisitos de la biblioteca.

Compartir una transformación entre listas y opciones

Con map de la biblioteca en el ámbito, la aplicación puede limpiar cualquiera de las dos representaciones, cambiar los tipos de los elementos y componer transformaciones:

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]

Los adaptadores de Structural.Adapters usan operaciones habituales, en lugar de imponer contenedores nuevos:

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 función compartida en Structural.Algorithms solicita al tipo de entrada un miembro Map estático:

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

^Input y ^Output son tipos resueltos estáticamente. En cada llamada concreta, el compilador selecciona el miembro con la firma necesaria. Las extensiones no añaden la implementación de una interfaz. La aplicación completa abre ambos módulos.

Operadores, contexto y tipos de salida

El mismo mecanismo admite operadores de tupla y propagación de contexto y construcción orientada por la salida. Los encabezados de tupla nativos necesitan la característica F# 11.2 habilitada en sus declaraciones, así como en los sitios de llamadas de resolución de extensiones.

Una adición independiente, AllowOverloadOnReturnType, permite que una fábrica declare sobrecargas distinguidas por sus tipos de retorno:

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"

Este atributo Core 11 no requiere versión preliminar. La anotación de resultado selecciona una sobrecarga; sin ella, la llamada sigue siendo ambigua. Consulte Selección por tipo de valor devuelto.

Gracias a @gusty por ampliar la compatibilidad de programación genérica.

Integración de .NET y la experiencia de desarrollo

Los delegados directos conservan el contrato real del controlador

Para exponer los nombres de los controladores y los atributos de enlace a los marcos de trabajo, F# 11 permite hacer referencia directamente a una función con nombre compatible en lugar de utilizar un contenedor. API mínimas de ASP.NET Core pueden entonces interpretar el contrato de rutas, servicios y encabezados::

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)

Consulte el proyecto ejecutable y las reglas de construcción y compatibilidad. Las capturas y la adaptación de argumentos todavía pueden requerir envoltorios.

Gracias a @kerams por los delegados directos.

Reutilizar documentación XML

Para mantener los comentarios compartidos de la API en un único lugar, inclúyalos en shared.xml:

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

Con GenerateDocumentationFile activado, F# 11 expande <include> en las entradas de miembro generadas:

/// <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)

La implementación de include actualiza ambas descripciones a partir de un único fragmento. Por otra parte, el solucionador de herramientas expone las descripciones de base para <inheritdoc/>; el compilador conserva esa etiqueta para los consumidores de la documentación. Consulte la documentación XML para ver ejemplos y reglas de resolución.

Gracias a @T-Gro por la documentación XML compartida en FSharp.Core.

Depurar funciones en línea en compilaciones no optimizadas

Para inspeccionar los argumentos de las funciones auxiliares insertadas, establezca un punto de interrupción en let total en una compilación no optimizada y entre en addFee. F# 11 ahora prefiere métodos que se puedan invocar en estas compilaciones:

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

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

Las compilaciones optimizadas conservan el inlining. Algunas construcciones deben seguir expandiéndose. Consulte Depurar funciones insertadas para obtener información sobre estas limitaciones y la opción --always-inline.

Asincronía y coordinación de tareas

Para conectar flujos de trabajo de F# con API basadas en tareas, FSharp.Core 11 agrega puentes compatibles con cancelación, lotes limitados y transformaciones de resultados.

Limite las operaciones de E/S pendientes, no solo la colección de resultados

Un lote grande de operaciones de lectura puede iniciar más trabajo del que una aplicación debería tener pendiente. Para decidir cuándo comienza el trabajo basado en tareas, un coordinador necesita fábricas, no tareas que ya se hayan iniciado:

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

Como máximo, cuatro lecturas se realizan de forma concurrente; los resultados mantienen el orden de entrada. Un error solicita la cancelación de las tareas relacionadas mediante los tokens proporcionados. Esto limita la simultaneidad, no las solicitudes por segundo. Consulte Funciones asincrónicas y de tareas para obtener información sobre las variantes secuenciales y las que devuelven un resultado de tipo unit.

Conserve la cancelación y la excepción subyacente al atravesar el límite

Cuando un flujo de trabajo inicia una operación externa, su token debe propagarse junto con esa operación. Async.StartTaskImmediate pasa el token del flujo de trabajo en ejecución a la factoría de tareas y espera a que finalice:

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 error en una sola tarea llega a este controlador como la excepción FileNotFoundException subyacente. Para una tarea o tarea de valor ya creada, utilice Async.Await; el Async.AwaitTask anterior conserva su comportamiento de encapsulación de excepciones.

Para los autores de llamadas basadas en tareas, Task.startAsyncImmediate token workflow proporciona la conversión inversa. Se inicia en el subproceso de llamada y continúa ejecutándose en él hasta la primera suspensión asincrónica.

Usar métodos auxiliares correspondientes para transformaciones breves

Para transformar un único resultado sin necesidad de utilizar una expresión de cálculo completa, utilice los métodos auxiliares correspondientes de Microsoft.FSharp.Control. Por ejemplo, lea un archivo, obtenga su longitud y devuelva un error normal como Error:

open System.IO
open System.Threading

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

Un archivo que contiene Report 1 proporciona Ok 8; un archivo que falta proporciona un Error objeto que contiene FileNotFoundException. La cancelación sigue propagándose. Async y ValueTask ofrecen métodos auxiliares equivalentes para map, bind, y el tratamiento de errores. Consulte Funciones asincrónicas y de tareas. Utilice una expresión de cálculo para ramas o varios valores dependientes.

Los métodos auxiliares de ValueTask requieren la compilación netstandard2.1 o net10.0 de Core y un único consumo.

Gracias a @bartelink por sus contribuciones a la API de async/task.

Volver a generar una excepción después de notificar un error de forma asincrónica

El registro operativo debe agregar información sin cambiar lo que captura la función que llama ni ocultar dónde comenzó el fallo. Los controladores de expresiones de cálculo ahora pueden utilizar reraise () incluso después de esperar a que finalice la operación asincrónica de registro:

Note

La nueva propagación de excepciones en expresiones de cálculo es una característica de F# 11.2. En el SDK de .NET 11.0.100, habilite <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 ()
    }

Después del inicio de sesión correcto, se propaga la misma excepción y la información original de la ubicación donde se lanzó la excepción. Un error del registrador puede reemplazar la excepción original. Esto aborda la solicitud de observabilidad asíncrona sin asincronía en tiempo de ejecución. Consulte Volver a generar una excepción desde una expresión de cálculo.

Gracias a @xperiandri por contribuir a esta compatibilidad.

FSharp.Core puede compilarse en versiones modernas de .NET

Para usar API modernas sin dejar de ser compatible con versiones de destino anteriores, FSharp.Core añade net10.0 junto a netstandard2.0 y netstandard2.1. NuGet selecciona la compilación compatible. El empaquetado de herramientas actualizado también permite la liberación asincrónica basada en tareas en F# Interactive, como en proyectos compilados. Consulte Marcos de destino de FSharp.Core.

Gracias a @T-Gro por sus contribuciones a la compatibilidad de Core con los marcos de destino modernos y al empaquetado de herramientas del SDK.

Ejecución asincrónica en tiempo de ejecución para autores de generadores

Los autores de generadores pueden permitir que el entorno de ejecución suspenda y reanude los métodos marcados, en lugar de utilizar máquinas de estados generadas por el compilador. Un builder experimental definido por la biblioteca expone el mecanismo como una expresión de cálculo:

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

Note

Este generador experimental utiliza una característica de F# 11.2 en el punto donde se define o expande su implementación insertada; en el SDK de .NET 11.0.100, habilite las características en versión preliminar. También requiere la compilación net10.0 de Core 11, las referencias correspondientes y un entorno de ejecución que admita la ejecución asincrónica en tiempo de ejecución. La combinación de entorno de ejecución y referencias verificada en este caso es .NET 11 RC1. Usar solo .NET 10 como destino no es suficiente. Consulte Experimentos asíncronos del entorno de ejecución.

FSharp.Core no incluye este ejemplo como constructor de propósito general. El código convencional de task y async no cambia automáticamente a este mecanismo, y los autores de llamadas que utilizan una API convencional ya compilada que devuelve tareas no necesitan necesariamente habilitar las características en versión preliminar.

Gracias a @majocha por el async en tiempo de ejecución.

Texto nativo compatible con AOT y matrices bidimensionales

Para admitir AOT nativo, F# 11 quita el formato basado en reflexión y los requisitos de asignación dinámica de las operaciones de texto y matriz más comunes.

Si ya tiene una cadena, no necesita utilizar el formato "%s" solo para escribirla. Las nuevas funciones de Core print y printn aceptan valores mediante la conversión de F# string; printn anexa una nueva línea:

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

La interpolación de cadenas usa la conversión directa y String.Concat en más casos. Los marcadores de posición de formato .NET, como {total:F2}, utilizan un formato independiente de la referencia cultural y encapsulan el valor; %A y los especificadores de formato de printf que incluyen una precisión pueden seguir requiriendo el formato de printf. Seleccione una referencia cultural explícita para la salida localizada.

Con --reflectionfree, los registros y las uniones también pueden generar una representación de texto mediante ToString. Consulte Interpolación y AOT nativo para ver ejemplos de referencias culturales y las rutas de acceso admitidas.

Gracias a @bbatsov por las funciones de impresión y a @charlesroddie por la implementación de la interpolación.

Mantener las operaciones ordinarias de cuadrícula con índice base cero

Una cuadrícula de base cero no debe requerir un módulo Array2D de reemplazo solo para evitar una ruta de asignación de código dinámico. Las implementaciones principales actualizadas admiten la inicialización y transformación conocidas:

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 y rebase usan la ruta de asignación basada en cero en las compilaciones de Core. Los map, mapi y copy que preservan los límites pueden usar una API de asignación de .NET más reciente en la compilación net10.0; las compilaciones de .NET Standard conservan su mecanismo alternativo. Las matrices con índice inicial distinto de cero no están en desuso, pero sus restricciones nativas de asignación se mantienen.

Consulte Matrices bidimensionales y AOT nativo para obtener rutas de asignación y restricciones restantes. Compruebe los diagnósticos de publicación de la aplicación completa.

Gracias a @T-Gro por las mejoras de Array2D.

Menor retención de memoria y menos trabajo repetido

F# 11 reduce los análisis repetidos del compilador y algunas asignaciones de memoria de las aplicaciones. Estas mejoras afectan a distintas cargas de trabajo.

Compartir bibliotecas referenciadas entre proyectos

Para evitar copias repetidas de dependencias binarias compartidas, el servicio de lenguaje puede compartir grafos de bibliotecas importadas elegibles entre proyectos.

En el experimento de memoria conservada de la PR de implementación, la solución ReSharper.FSharp de diez proyectos cayó de 1547,2 MB a 1002,2 MB con el uso compartido habilitado. Se trata de una medición comparativa con esta optimización deshabilitada y habilitada para esa carga de trabajo, no de una reducción general del uso de memoria al pasar de F# 10 a F# 11. Las soluciones dominadas por referencias a proyectos en lugar de por referencias binarias compartidas pueden experimentar mejoras mucho menores.

La lectura de metadatos y la especialización de funciones insertadas también reducen las asignaciones temporales de memoria, que constituyen una métrica independiente. Consulte Rendimiento de herramientas para ver el ámbito y los controles.

Gracias a @auduchinok por las importaciones de bibliotecas compartidas.

Compilar trabajos independientes en paralelo de forma predeterminada

La compilación en paralelo ahora está habilitada de forma predeterminada, y se mantienen los resultados deterministas y las reglas de ordenación de los archivos de código fuente. Las mejoras en el almacenamiento en caché de sobrecargas y los patrones con condiciones de guarda evitan otras operaciones repetitivas. Visual Studio/FCS también reducen los análisis de documentos sin modificar y limitan las comprobaciones simultáneas; consulte Rendimiento de herramientas.

Evitar la sobrecarga de asignación de memoria de las devoluciones de llamada y los resultados vacíos

Las funciones List y Array seleccionadas ahora insertan devoluciones de llamada. Para sus propios métodos auxiliares insertados, OptimizeClosureIfNotInlined adapta una sola vez una devolución de llamada currificada opaca:

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

Pueden seguir produciéndose otras asignaciones de memoria. Consulte Optimizar devoluciones de llamada currificadas para conocer los requisitos.

Los resultados vacíos son otro caso habitual de operaciones completadas correctamente. La carga de trabajo de FSharpLint que motiva esta mejora combinaba repetidamente reglas que no detectaban advertencias. Core ahora reutiliza arrays vacíos compartidos en más operaciones:

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

Este cambio elimina los objetos de matriz vacía redundantes, pero no todas las asignaciones de memoria de la canalización. No dé por sentado que los distintos resultados vacíos tienen identidades de referencia diferentes.

Gracias a @T-Gro por su contribución a las devoluciones de llamada insertadas de colecciones y las matrices vacías compartidas.

Llamadas a API más precisas y mejoras prácticas para el uso cotidiano

Exigir nombres cuando puede cambiar el orden generado de los parámetros

Para proteger las API generadas frente a cambios silenciosos en el orden de los parámetros, un autor puede requerir argumentos con nombre:

Note

La aplicación de esta restricción es una característica de F# 11.2; en el SDK de .NET 11.0.100, habilite las características en versión preliminar. También es necesario disponer de una definición de System.Diagnostics.CodeAnalysis.RequireNamedArgumentsAttribute. El ejemplo completo incluye una definición de compatibilidad para las referencias que carecen de ella.

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.

Al reordenar la declaración, se preserva el resultado de la llamada con argumentos con nombre, 0.75M; se rechaza una llamada con argumentos posicionales. Para el caso de uso de la API generada, el proveedor debe emitir la anotación.

Reconocer un argumento genérico ya encapsulado

Para evitar encapsular una tarea existente como un valor de resultado simple, una nueva regla de resolución de sobrecargas que se aplica como último recurso da preferencia a Task<'T> sobre 'T sin encapsular:

Note

El comportamiento de desempate más concreto y prioridad de sobrecarga son las características F# 11.2; en el SDK de .NET 11.0.100, habilite la versión preliminar.

open System.Threading.Tasks

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

El resultado es ValueTask<int>, no ValueTask<Task<int>>. Consulte Selección de sobrecargas para conocer las limitaciones de esta regla.

Preferir una nueva sobrecarga sin quitar el punto de entrada anterior

Es posible que una biblioteca mantenga un método de diagnóstico antiguo para los archivos binarios existentes al agregar una sobrecarga que incluya la ubicación en el código fuente de quien llama. OverloadResolutionPriorityAttribute hace que esa preferencia sea explícita para el código que llama recién compilado:

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.

La llamada sin modificar selecciona la sobrecarga que tiene en cuenta la información de línea. Consulte Selección de sobrecargas para ver el ámbito y las reglas de compatibilidad.

Utilizar las garantías de nulabilidad condicional de .NET

Si la ruta de acceso del archivo no es nula, el resultado que admite valores null de una API de Path no debería obligarle a realizar una comprobación que el propio contrato de la API descarta. F# 11 reconoce la garantía existente de .NET NotNullIfNotNull: una entrada con nombre que no sea nula implica un resultado no 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

Habilite <Nullable>enable</Nullable>. Las entradas que admiten valores NULL siguen requiriendo un tratamiento de estos valores; consulte Contratos de devolución condicional. Para las API de C# con T? sin restricciones, utilice el método auxiliar independiente Unchecked.withNull.

Gracias a @kerams por la compatibilidad con la nulabilidad condicional.

Pequeñas mejoras lingüísticas

Para invocar un tipo de registro con sintaxis de constructor .NET, use los constructores de registros de F# 11.2:

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

Los argumentos con nombre evitan depender del orden de los campos. Gracias a @charlesroddie por esta característica.

Para el origen condicional, #elif evita los grupos anidados #if :

#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 primera rama coincidente gana. El proyecto de ejemplo define EU_WAREHOUSE e imprime Europe/Paris; sin ningún símbolo definido, la salida es UTC.

Para los casos relacionados de herramientas e interoperabilidad:

Antes de actualizar

Tenga en cuenta estos cambios derivados al actualizar:

Código afectado Action
Bibliotecas con archivos .fsi Copie los atributos de contrato de aplicación obligatoria en las firmas; las omisiones ahora generan errores FS3888.
Bibliotecas de funciones insertadas basadas en extensiones Revise las firmas inferidas y los requisitos de productor/consumidor. La inferencia de tipos de F# 11.2 puede conservar restricciones más generales.
Delegados de métodos de instancia Valide las instancias receptoras antes de la construcción directa, ya que los errores provocados por instancias receptoras nulas se producen ahora.
Salida interpolada localizada Los marcadores de posición de formato .NET utilizan la referencia cultural invariable. Seleccione una referencia cultural explícita cuando desee obtener una salida localizada.
Generadores personalizados con do! final Proporcione enlace y finalización de tipo unit; do! al final de una expresión ya no selecciona los miembros de reenvío final.
Sin terminar let! Agregue el cuerpo de cálculo siguiente; ahora, un enlace incompleto genera un diagnóstico más directo.
Configuración del lenguaje anterior Los modos numerados admitidos comienzan en F# 8; Se quita el modo de sangría no estricta. Consulte Versión del lenguaje y sangría.
Operaciones bit a bit con enumeraciones Use enumeraciones con un tipo entero subyacente; se rechazan los tipos subyacentes no válidos.