C # 8 y validez nula. ¿Cómo vivimos con esto?

¡Hola colegas! Es hora de mencionar que tenemos planes para lanzar el libro fundamental de Ian Griffiths sobre C # 8:


Mientras tanto, en su blog, el autor ha publicado dos artículos relacionados en los que considera las complejidades de los nuevos fenómenos, como la nulabilidad, el olvido y la conciencia nula. Hemos traducido ambos artículos bajo un mismo título y sugerimos discutirlos.

La nueva característica más ambiciosa en C # 8.0 se denomina referencias anulables .

El propósito de esta nueva característica es suavizar el daño de una cosa peligrosa, que el científico informático Tony Hoar llamó una vez su " error de mil millones de dólares ". C # tiene una palabra clavenull(el equivalente se encuentra en muchos otros idiomas), y las raíces de esta palabra clave se remontan al lenguaje Algol W, en el desarrollo del cual participó Hoar. En este lenguaje antiguo (apareció en 1966), las variables que se refieren a instancias de cierto tipo podrían recibir un significado especial que indica que en este momento esta variable no está referenciada en ningún lado. Esta oportunidad fue muy prestada, y hoy en día muchos expertos (incluido el propio Hoar) creen que se ha convertido en la mayor fuente de costosos errores de software de todos los tiempos.

¿Qué hay de malo en asumir cero? En un mundo donde cualquier enlace puede apuntar a cero, debe considerar esto siempre que se usen enlaces en su código, de lo contrario corre el riesgo de ser rechazado en el tiempo de ejecución. A veces no es demasiado pesado; si inicializa una variable con una expresión newen el mismo lugar donde la declara, entonces sabe que esta variable no es igual a cero. Pero incluso un ejemplo tan simple está cargado de cierta carga cognitiva: antes del lanzamiento de C # 8, el compilador no podía decirle si está haciendo algo que pueda convertir este valor null. Pero, tan pronto como comience a unir diferentes fragmentos de código, se vuelve mucho más difícil juzgar con certeza sobre tales cosas: ¿qué tan probable es que esta propiedad que estoy leyendo ahora pueda volver null? ¿Está permitido transmitir?nullen ese método? ¿En qué situaciones puedo estar seguro de que el método al que llamo establecerá este argumento outno nullen un valor diferente? Además, el asunto ni siquiera se limita a recordar revisar tales cosas; no está del todo claro qué debe hacer si se encuentra con cero.

Con los tipos numéricos en C # no existe tal problema: si escribe una función que toma algunos números como entrada y devuelve un número como resultado, entonces no tiene que preguntarse si los valores transmitidos son realmente números y si algo entre ellos puede confundirse. Al llamar a tal función, no es necesario pensar si puede devolver algo en lugar de un número. A menos que tal desarrollo de eventos le interese como una opción: en este caso, puede declarar parámetros o resultados del tipoint?, lo que indica que en este caso particular realmente desea permitir la transmisión o devolución de un valor nulo. Entonces, para los tipos numéricos y, en un sentido más general, los tipos significativos, la tolerancia cero siempre ha sido una de esas cosas que se hacen voluntariamente, como una opción.

En cuanto a los tipos de referencia, antes de C # 8.0, la permisibilidad de cero no solo se configuraba de manera predeterminada, sino que tampoco se podía deshabilitar.

De hecho, por razones de compatibilidad con versiones anteriores, la validez cero continúa funcionando de manera predeterminada incluso en C # 8.0, porque las nuevas funciones de lenguaje en esta área permanecen deshabilitadas hasta que las solicite explícitamente.

Sin embargo, tan pronto como habilite esta nueva función, todo cambia. La forma más fácil de activarlo es agregarlo <Nullablegt;enable</Nullablegt;dentro del elemento.<PropertyGroup>en su archivo .csproj. (Observo que también hay disponible más control de filigrana. Si realmente lo necesita, puede configurar el comportamiento para que se permita por nullseparado en cada línea. Pero, cuando recientemente decidimos incluir esta característica en todos nuestros proyectos, resultó que se activaría a escala un proyecto a la vez es una tarea factible).

