C# 8.0 – Specification – Nullable reference types
Índice general – C# 8.0 – Specification
Introducción
Microsoft ha extendido o añadido en C# 8.0 el control de avisos o warnings en el código con nullables.
¿Pero porqué y para qué?. ¿Era realmente necesario?.
Comparándolo con lenguajes como F# que carecen de referencias null de forma directa (F# – Null Values), esta funcionalidad de Microsoft podría parecer una moda, un capricho o un simple deseo.
Pero reconozcámoslo, el origen de utilizar null tiene nombre y apellidos, Tony Hoare.
A este afable caballero -Sir Charles Antony Richard Hoare para más señas- le debemos mucho (bueno y malo) y posee un currículum envidiable y premios recibidos entre los que encontramos el importantísimo premio Turing.
Tony Hoare admitió en una conferencia celebrada en el año 2009 haber cometido el error del billón de dólares «billion-dolar mistake«.
Ese error no es otro que haber inventado en el año 1965 las referencias null como parte del lenguaje de programación ALGOL W.
Lo que Tony Hoare comentó en aquella conferencia es lo siguiente:
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
Dicho de otro modo, nadie duda ni debe dudar de lo que Tony Hoare ha dado a la informática, pero como vemos (y este es un claro y cristalino ejemplo), hasta las mentes más brillantes pueden errar.
Otra cosa es que la gente se atreva a contradecir a mentes como la de Tony, pero lo cierto es que lo que a veces sobre el papel parece resultar una buena idea puede no serlo tanto visto con perspectiva.
Llegados a este punto, Microsoft implementó en .NET y en su momento la referencia null, quiero pensar que no porque Tony lo hubiera potenciado y fuera extendido su uso (muchos lenguajes de programación lo usan), sino porque como le pasaba a Tony, su uso e implementación era la más sencilla y rápida.
Lamentablemente, su uso es uno de los errores más frecuentes en programación desencadenando daños colaterales, errores, pérdidas de dinero, de tiempo y de oportunidades (estas dos últimas no las dijo Tony pero lo digo yo porque casi siempre nos fijamos en el dinero cómo principal consecuencia para analizar un determinado problema).
Para más información acerca de null reference, te invito también a acceder a este enlace sobre Null pointer (o null reference).
Así que aquí nos podríamos preguntar por ejemplo:
¿Y qué sucede en Java cuando accedemos a una refencia nula?.
Que recibimos un NullPointerException.
¿Y en .NET qué sucede?
Algo parecido a lo que pasa en Java, pero en este caso recibiremos un NullReferenceException.
Ahora bien, ¿son los null el demonio?.
No exactamente, porque como todo en la vida, dependiendo de cómo nosotros mismos programemos nuestras aplicaciones aseguraremos la robusted necesaria a nuestro sistema o no, pero es indudable que si se nos facilita de alguna manera la vida, podremos asegurar más esa robusted.
Por lo tanto, y en el caso de .NET concretamente, si Microsoft nos proporcionara la posibilidad de que seamos nosotros los que elijamos qué hacer o cómo programar el tratamiento de null, sobre todo para los más despistados o los programadores menos experimentados, podremos evitar muy posiblemente problemas no deseados en tiempo de ejecución.
Así que después de hacer este largo preámbulo, vamos a centrarnos en sí en la nueva funcionalidad que nos ocupa para que entendamos los cambios introducidos y cómo trabajar con ellos.
Recordando el tratamiento de null
Creo que a nadie se le escapa a estas alturas que los tipos por referencia son punteros que direccionan al dato en sí (otra cosa son los tipos por valor que no voy a tratar en esta entrada).
Otra característica a destacar es que todos los tipos por referencia son nullables, es decir, un puntero puede tener un valor null.
El clásico tipo por referencia es el tipo string.
Así que el siguiente código, no generará ningún tipo de error en tiempo de compilación:
private static void NullableReferenceTypes() { string data = null; var result = ConvertToUpper(data); Console.WriteLine(result); } private static string ConvertToUpper(string data) { return data.ToUpper(); }
La aplicación que contiene esta declaración de código se ejecutará correctamente hasta que ejecute el método NullableReferenceTypes del ejemplo.
Es entonces cuando se producirá un error de tipo:
System.NullReferenceException: 'Object reference not set to an instance of an object.' data was null
Claro está en que podríamos mitigar el problema utilizando un try…catch, pero también es lógico pensar que eso conlleva un coste y que lo «lógico y razonable» es que a la función no entre un null.
Así que también podríamos preguntar antes si data es null, pero ¿y si se nos olvida?.
Así que como vemos, hay opciones de mitigar el problema, pero nuestras aplicaciones no están exentas de sufrir el problema planteado.
¿Y cuál es el objetivo principal entonces?.
Que en tiempo de desarrollo recibamos información sobre estos posibles olvidos para que evitemos problemas en tiempo de ejecución.
Nullable reference types
En C# 8.0 deberemos indicar explícitamente que queremos que un tipo por refencia utilice null.
Esto se consigue de la misma manera a como lo hacemos con los tipor por valor, añadiendo en el tipo de referencia el carácter: ?
Para lograr este propósito tenemos dos formas:
- Indicarlo así en el proyecto (fichero csproj)
- Utilizar directivas
Utilizarlo en el proyecto implica su uso para todo el código del proyecto.
Utilizarlo en directivas implica su uso en porciones de código únicamente.
CS8600 Converting null literal or possible null value to non-nullable type
Veamos un ejemplo de su uso para el caso del código CS8600.
Dentro de mi proyecto (.csproj), he definido a éste de la forma:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> </Project>
Nullable habilitará o no el uso de warnings en el caso de uso de null.
Los posibles valores de Nullable son ‘disable’, ‘enable’, ‘warnings’ o ‘annotations’.
Lo más habitual será utilizar disable o enable.
En nuestro caso enable para que nos avise de esos «olvidos o despistes».
Si compilamos el código que vimos anteriormente, recibiremos para string data = null; un warning:
Warning CS8600 Converting null literal or possible null value to non-nullable type.
Sin embargo, si nuestro código contiene más asignaciones a null, podemos recibir más avisos, y quizás, sólo queremos recibir mensajes para una porción de código concreta, por lo que ahí, podemos utilizar directivas.
El fichero de proyecto no debería tener ninguna definición a Nullable, así que la podemos eliminar para demostrar cómo funcionan las directivas.
Nuestro código utilizando directivas quedará de la siguiente forma:
private static void NullableReferenceTypes() { #nullable enable string data = null; #nullable restore var result = ConvertToUpper(data); Console.WriteLine(result); } private static string ConvertToUpper(string data) { return data.ToUpper(); }
En este caso, sólo aparecerá un warning para string data = null;.
Llegados a este punto, que duda cabe que cuando aparece un sólo warning en compilación, deberíamos revisar nuestra aplicación para que se despliegue sin ningún warning.
¿A que todos lo hacéis?.
Deberíamos hacerlo SIEMPRE sin duda, pero las prisas… y esas cosas… en fin… luego pasa lo que pasa en producción y nos volvemos locos intentando saber porqué.
Así que imaginemos que queremos que esos warnings sean errores.
Es decir, en mi caso quiero utilizar Nullable pero que los warnings de tipo CS8600 me los devuelva como errores.
Bastará en este caso modificar nuestro fichero de proyecto (csproj) para incluir WarningsAsErrors de la forma:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <Nullable>enable</Nullable> <WarningsAsErrors>CS8600</WarningsAsErrors> </PropertyGroup> </Project>
Si compilamos ahora nuestra aplicación, comprobaremos que ésta no se compila y devuelve un error del tipo:
Error CS8600 Converting null literal or possible null value to non-nullable type.
Si queremos incluir como errores más de un tipo CSxxxx, deberemos añadirlos concatenados y separados por ;
También podemos combinar entre directivas y WarningsAsErrors.
CS8602 Possible dereference of a null reference
Volviendo a los posibles avisos sobre tipos por referencia, veamos el CS8602 que indica posible de-referencia a referencia de nulos.
El tratamiento es muy parecido a lo que veíamos anteriormente, así que aunque los trate separadamente para demostrar cómo son y cómo diferenciarlos, el tratamiento se repite (recuerda que son tipos por referencia nullable).
Nuestro fichero de proyecto en este caso lo he dejado de esta forma:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> </Project>
El código que devuelve un warning de tipo CS8602 queda de la siguiente forma:
private static void PossibleDereferenceNullableReferenceTypes() { string data = null; Console.WriteLine(data.Length); }
Al compilar nuestra aplicación, recibiremos un warning de tipo:
Warning CS8602 Possible dereference of a null reference
En este caso, una forma de resolver el problema sería:
private static void PossibleDereferenceNullableReferenceTypes() { string data = null; Console.WriteLine(data?.Length); }
Nota: Aquí en este ejemplo, string data = null; genera un warning de tipo CS8600.
8603 Possible null reference return.
Continuando con los diferentes problemas con los que podemos encontrarnos, también podemos recibir avisos sobre otras partes del código relacionadas con lo tipos por referencia nulos, como por ejemplo con el código CS8603.
El tratamiento vuelve a ser el mismo.
Nuestro fichero de proyecto en este caso lo he dejado de esta forma:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> </Project>
El código que devuelve un warning de tipo CS8603 queda de la siguiente forma:
private static string PossibleNullableReferenceTypes() { var collection = new List(); return collection.Count > 0 ? collection[0] : null; }
Al compilar nuestra aplicación, recibiremos un warning de tipo:
Warning CS8603 Possible null reference return.
Exactamente de la misma forma, podemos hacer que este tipo de warnings se conviertan en errores.
En este caso, una forma de resolver el problema sería:
private static string PosibleNullableReferenceTypes() { var collection = new List(); return collection.Count > 0 ? collection[0] : String.Empty; }
Ahora bien, si queremos que realmente el valor de la función pueda ser null entonces podemos indicar que el tipo sea nullable, por lo que nuestro código «compatible» quedaría de la siguiente forma:
private static string? PosibleNullableReferenceTypes() { var collection = new List(); return collection.Count > 0 ? collection[0] : null; }
CS8604 Possible null reference argument for parameter xxx
Y lo mismo sucede en el caso del warning con código CS8604.
Nuestro fichero de proyecto quedará de la siguiente forma:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> </Project>
Y un ejemplo de código que desencadene este tipo de código sería el siguiente:
private static void PosibleArgumentNullableReferenceTypes() { string data = null; var result = ConvertToUpper(data); }
En este caso concreto, el mensaje que aparecerá en el compilador será similar al siguiente:
Warning CS8604 Possible null reference argument for parameter 'data' in 'string Program.ConvertToUpper(string data)'.
También nos podemos encontrar con el código CS8601.
No lo voy a mostrar aquí, pero el tratamiento será exactamente igual.
Podemos encontrar más información sobre estos códigos de error en la información de Microsoft sobre ErrorCode.cs.
Y sobre todos los códigos de error introducidos en C# 8.0 en este otro enlace sobre ErrorCode.cs.
null-forgiving operator
Ahora bien, también tenemos la posibilidad de «decirle» al compilador que no tenga en cuenta advertencias sobre un posible nulo sobre todo en el tratamiento del código CS8602.
Es lo que se conoce como null-forgiving operator.
El operador en concreto es: !
Lo mejor para entenderlo es verlo con un ejemplo:
Imaginemos la siguiente clase de partida:
private class Employee { public string? Name { get; set; } }
Ahora imaginemos el siguiente código de aplicación:
private static void NullForgivingOperator() { Employee? person = GetEmployee(); Console.WriteLine(person.Name); } private static Employee? GetEmployee() { return new Employee() { Name = "John" }; }
Si compilamos el código recibiremos un warning de código CS8602.
Lo que viene a indicarnos el compilador es que «es posible» que lo que tengamos en person.Name sea un null.
Imaginemos ahora que estamos completamente seguros de que NUNCA vendrá un null ahí.
No queremos que el compilador tenga en cuenta o considere la posibilidad de que venta un null, así que vamos a «forzar» que el compilador ignore su tratamiento de análisis.
Para ello, utilizaremos el operador !
Nuestro código en este ejemplo, quedará de la siguiente forma:
private static void NullForgivingOperator() { Employee? person = GetEmployee(); Console.WriteLine(person!.Name); }
Otras consideraciones
Una advertencia IMPORTANTE sobre todo al principio de usar esta forma de trabajar.
El uso de esta característica NO IMPLICA que nos relajemos o dejemos de atender posibles usos como verificación de null o uso de try…catch por poner dos ejemplos, sin embargo, no deberíamos tener que preguntar por null si hacemos bien las cosas.
Las migraciones deben ser realizadas de forma gradual.
En otra entrada intentaré indicar lo que a mi juicio sería un correcto flujo de trabajo a la hora de utilizar esta características en aplicaciones de C# que queremos migrar a C# 8.0.
Happy Coding!