F# actualmente soporta varias versiones de nulabilidad. Primero hay tipos normales de referencia en .NET. Hoy en día no hay manera de informar inequívocamente al compilador si una variable de tipo de referencia específica es nullable o no, por lo que se desaconseja su uso en F #.
La alternativa preferida es la Option<T>. También conocido como un tipo de "tal vez", esta es una forma segura para expresar el concepto de nulabilidad. Cuando se usa con el código F # idiomático, solo puede leer el valor después de verificar si no es nulo (no es "ninguno" en el lenguaje F #). Esto generalmente se hace a través de la comparación de patrones. Por ejemplo,
match ParseDateTime inputString with
| Some(date) -> printfn "%s" (date.ToLocalTime().ToString())
| None -> printfn "Failed to parse the input."
| Some(date) -> printfn "%s" (date.ToLocalTime().ToString())
| None -> printfn "Failed to parse the input."
Option<T> es un tipo de referencia. Esto puede llevar a una gran cantidad de uso de memoria innecesaria, por lo que se creó una alternativa basada en estructura llamada ValueOption<T> en F # 4.5. Sin embargo, a ValueOption<T> le faltan algunas características que no estarán completas hasta que se lance F # 4.6.
Otro tipo de null F # con el que los desarrolladores deben tratar es Nullable<T>. Esto es similar a ValueOption<T>, pero está restringido a solo tipos de valor.
Las clases también se pueden marcar como nullables. Si el tipo tiene el atributo AllowNullLiteral, entonces todas las variables de ese tipo se consideran nullables. Esto puede ser problemático cuando desea que algunas variables de ese tipo sean nullables, pero no otras.
Las clases también se pueden marcar como nullables. Si el tipo tiene el atributo AllowNullLiteral, entonces todas las variables de ese tipo se consideran nullables. Esto puede ser problemático cuando desea que algunas variables de ese tipo sean nullables, pero no otras.
Un problema fundamental de diseño con F # tal como se encuentra actualmente es que todas estas formas diferentes de nulabilidad son incompatibles. No solo se necesitan conversiones entre los diferentes tipos de nulos, sino que también existen diferencias importantes en la forma en que funcionan. Por ejemplo, una Option<T> es recursiva, lo que le permite tener una Option<Option<Int32>> mientras que Nullable<T> no lo es. Esto puede llevar a problemas inesperados cuando se mezclan.
Otra forma en que esta incompatibilidad entra en juego es el atributo CLIMutable. Normalmente, los tipos de registro son inmutables, pero eso los hace incompatibles con los ORM. Este atributo soluciona ese problema, pero introduce uno nuevo. Ahora que el registro es mutable, los nulos se pueden deslizar después de crear el objeto, rompiendo la suposición de que los registros no contienen nulos.
El plan actual es indicar variables anulables con un ? sufijo como vemos en C#. Y al igual que C# 8, recibirá advertencias si intenta invocar un método o una propiedad en una variable que puede contener nulos sin verificar primero si es nulo. Del mismo modo, la asignación de un valor nulo a una variable no anulable es solo una advertencia, por lo que el código heredado continúa compilando.
Esta funcionalidad se considera como opt-in. Los nuevos proyectos lo tendrán activado de forma predeterminada, mientras que los proyectos existentes lo desactivarán de forma predeterminada.
Los ejemplos a continuación fueron proporcionados por la propuesta de Tipos de Referencia de Nullable y están sujetos a cambios.
// Declared type at let-binding
let notAValue : string? = null
// Declared type at let-binding
let isAValue : string? = "hello world"
let isNotAValue2 : string = null // gives a nullability warning
let getLength (x: string?) = x.Length // gives a nullability warning since x is a nullable string
// Parameter to a function
let len (str: string?) =
match str with
| null -> -1
| NonNull s -> s.Length // binds a non-null result
// Parameter to a function
let len (str: string?) =
let s = nullArgCheck "str" str // Returns a non-null string
s.Length // binds a non-null result
// Declared type at let-binding
let maybeAValue : string? = hopefullyGetAString()
// Array type signature
let f (arr: string?[]) = ()
// Generic code, note 'T must be constrained to be a reference type
let findOrNull (index: int) (list: 'T list) : 'T? when 'T : not struct =
match List.tryItem index list with
| Some item -> item
| None -> null
let notAValue : string? = null
// Declared type at let-binding
let isAValue : string? = "hello world"
let isNotAValue2 : string = null // gives a nullability warning
let getLength (x: string?) = x.Length // gives a nullability warning since x is a nullable string
// Parameter to a function
let len (str: string?) =
match str with
| null -> -1
| NonNull s -> s.Length // binds a non-null result
// Parameter to a function
let len (str: string?) =
let s = nullArgCheck "str" str // Returns a non-null string
s.Length // binds a non-null result
// Declared type at let-binding
let maybeAValue : string? = hopefullyGetAString()
// Array type signature
let f (arr: string?[]) = ()
// Generic code, note 'T must be constrained to be a reference type
let findOrNull (index: int) (list: 'T list) : 'T? when 'T : not struct =
match List.tryItem index list with
| Some item -> item
| None -> null
Como puede ver, la nueva sintaxis se adapta bien a los patrones F # existentes, incluso admite la coincidencia de patrones de manera similar a la Option<T>.
También hay un conjunto de funciones de ayuda para agregar en el código general y la coincidencia de patrones.
isNull: determina si el valor dado es nulo.
nonNull: afirma que el valor no es nulo. Provoca una NullReferenceException cuando el valor es nulo, de lo contrario, devuelve el valor.
withNull: convierte el valor en un tipo que admite nulo como un valor normal.
(| NonNull |): cuando se usa en un patrón, se afirma que el valor que se compara no es nulo.
(| Null | NotNull |): Un patrón activo que determina si el valor dado es nulo.
Las firmas de funciones completas están disponibles en la propuesta.