Cuando los enlaces permitidos en C # 8.0 están nullcompletamente activados, la situación cambia: ahora, por defecto, se supone que los enlaces no permiten nulos solo si usted no especifica lo contrario, exactamente como con los tipos significativos ( incluso la sintaxis es la misma: podría escribir int ?, si realmente desea que el valor entero sea opcional. ¿Ahora escribe cadena ?, si quiere decir que desea una referencia de cadena onull.)

Este es un cambio muy significativo y, en primer lugar, debido a su importancia, esta nueva característica está deshabilitada de forma predeterminada. Microsoft podría haber diseñado esta función de lenguaje de manera diferente: podría dejar los enlaces predeterminados anulables e introducir una nueva sintaxis que le permitiría especificar que desea asegurarse de que no esté permitido null. Quizás esto reduciría la barra al explorar esta posibilidad, pero a la larga tal solución sería incorrecta, ya que en la práctica la mayoría de los enlaces en la gran masa de código C # no están diseñados para señalar null.

Asumir cero es una excepción, no una regla, y es por eso que, cuando esta nueva función de idioma está habilitada, evitar que el nulo se convierta en un nuevo valor predeterminado. Esto se refleja incluso en el nombre de la característica original: "referencias anulables". El nombre es curioso, dado que los enlaces podrían apuntar nulla C # 1.0. Pero los desarrolladores decidieron enfatizar que ahora la suposición nula entra en la categoría de cosas que deben solicitarse explícitamente.

C # 8.0 suaviza el proceso de introducción de enlaces permisivos null, ya que le permite introducir esta función gradualmente. Uno no tiene que hacer una elección de sí o no. Esto es bastante diferente de la característica async/awaitagregada en C # 5.0, que tendió a extenderse: de hecho, las operaciones asincrónicas obligan a la persona que llama a serasyncy, por lo tanto, el código que llama a esta persona que llama debe estar async, y así sucesivamente, en la parte superior de la pila. Afortunadamente, los tipos que permiten nullse construyen de manera diferente: se pueden implementar de forma selectiva y gradual. Puede trabajar los archivos uno por uno, o incluso línea por línea, si es necesario.

El aspecto más importante de los tipos que permitennull(gracias a lo cual se simplifica la transición a ellos), es que por defecto están deshabilitados. De lo contrario, la mayoría de los desarrolladores se negarían a usar C # 8.0, ya que dicha transición provocaría advertencias en casi cualquier base de código. Sin embargo, por las mismas razones, el umbral de entrada para usar esta nueva característica se siente bastante alto: si una nueva característica realiza cambios tan drásticos que está deshabilitada de manera predeterminada, entonces probablemente no querrá meterse con ella, pero hay problemas asociados con cambiarla. siempre parecerá una molestia innecesaria. Pero esto sería una pena, porque la característica es muy valiosa. Ayuda a encontrar errores en el código antes de que los usuarios lo hagan por usted.

Entonces, si está considerando introducir tipos que permitannull, asegúrese de tener en cuenta que puede introducir esta función paso a paso.

Solo advertencias

El nivel más duro de control sobre todo el proyecto después de un simple encendido / apagado es la capacidad de activar advertencias independientemente de las anotaciones. Por ejemplo, si habilito completamente la suposición cero para Corvus.ContentHandling.Json en nuestro repositorio Corvus.ContentHandling , agregando <Nullablegt;enable</Nullablegt;al grupo de propiedades en el archivo del proyecto, aparecerán inmediatamente en su estado actual 20 advertencias del compilador. Sin embargo, si lo uso en su lugar, <Nullablegt;warnings</Nullablegt;solo recibiré una advertencia.

¡Pero espera! ¿Por qué se me mostrarán menos advertencias? Al final, acabo de pedir advertencias. La respuesta a esta pregunta no es del todo obvia: el hecho es que algunas variables y expresiones pueden ser nullnulas.

Neutralidad nula

C # admite dos interpretaciones de validez nula. En primer lugar, cualquier variable de un tipo de referencia puede declararse como admitiendo o no admitiendo null, y en segundo lugar, el compilador nullsiempre que sea posible concluirá lógicamente si esta variable puede o no estar en un punto particular del código. Este artículo trata solo sobre la primera variedad de admisibilidadnull, es decir, sobre el tipo estático de una variable (de hecho, esto se aplica no solo a las variables y parámetros y campos cercanos a ellos en espíritu; la admisibilidad estática y lógicamente deducible se nulldetermina para cada expresión en C #). De hecho, la admisibilidad nullen su primer sentido , el que estamos considerando es una extensión del sistema de tipos.

