Esta es una de las reglas que se repiten una y otra vez y que me deja perplejo.
Los nulos son malvados y deben evitarse siempre que sea posible .
Pero, pero, por mi ingenuidad, déjame gritar, ¡a veces un valor PUEDE estar significativamente ausente!
Permítanme preguntar esto en un ejemplo que proviene de este horrible código montado contra el patrón en el que estoy trabajando en este momento . Este es, en esencia, un juego multijugador basado en turnos web, donde los turnos de ambos jugadores se ejecutan simultáneamente (como en Pokémon, y opuestamente al ajedrez).
Después de cada turno, el servidor transmite una lista de actualizaciones al código JS del lado del cliente. La clase de actualización:
public class GameUpdate
{
public Player player;
// other stuff
}
Esta clase se serializa a JSON y se envía a los jugadores conectados.
La mayoría de las actualizaciones naturalmente tienen un jugador asociado con ellas; después de todo, es necesario saber qué jugador hizo qué movimiento en este turno. Sin embargo, algunas actualizaciones no pueden tener un reproductor significativamente asociado con ellas. Ejemplo: El juego ha sido forzado porque el límite de turno sin acciones ha sido excedido. Para tales actualizaciones, creo, es significativo que el jugador sea anulado.
Por supuesto, podría "arreglar" este código utilizando la herencia:
public class GameUpdate
{
// stuff
}
public class GamePlayerUpdate : GameUpdate
{
public Player player;
// other stuff
}
Sin embargo, no veo cómo esto mejora, por dos razones:
- El código JS ahora simplemente recibirá un objeto sin Player como una propiedad definida, que es lo mismo que si fuera nulo ya que ambos casos requieren verificar si el valor está presente o ausente;
- En caso de que se agregue otro campo anulable a la clase GameUpdate, tendría que poder usar la herencia múltiple para continuar con este diseño, pero MI es malo en sí mismo (según los programadores más experimentados) y, lo que es más importante, C # no lo tengo para que no pueda usarlo.
Tengo la impresión de que este fragmento de este código es uno de los muchos en los que un programador experimentado y bueno gritaría con horror. Al mismo tiempo, no puedo ver cómo, en este lugar, es nulo dañar algo y qué debería hacerse en su lugar.
¿Podría explicarme este problema?
fuente
Respuestas:
Es mejor devolver muchas cosas que anularlas.
(que es lo que te hizo considerar nulo en primer lugar)
El truco es darse cuenta de cuándo hacer esto. Null es realmente bueno para explotar en tu cara, pero solo cuando lo salvas sin verificarlo. No es bueno para explicar por qué.
Cuando no necesite hacer estallar, y no quiera verificar si es nulo, use una de estas alternativas. Puede parecer extraño devolver una colección si solo tiene 0 o 1 elementos, pero es realmente bueno para permitirle tratar silenciosamente los valores perdidos.
Las mónadas suenan elegantes, pero aquí todo lo que están haciendo es dejar que sea obvio que los tamaños válidos para la colección son 0 y 1. Si los tiene, considérelos.
Cualquiera de estos hace un mejor trabajo al dejar su intención clara que la nula. Esa es la diferencia más importante.
Puede ser útil entender por qué regresa en lugar de simplemente lanzar una excepción. Las excepciones son esencialmente una forma de rechazar una suposición (no son la única forma). Cuando pregunta por el punto donde se cruzan dos líneas, asume que se cruzan. Pueden ser paralelos. ¿Deberías lanzar una excepción de "líneas paralelas"? Bueno, podría, pero ahora tiene que manejar esa excepción en otro lugar o dejar que detenga el sistema.
Si prefiere quedarse donde está, puede convertir el rechazo de esa suposición en un tipo de valor que puede expresar resultados o rechazos. No es tan raro. Lo hacemos en álgebra todo el tiempo. El truco es hacer que ese valor sea significativo para que podamos entender lo que sucedió.
Si el valor devuelto puede expresar resultados y rechazos, debe enviarse a un código que pueda manejar ambos. El polimorfismo es realmente poderoso para usar aquí. En lugar de simplemente intercambiar cheques nulos por
isPresent()
cheques, puede escribir código que se comporte bien en cualquier caso. Ya sea reemplazando valores vacíos con valores predeterminados o haciendo silenciosamente nada.El problema con nulo es que puede significar demasiadas cosas. Entonces, simplemente termina destruyendo información.
En mi experimento de pensamiento nulo favorito, le pido que imagine una máquina Rube Goldbergian compleja que envía mensajes codificados utilizando luz de colores recogiendo bombillas de colores de una cinta transportadora, atornillándolas en un enchufe y encendiéndolas. La señal es controlada por los diferentes colores de los bulbos que se colocan en la cinta transportadora.
Se le ha pedido que haga que esta cosa horriblemente costosa cumpla con RFC3.14159 que establece que debería haber un marcador entre los mensajes. El marcador no debe ser luz en absoluto. Debe durar exactamente un pulso. La misma cantidad de tiempo que normalmente brilla una luz de un solo color.
No puede dejar espacios entre los mensajes porque el artilugio activa las alarmas y se detiene si no puede encontrar una bombilla para enchufar.
Todos los que están familiarizados con este proyecto se estremecen al pensar en tocar la maquinaria o su código de control. No es fácil cambiar. ¿Qué haces? Bueno, puedes sumergirte y comenzar a romper cosas. Tal vez actualizar este diseño horrible cosas. Sí, podrías hacerlo mucho mejor. O podría hablar con el conserje y comenzar a recolectar bombillas quemadas.
Esa es la solución polimórfica. El sistema ni siquiera necesita saber que algo ha cambiado.
Es realmente agradable si puedes lograrlo. Al codificar un rechazo significativo de una suposición en un valor, no tiene que forzar el cambio de la pregunta.
¿Cuántas manzanas me deberás si te doy 1 manzana? -1. Porque te debía 2 manzanas de ayer. Aquí la suposición de la pregunta, "me deberás", se rechaza con un número negativo. Aunque la pregunta era incorrecta, en esta respuesta se codifica información significativa. Al rechazarlo de manera significativa, la pregunta no se ve obligada a cambiar.
Sin embargo, a veces cambiar la pregunta en realidad es el camino a seguir. Si la suposición se puede hacer más confiable, aunque aún sea útil, considere hacerlo.
Su problema es que, normalmente, las actualizaciones provienen de los jugadores. Pero ha descubierto la necesidad de una actualización que no provenga del jugador 1 o del jugador 2. Necesita una actualización que diga que el tiempo ha expirado. Ninguno de los jugadores está diciendo esto, ¿qué debes hacer? Dejar al jugador nulo parece tan tentador. Pero destruye la información. Estás tratando de codificar el conocimiento en un agujero negro.
El campo de jugador no te dice quién se acaba de mudar. Crees que sí, pero este problema demuestra que es una mentira. El campo del jugador te dice de dónde vino la actualización. La actualización de tiempo expirado proviene del reloj del juego. Mi recomendación es darle al reloj el crédito que merece y cambiar el nombre del
player
campo a algo asísource
.De esta manera, si el servidor se está cerrando y tiene que suspender el juego, puede enviar una actualización que informa lo que está sucediendo y se enumera como la fuente de la actualización.
¿Esto significa que nunca deberías dejar algo afuera? No. Pero debe considerar qué tan bien se puede suponer que nada significa realmente un solo valor predeterminado bien conocido. Nada es realmente fácil de usar en exceso. Así que ten cuidado cuando lo uses. Hay una escuela de pensamiento que abarca sin favorecer nada . Se llama convención sobre la configuración .
A mí me gusta, pero solo cuando la convención se comunica claramente de alguna manera. No debería ser una forma de enturbiar a los novatos. Pero incluso si está usando eso, nulo aún no es la mejor manera de hacerlo. Nulo es una nada peligrosa . Null pretende ser algo que puedes usar hasta que lo uses, luego hace un agujero en el universo (rompe tu modelo semántico). A menos que hacer un agujero en el universo sea el comportamiento que necesitas, ¿por qué estás jugando con esto? Usa algo más.
fuente
null
No es realmente el problema aquí. El problema es cómo la mayoría de los lenguajes de programación actuales manejan la información que falta; no toman en serio este problema (o al menos no brindan el apoyo y la seguridad que la mayoría de los terrícolas necesitan para evitar las trampas) Esto lleva a todos los problemas que hemos aprendido a temer (Javas NullPointerException, Segfaults, ...).Veamos cómo lidiar con ese problema de una mejor manera.
Otros sugirieron el patrón de objeto nulo . Si bien creo que a veces es aplicable, hay muchos escenarios en los que simplemente no hay un valor predeterminado de talla única. En esos casos, los consumidores de la información posiblemente ausente deben poder decidir por sí mismos qué hacer si falta esa información.
Hay lenguajes de programación que proporcionan seguridad contra punteros nulos (llamada seguridad nula) en tiempo de compilación. Para dar algunos ejemplos modernos: Kotlin, Rust, Swift. En esos idiomas, el problema de la falta de información se ha tomado en serio y el uso de nulo es totalmente indoloro: el compilador le impedirá desreferenciar los puntos nulos.
En Java, se han realizado esfuerzos para establecer las anotaciones @ Nullable / @ NotNull junto con los complementos del compilador + IDE para alcanzar el mismo efecto. No fue realmente exitoso, pero eso está fuera del alcance de esta respuesta.
Un tipo genérico explícito que denota una posible ausencia es otra forma de tratarlo. Por ejemplo, en Java hay
java.util.Optional
. Puede funcionar:a nivel de proyecto o empresa, puede establecer reglas / pautas
java.util.Optional
lugar denull
Su comunidad / ecosistema de idiomas puede haber encontrado otras formas de abordar este problema mejor que el compilador / intérprete.
fuente
Optional.isPresent
isPresent()
no es el uso recomendado de OpcionalNo veo ningún mérito en la declaración de que nulo es malo. Revisé las discusiones al respecto y solo me ponen impaciente e irritado.
Nulo puede usarse incorrectamente, como un "valor mágico", cuando en realidad significa algo. Pero tendrías que hacer una programación bastante retorcida para llegar allí.
Nulo debe significar "no existe", que no se creó (todavía) o (ya) desapareció o no se proporciona si es un argumento. No es un estado, todo falta. En un entorno OO donde las cosas se crean y destruyen todo el tiempo, es una condición válida y significativa diferente de cualquier estado.
No del todo cierto, puedo obtener una advertencia por no verificar nulo antes de usar la variable. Y no es lo mismo. Es posible que no quiera ahorrar los recursos de un objeto que no tiene ningún propósito. Y lo más importante, tendré que buscar la alternativa de la misma manera para obtener el comportamiento deseado. Si me olvido de hacer eso, preferiría obtener una NullReferenceException en tiempo de ejecución que algún comportamiento indefinido / no intencionado.
Hay un problema a tener en cuenta. En un contexto de subprocesos múltiples, debe protegerse contra las condiciones de carrera asignando primero a una variable local antes de realizar la verificación de nulo.
No, significa / no debe significar nada, por lo que no hay nada que destruir. El nombre y el tipo de la variable / argumento siempre dirá lo que falta, eso es todo lo que hay que saber.
Editar:
Estoy a medio camino del video candied_orange vinculado a una de sus otras respuestas sobre programación funcional y es muy interesante. Puedo ver su utilidad en ciertos escenarios. El enfoque funcional que he visto hasta ahora, con trozos de código volando, generalmente me da vergüenza cuando se usa en un entorno OO. Pero supongo que tiene su lugar. Algun lado. Y supongo que habrá idiomas que harán un buen trabajo ocultando lo que percibo como un desastre confuso cada vez que lo encuentre en C #, lenguajes que proporcionan un modelo limpio y viable.
Si ese modelo funcional es su mentalidad / marco, realmente no hay alternativa para impulsar cualquier percance como descripciones documentadas de errores y, en última instancia, dejar que la persona que llama se ocupe de la basura acumulada que fue arrastrada hasta el punto de inicio. Aparentemente, así es como funciona la programación funcional. Llámalo una limitación, llámalo un nuevo paradigma. Puede considerar que es un truco para los discípulos funcionales que les permite continuar un poco más en el camino impuro, puede estar encantado con esta nueva forma de programación que desata nuevas y emocionantes posibilidades. Lo que sea, me parece inútil entrar en ese mundo, volver a la forma tradicional de hacer las cosas (sí, podemos lanzar excepciones, lo siento), elegir algún concepto de él,
Cualquier concepto puede ser usado de manera incorrecta. Desde mi punto de vista, las "soluciones" presentadas no son alternativas para un uso apropiado de nulo. Son conceptos de otro mundo.
fuente
None
representa ...Tu pregunta.
Totalmente a bordo con usted allí. Sin embargo, hay algunas advertencias que abordaré en un segundo. Pero primero:
Creo que esta es una declaración bien intencionada pero demasiado generalizada.
Los nulos no son malos. No son antipatrones. No son algo que deba evitarse a toda costa. Sin embargo, los valores nulos son propensos a errores (por no comprobar los valores nulos).
Pero no entiendo cómo el hecho de no verificar nulo es de alguna manera una prueba de que nulo es inherentemente incorrecto de usar.
Si nulo es malo, también lo son los índices de matriz y los punteros de C ++. Ellos no están. Son fáciles de disparar en el pie, pero se supone que no deben evitarse a toda costa.
Para el resto de esta respuesta, voy a adaptar la intención de "los nulos son malvados" a otros "los nulos deben ser manejados de manera responsable ".
Tu solución.
Si bien estoy de acuerdo con su opinión sobre el uso de nulo como regla global; No estoy de acuerdo con su uso de nulo en este caso particular.
Para resumir los comentarios a continuación: estás malinterpretando el propósito de la herencia y el polimorfismo. Si bien su argumento para usar nulo tiene validez, su "prueba" adicional de por qué la otra forma es mala se basa en el mal uso de la herencia, lo que hace que sus puntos sean algo irrelevantes para el problema nulo.
Si estas actualizaciones son significativamente diferentes (que lo son), entonces necesita dos tipos de actualizaciones separadas.
Considere los mensajes, por ejemplo. Tiene mensajes de error y mensajes de chat de mensajería instantánea. Pero si bien pueden contener datos muy similares (un mensaje de cadena y un remitente), no se comportan de la misma manera. Deben usarse por separado, como clases diferentes.
Lo que está haciendo ahora es diferenciar efectivamente entre dos tipos de actualización en función de la nulidad de la propiedad Player. Eso es equivalente a decidir entre un mensaje de MI y un mensaje de error basado en la existencia de un código de error. Funciona, pero no es el mejor enfoque.
Su argumento se basa en errores del polimorfismo. Si tiene un método que devuelve un
GameUpdate
objeto, generalmente no debería importar diferenciar entreGameUpdate
,PlayerGameUpdate
oNewlyDevelopedGameUpdate
.Una clase base debe tener un contrato que funcione completamente, es decir, sus propiedades se definen en la clase base y no en la clase derivada (dicho esto, el valor de estas propiedades base, por supuesto, puede anularse en las clases derivadas).
Esto hace que su argumento sea discutible. En una buena práctica, nunca debe preocuparte que tu
GameUpdate
objeto tenga o no un jugador. Si está intentando acceder a laPlayer
propiedad de aGameUpdate
, está haciendo algo ilógico.Los lenguajes mecanografiados como C # ni siquiera le permitirán intentar acceder a la
Player
propiedad de aGameUpdate
porqueGameUpdate
simplemente no tiene la propiedad. Javascript es considerablemente más laxo en su enfoque (y no requiere precompilación), por lo que no se molesta en corregirlo y, en su lugar, hace que explote en tiempo de ejecución.No debería crear clases basadas en la adición de propiedades anulables. Deberías crear clases basadas en tipos de actualizaciones de juegos funcionalmente únicos .
¿Cómo evitar los problemas con nulo en general?
Creo que es relevante elaborar cómo puede expresar significativamente la ausencia de un valor. Esta es una colección de soluciones (o malos enfoques) sin ningún orden en particular.
1. Utilice nulo de todos modos.
Este es el enfoque más fácil. Sin embargo, se está registrando para un montón de comprobaciones nulas y (si no lo hace) para solucionar las excepciones de referencia nula.
2. Use un valor sin sentido no nulo
Un ejemplo simple aquí es
indexOf()
:En lugar de
null
, obtienes-1
. A nivel técnico, este es un valor entero válido. Sin embargo, un valor negativo es una respuesta sin sentido ya que se espera que los índices sean enteros positivos.Lógicamente, esto solo puede usarse para casos en los que el tipo de retorno permite más valores posibles de los que pueden devolverse significativamente (indexOf = enteros positivos; int = enteros negativos y positivos)
Para los tipos de referencia, aún puede aplicar este principio. Por ejemplo, si tiene una entidad con una ID entera, podría devolver un objeto ficticio con su ID establecida en -1, en lugar de nulo.
Simplemente tiene que definir un "estado ficticio" mediante el cual puede medir si un objeto es realmente utilizable o no.
Tenga en cuenta que no debe confiar en valores de cadena predeterminados si no controla el valor de la cadena. Puede considerar establecer el nombre de un usuario en "nulo" cuando desee devolver un objeto vacío, pero podría encontrar problemas cuando Christopher Null se suscriba a su servicio .
3. Lanzar una excepción en su lugar.
No simplemente no. Las excepciones no deben manejarse para el flujo lógico.
Dicho esto, hay algunos casos en los que no desea ocultar la excepción (nula referencia), sino que muestra una excepción apropiada. Por ejemplo:
Esto puede arrojar un
PersonNotFoundException
(o similar) cuando no hay una persona con ID 123.Obviamente, esto solo es aceptable para los casos en que no encontrar un artículo hace que sea imposible realizar un procesamiento adicional. Si no es posible encontrar un elemento y la aplicación puede continuar, NUNCA debe lanzar una excepción . No puedo enfatizar esto lo suficiente.
fuente
El punto de por qué los nulos son malos no es que el valor faltante se codifique como nulo, sino que cualquier valor de referencia puede ser nulo. Si tiene C #, probablemente esté perdido de todos modos. No veo un punto para usar algunos Opcionales allí porque un tipo de referencia ya es opcional. Pero para Java hay anotaciones @Notnull, que parece que puede configurar a nivel de paquete , luego, para los valores que se pueden perder, puede usar la anotación @Nullable.
fuente
Los nulos no son malos, no existe una forma realista de implementar ninguna de las alternativas sin tratar en algún momento algo que parece un nulo.
Los nulos son sin embargo peligrosos . Al igual que cualquier otra construcción de bajo nivel, si te equivocas, no hay nada debajo para atraparte.
Creo que hay muchos argumentos válidos contra nulo:
Que si ya está en un dominio de alto nivel, hay patrones más seguros que usar el modelo de bajo nivel.
A menos que necesite las ventajas de un sistema de bajo nivel y esté preparado para ser cauteloso, tal vez no debería usar un lenguaje de bajo nivel.
etc ...
Todos estos son válidos y además no tener que hacer el esfuerzo de escribir getters y setters, etc. se considera una razón común para no haber seguido ese consejo. No es realmente sorprendente que muchos programadores hayan llegado a la conclusión de que los nulos son peligrosos y perezosos (¡una mala combinación!), Pero como muchos otros consejos "universales", no son tan universales como están redactados.
Las buenas personas que escribieron el idioma pensaron que serían útiles para alguien. Si comprende las objeciones y aún cree que son adecuadas para usted, nadie más lo sabrá mejor.
fuente
Java tiene una
Optional
clase con unifPresent
+ lambda explícito para acceder a cualquier valor de forma segura. Esto también se puede hacer en JS:Vea esta pregunta de StackOverflow .
Por cierto: muchos puristas encontrarán este uso dudoso, pero no lo creo. Existen características opcionales.
fuente
java.lang.Optional
sernull
también?Optional.of(null)
lanzaría una excepción,Optional.ofNullable(null)
daríaOptional.empty()
el valor no presente.Optional<Type> a = null
?Optional<Optional<Type>> a = Optional.empty();
;) - En otras palabras, en JS (y otros idiomas) tales cosas no serán factibles. Scala con valores predeterminados haría. Java tal vez con una enumeración. JS dudo:Optional<Type> a = Optional.empty();
.null
o vacío, a dos más algunos métodos adicionales en el objeto interpuesto. Eso es todo un logro.CAR Hoare llamó a nulos su "error de mil millones de dólares". Sin embargo, es importante tener en cuenta que pensó que la solución no era "no nulos en ninguna parte", sino "punteros no anulables como predeterminados y nulos solo cuando son semánticamente necesarios".
El problema con nulo en los idiomas que repiten su error es que no se puede decir que una función espera datos no anulables; Es fundamentalmente inexpresable en el idioma. Eso obliga a las funciones a incluir una gran cantidad de repeticiones extrañas inútiles, y olvidar una comprobación nula es una fuente común de errores.
Para hackear esa falta fundamental de expresividad, algunas comunidades (por ejemplo, la comunidad Scala) no usan nulo. En Scala, si desea un valor de tipo anulable
T
, toma un argumento de tipoOption[T]
, y si desea un valor no anulable, simplemente toma unT
. Si alguien pasa un valor nulo a su código, su código explotará. Sin embargo, se considera que el código de llamada es el código con errores.Option
Sin embargo, es un buen reemplazo para nulo, porque los patrones comunes de manejo de nulos se extraen en funciones de biblioteca como.map(f)
o.getOrElse(someDefault)
.fuente