Sin embargo, resulta que si nos centramos solo en la admisibilidad nula para un tipo, la situación no será tan coherente como se podría suponer. Esto no es solo un contraste entre "validez nula" e "inválidonull". De hecho, hay dos posibilidades más. Hay una categoría de "desconocido", que es obligatoria debido a la disponibilidad de genéricos; si tiene un parámetro de tipo ilimitado, entonces no será posible averiguar nada sobre la validez null: el código que utiliza el método o tipo generalizado apropiado puede sustituir un argumento en ellos, permitiendo o no permitiendo null. Puede agregar restricciones, pero a menudo tales restricciones no son deseables, ya que limitan el alcance del tipo o método generalizado. Entonces, para variables o expresiones de algún parámetro de tipo ilimitado, la T(no) validez de cero debe ser desconocida; quizás, en cada caso, la cuestión de la admisibilidadnullse decidirá por separado para ellos, pero no sabemos qué opción aparecerá en el código genérico, ya que dependerá del argumento de tipo.

La última categoría se llama "neutral". Según el principio de "neutralidad", todo funcionó antes del advenimiento de C # 8.0, y esto funcionará si no activa la capacidad de trabajar con enlaces anulables. (Básicamente, este es un ejemplo de retroactividad . Aunque la idea de neutralidad nula se introdujo por primera vez en C # 8.0 como un estado natural de código antes de activar la validez nula para las referencias, los diseñadores de C # insistieron en que esta propiedad nunca fue realmente ajena a C #).

Quizás no tenga que explicar qué significa "neutralidad" en este caso, ya que fue en este sentido que C # siempre funcionó, por lo que usted mismo comprende todo ... aunque, quizás, esto es un poco deshonesto. Entonces escuche: en un mundo donde se sabe acerca de la admisibilidad null, la característica más importante de nulllas expresiones neutrales es que no provocan advertencias sobre la aceptabilidad nula. Puede asignar una expresión nula-neutral como una nullvariable permitida , pero no permitida. Nullvariables neutrales (así como propiedades, campos, etc.), puede asignar expresiones que el compilador considera "posibles null" o "no null".

Por eso, si solo activa las advertencias, entonces no hay tantas advertencias nuevas. Todo el código permanece en el contexto de las anotaciones de validez deshabilitadas null, por lo que todas las variables, parámetros, campos y propiedades serán nullneutrales, lo que significa que no recibirá ninguna advertencia si intenta usarlas junto con cualquier entidad que tenga en cuenta null.

¿Por qué, entonces, recibo advertencias? Una razón común se debe a un intento de hacer amigos de una manera inaceptable dos piezas de código que tienen en cuenta null. Por ejemplo, supongamos que tengo una biblioteca donde los enlaces permisivos están completamente incluidos null, y esta biblioteca tiene la siguiente clase profundamente ideada:

public static class NullableAwareClass
	{
	    public static string? GetNullable() => DateTime.Now.Hour > 12 ? null : "morning";
	

	    public static int RequireNonNull(string s) => s.Length;
	}

Además, en otro proyecto, puedo escribir este código en el contexto donde se activan las advertencias de validez nula, pero las anotaciones correspondientes están deshabilitadas:

static int UseString(string x) => NullableAwareClass.RequireNonNull(x);

Como las anotaciones sobre la validez nula están deshabilitadas, el parámetro xaquí es nullneutral. Esto significa que el compilador no puede determinar si este código es verdadero o no. Si el compilador emitió advertencias en los casos en que nulllas expresiones neutrales se mezclan con las que se tienen en cuenta null, una proporción significativa de estas advertencias podría considerarse dudosa; por lo tanto, no se emiten advertencias.

Con este contenedor, en realidad oculté el hecho de que el código tiene en cuenta la validez null. Esto significa que ahora puedo escribir así:

	int x = UseString(NullableAwareClass.GetNullable());

El compilador sabe lo que GetNullablepuede devolver null, pero como llamé al método con un parámetro neutral nulo, el programa no sabe si esto es correcto o incorrecto. Usando el nullcontenedor neutral, desarmé el compilador, que ahora no ve ningún problema aquí. Sin embargo, si combinara estos dos métodos directamente, todo sería diferente:

int y = NullableAwareClass.RequireNonNull(NullableAwareClass.GetNullable());

Aquí paso el resultado GetNullabledirectamente a RequireNonNull. Si intentara hacer esto en un contexto donde las suposiciones nulas están habilitadas, el compilador generaría una advertencia, independientemente de si activé o desactivé el contexto de las anotaciones correspondientes. En este caso particular, el contexto de las anotaciones no importa, ya que no hay declaraciones con un tipo de referencia. Si habilita las advertencias sobre el supuesto de nulo, pero deshabilita las anotaciones correspondientes, todas las declaraciones se volverán nullneutrales, lo que, sin embargo, no significa que todas las expresiones se vuelvan tales. Entonces, sabemos que el resultado GetNullablees nulo. Por lo tanto, recibimos una advertencia.

Resumiendo: ya que todas las declaraciones en el contexto de anotaciones deshabilitadas que permiten nullsonnull-neutral, no recibiremos muchas advertencias, ya que la mayoría de las expresiones serán null-neutral. Pero el compilador aún podrá detectar errores relacionados con la suposición nullen aquellos casos en que las expresiones no pasan por algún intermediario nulo-neutral. Además, el mayor beneficio en este caso será detectar errores asociados con intentos de desreferenciar valores nulos potenciales utilizando ., por ejemplo:

int z = NullableAwareClass.GetNullable().Length;

Si su código está bien diseñado, entonces no debería haber una gran cantidad de errores de este tipo.

Anotación gradual de todo el proyecto

Después de dar el primer paso: simplemente active las advertencias, luego puede proceder a la activación gradual de las anotaciones, archivo por archivo. Es conveniente incluirlos inmediatamente en todo el proyecto, ver en qué archivos aparecen las advertencias y luego seleccionar un archivo en el que haya relativamente pocas advertencias. Nuevamente, apáguelos al nivel de todo el proyecto y escriba en la parte superior del archivo que seleccionó #nullable enable. Esto habilitará completamente la suposición null(tanto para advertencias como para anotaciones) en todo el archivo (a menos que las desactive nuevamente usando otra directiva#nullable) Luego puede revisar todo el archivo y asegurarse de que todas las entidades que probablemente sean nulas estén anotadas como permitidas null(es decir, agregar ?), y luego lidiar con las advertencias en este archivo, si queda alguna.

Puede resultar que agregar todas las anotaciones necesarias es todo lo que se requiere para eliminar todas las advertencias. También es posible lo contrario: puede notar que cuando anota cuidadosamente un archivo sobre la valideznull, otras advertencias han aparecido en otros archivos que lo usan. Como regla general, no hay muchas advertencias de este tipo, y tiene tiempo para solucionarlas rápidamente. Pero, si por alguna razón después de este paso simplemente se ahoga en las advertencias, todavía tiene un par de soluciones. En primer lugar, puede simplemente cancelar la selección, dejar este archivo y tomar otro. En segundo lugar, puede desactivar selectivamente las anotaciones para aquellos miembros que cree que están causando la mayoría de los problemas. ( #nullablePuede usar la directiva tantas veces como lo desee, por lo que puede controlar la configuración de validez nula, incluso línea por línea, si lo desea). Quizás si regresa a este archivo más tarde cuando ya active la validez nula en la mayor parte del proyecto, verá menos advertencias que la primera vez.

Hay momentos en que los problemas no se pueden resolver de una manera tan directa. Entonces, en ciertos escenarios relacionados con la serialización (por ejemplo, cuando se usa Json.NET o Entity Framework), el trabajo puede ser más difícil. Creo que este problema merece un artículo separado.

Los enlaces con el supuesto nullmejoran la expresividad de su código y aumentan las posibilidades de que el compilador detecte sus errores antes de que los usuarios se encuentren con ellos. Por lo tanto, es mejor incluir esta característica si es posible. Y, si lo incluye de forma selectiva, los beneficios comenzarán a sentirse más rápido.

All Articles