¿Es mejor un nuevo campo booleano que una referencia nula cuando un valor puede estar significativamente ausente?

39

Por ejemplo, supongamos que tengo una clase Member, que tiene un lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

cuyo lastChangePasswordTime puede ser significativo ausente, porque algunos miembros nunca pueden cambiar sus contraseñas.

Pero de acuerdo con Si los valores nulos son malos, ¿qué se debe usar cuando un valor puede estar significativamente ausente? y https://softwareengineering.stackexchange.com/a/12836/248528 , no debería usar nulo para representar un valor significativamente ausente. Así que trato de agregar una bandera booleana:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Pero creo que es bastante obsoleto porque:

  1. Cuando isPasswordChanged es falso, lastChangePasswordTime debe ser nulo, y comprobar lastChangePasswordTime == null es casi idéntico a comprobar isPasswordChanged es falso, por lo que prefiero comprobar lastChangePasswordTime == null directamente

  2. Al cambiar la lógica aquí, puedo olvidar actualizar ambos campos.

Nota: cuando un usuario cambia las contraseñas, registraría el tiempo así:

this.lastChangePasswordTime=Date.now();

¿Es el campo booleano adicional mejor que una referencia nula aquí?

ocomfd
fuente
51
Esta pregunta es bastante específica del lenguaje, porque lo que constituye una buena solución depende en gran medida de lo que ofrezca su lenguaje de programación. Por ejemplo, en C ++ 17 o Scala, usaría std::optionalo Option. En otros idiomas, es posible que tenga que construir un mecanismo apropiado usted mismo, o en realidad puede recurrir a nullalgo similar porque es más idiomático.
Christian Hackl
16
¿Hay alguna razón por la que no desee que el último cambio de contraseña se establezca en el tiempo de creación de contraseña (la creación es un mod, después de todo)?
Kristian H
@ChristianHackl hmm, estoy de acuerdo en que hay diferentes soluciones "perfectas", pero no veo ningún lenguaje (principal) donde usar un booleano por separado sería una mejor idea en general que hacer comprobaciones nulas / nulas. Sin embargo, no estoy totalmente seguro de C / C ++, ya que no he estado activo allí durante bastante tiempo.
Frank Hopkins
@FrankHopkins: Un ejemplo sería lenguajes donde las variables pueden dejarse sin inicializar, por ejemplo, C o C ++. lastChangePasswordTimepuede ser un puntero no inicializado allí, y compararlo con cualquier cosa sería un comportamiento indefinido. No es una razón realmente convincente para no inicializar el puntero a NULL/ en su nullptrlugar, especialmente no en C ++ moderno (donde no usarías un puntero), pero ¿quién sabe? Otro ejemplo sería idiomas sin punteros, o con mal soporte para punteros, tal vez. (FORTRAN 77 viene a la mente ...)
Christian Hackl
Yo usaría una enumeración con 3 casos para esto :)
J. Doe

Respuestas:

72

No veo por qué, si tiene un valor significativamente ausente, nullno debe usarse si es deliberado y cuidadoso al respecto.

Si su objetivo es rodear el valor anulable para evitar referenciarlo accidentalmente, sugeriría crear el isPasswordChangedvalor como una función o propiedad que devuelva el resultado de una verificación nula, por ejemplo:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

En mi opinión, hacerlo de esta manera:

  • Ofrece una mejor legibilidad del código que una comprobación nula, lo que podría perder contexto.
  • Elimina la necesidad de tener que preocuparse por mantener el isPasswordChangedvalor que menciona.

La forma en que usted conserva los datos (presumiblemente en una base de datos) sería responsable de garantizar que se conserven los valores nulos.

TZHX
fuente
29
+1 para la apertura. El uso sin sentido de nulo generalmente se desaprueba solo en casos donde nulo es un resultado inesperado, es decir, donde no tendría sentido (para un consumidor). Si nulo es significativo, entonces no es un resultado inesperado.
Flater
28
Lo pondría un poco más fuerte ya que nunca tendré dos variables que mantengan el mismo estado. Fallará. Ocultar un valor nulo, suponiendo que el valor nulo tenga sentido dentro de la instancia, desde el exterior es algo muy bueno. En este caso, donde más tarde puede decidir que 1970-01-01 denota que la contraseña nunca se ha cambiado. Entonces la lógica del resto del programa no tiene que preocuparse.
Doblado
3
Puede olvidarse de llamar isPasswordChangedal igual que puede olvidarse de verificar si es nulo. No veo nada ganado aquí.
Gherman
9
@Gherman Si el consumidor no entiende el concepto de que nulo es significativo o que necesita verificar si el valor está presente, se pierde de cualquier manera, pero si se usa el método, está claro para todos los que leen el código por qué hay es una comprobación y que existe una posibilidad razonable de que el valor sea nulo / no presente. De lo contrario, no está claro si fue solo un desarrollador que agregó un cheque nulo simplemente "porque no" o si es parte del concepto. Claro, uno puede averiguarlo, pero de una manera tiene la información directamente y la otra necesita investigar la implementación.
Frank Hopkins
2
@FrankHopkins: Buen punto, pero si se usa la concurrencia, incluso la inspección de una sola variable puede requerir bloqueo.
Christian Hackl
63

nullsno son malvados Usarlos sin pensar es. Este es un caso donde nulles exactamente la respuesta correcta: no hay fecha.

Tenga en cuenta que su solución crea más problemas. ¿Cuál es el significado de la fecha establecida en algo, pero isPasswordChangedes falso? Acaba de crear un caso de información conflictiva que necesita capturar y tratar especialmente, mientras que un nullvalor tiene un significado claramente definido y no ambiguo y no puede entrar en conflicto con otra información.

Entonces no, su solución no es mejor. Permitir un nullvalor es el enfoque correcto aquí. Las personas que afirman que nullsiempre es malo, sin importar el contexto, no entienden por qué nullexiste.

Tom
fuente
17
Históricamente, nullexiste porque Tony Hoare cometió el error de mil millones de dólares en 1965. Las personas que afirman que nulles "malvado" están simplificando demasiado el tema, por supuesto, pero es bueno que los lenguajes modernos o los estilos de programación se estén alejando null, reemplazando con tipos de opciones dedicados.
Christian Hackl
9
@ChristianHackl: solo por interés histórico, ¿cómo dijo Hoare cuál sería la mejor alternativa práctica (sin cambiar a un paradigma completamente nuevo)?
AnoE
55
@AnoE: Pregunta interesante. No sé qué dijo Hoare sobre eso, pero la programación libre de nulos es a menudo una realidad en estos días en lenguajes de programación modernos y bien diseñados.
Christian Hackl
10
@ Tom Wat? En lenguajes como C # y Java, el DateTimecampo es un puntero. ¡Todo el concepto de nullsolo tiene sentido para los punteros!
Todd Sewell
16
@Tom: los punteros nulos solo son peligrosos en lenguajes e implementaciones que les permiten indexarse ​​y desreferenciarse ciegamente, con cualquier hilaridad que pueda resultar. En un lenguaje que atrapa los intentos de indexar o desreferenciar un puntero nulo, a menudo será menos peligroso que un puntero a un objeto ficticio. Hay muchas situaciones en las que tener un programa que termine de manera anormal puede ser preferible a que produzca números sin sentido, y en los sistemas que los atrapan, los punteros nulos son la receta correcta para eso.
supercat
29

Dependiendo de su lenguaje de programación, puede haber buenas alternativas, como un optionaltipo de datos. En C ++ (17), esto sería std :: opcional . Puede estar ausente o tener un valor arbitrario del tipo de datos subyacente.

dasmy
fuente
8
También hay Optionalen Java; el significado de un Optionalser nulo depende de usted ...
Matthieu M.
1
Vine aquí para conectarse con Opcional también. Codificaría esto como un tipo en un idioma que los tuviera.
Zipp
2
@FrankHopkins Es una pena que nada en Java te impida escribir Optional<Date> lastPasswordChangeTime = null;, pero no me rendiría solo por eso. En cambio, inculcaría una regla de equipo muy firme, que nunca se permite asignar valores opcionales null. Opcional le compra demasiadas características agradables para renunciar a eso fácilmente. Puede asignar fácilmente valores predeterminados ( orElseo con una evaluación perezosa:) orElseGet, arrojar errores ( orElseThrow), transformar valores de forma nula y segura ( map, flatmap), etc.
Alexander - Restablecer Mónica
55
@FrankHopkins Checking isPresentes casi siempre un olor a código, en mi experiencia, y una señal bastante confiable de que los desarrolladores que usan estas opciones están "perdiendo el punto". De hecho, básicamente no ofrece ningún beneficio ref == null, pero tampoco es algo que deba usar con frecuencia. Incluso si el equipo es inestable, una herramienta de análisis estático puede establecer la ley, como un linter o FindBugs , que pueden detectar la mayoría de los casos.
Alexander - Restablece a Mónica
55
@FrankHopkins: Puede ser que la comunidad Java solo necesite un pequeño cambio de paradigma, lo que aún puede llevar muchos años. Si observa Scala, por ejemplo, es compatible nullporque el lenguaje es 100% compatible con Java, pero nadie lo usa, y Option[T]está en todas partes, con un excelente soporte de programación funcional como map, flatMapcoincidencia de patrones, etc. lejos, mucho más allá de los == nullcontroles. Tiene razón en que, en teoría, un tipo de opción suena como poco más que un puntero glorificado, pero la realidad demuestra que ese argumento es incorrecto.
Christian Hackl
11

Utilizando correctamente nulo

Hay diferentes formas de usar null. La forma más común y semánticamente correcta es usarlo cuando puede o no tener un solo valor. En este caso, un valor es igual nullo es algo significativo como un registro de la base de datos o algo así.

En estas situaciones, generalmente lo usa así (en pseudocódigo):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

Problema

Y tiene un gran problema. ¡El problema es que para cuando invocas doSomethingUsefulel valor puede no haber sido verificado null! Si no fuera así, el programa probablemente se bloqueará. Y es posible que el usuario ni siquiera vea ningún mensaje de error bueno, quedando con algo como "error horrible: ¡valor deseado pero nulo!" (después de la actualización: aunque puede haber incluso menos errores informativos como Segmentation fault. Core dumped., o peor aún, ningún error y manipulación incorrecta en nulo en algunos casos)

Olvidar escribir cheques nully manejar nullsituaciones es un error extremadamente común . Es por eso que Tony Hoare, quien inventó, nulldijo en una conferencia de software llamada QCon London en 2009 que cometió el error de mil millones de dólares en 1965: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Error-Tony-Hoare

Evitando el problema

Algunas tecnologías e idiomas hacen que sea nullimposible olvidar la comprobación de diferentes maneras, reduciendo la cantidad de errores.

Por ejemplo, Haskell tiene la Maybemónada en lugar de nulos. Supongamos que DatabaseRecordes un tipo definido por el usuario. En Haskell, un valor de tipo Maybe DatabaseRecordpuede ser igual Just <somevalue>o igual a Nothing. Luego puede usarlo de diferentes maneras, pero no importa cómo lo use, no puede aplicar alguna operación Nothingsin saberlo.

Por ejemplo, esta función llamada zeroAsDefaultdevuelve xpara Just xy 0para Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl dice que C ++ 17 y Scala tienen sus propios modos. Por lo tanto, puede intentar averiguar si su idioma tiene algo así y usarlo.

Los nulos todavía se usan ampliamente

Si no tienes nada mejor, entonces usar nullestá bien. Solo mantente atento. Las declaraciones de tipo en funciones te ayudarán de alguna manera.

Además, eso puede sonar no muy progresivo, pero debe verificar si sus colegas quieren usar nullo algo más. Pueden ser conservadores y es posible que no quieran usar nuevas estructuras de datos por algunos motivos. Por ejemplo, admitir versiones anteriores de un idioma. Tales cosas deben declararse en los estándares de codificación del proyecto y debatirse adecuadamente con el equipo.

En su propuesta

Sugiere usar un campo booleano separado. Pero debe verificarlo de todos modos y aún así puede olvidar verificarlo. Entonces no hay nada ganado aquí. Si incluso puede olvidar algo más, como actualizar ambos valores cada vez, entonces es aún peor. Si nullno se resuelve el problema de olvidar verificar, entonces no tiene sentido. Evitar nulles difícil y no debe hacerlo de tal manera que lo empeore.

Cómo no usar nulo

Finalmente, hay formas comunes de usar nullincorrectamente. Una de esas formas es usarlo en lugar de estructuras de datos vacías, como matrices y cadenas. ¡Una matriz vacía es una matriz adecuada como cualquier otra! Casi siempre es importante y útil para las estructuras de datos, que pueden ajustarse a múltiples valores, para poder estar vacías, es decir, tiene una longitud 0.

Desde el punto de vista de álgebra, una cadena vacía para cadenas es muy similar a 0 para números, es decir, identidad:

a+0=a
concat(str, '')=str

La cadena vacía permite que las cadenas en general se conviertan en un monoide: https://en.wikipedia.org/wiki/Monoid Si no lo obtiene, no es tan importante para usted.

Ahora veamos por qué es importante para la programación con este ejemplo:

for (element in array) {
  doSomething(element);
}

Si pasamos una matriz vacía aquí, el código funcionará bien. Simplemente no hará nada. Sin embargo, si pasamos un nullaquí, es probable que tengamos un error con un error como "no se puede recorrer nulo, lo siento". Podríamos envolverlo, ifpero eso es menos limpio y, de nuevo, es posible que se olvide de verificarlo

Cómo manejar nulo

Lo que doSomethingAboutIt()debería estar haciendo y especialmente si debería arrojar una excepción es otro tema complicado. En resumen, depende de si nullfue un valor de entrada aceptable para una tarea determinada y de lo que se espera en respuesta. Las excepciones son para eventos que no se esperaban. No profundizaré más en ese tema. Esta respuesta ya es larga.

Gherman
fuente
55
En realidad, horrible error: wanted value but got null!es mucho mejor que el más típico Segmentation fault. Core dumped....
Toby Speight
2
@TobySpeight True, pero preferiría algo que se encuentre dentro de los términos del usuario como Error: the product you want to buy is out of stock.
Gherman
Claro, solo estaba observando que podría ser (y a menudo es) aún peor . Estoy completamente de acuerdo en lo que sería mejor! Y su respuesta es más o menos lo que hubiera dicho a esta pregunta: +1.
Toby Speight
2
@Gherman: Pasar nulldonde nullno está permitido es un error de programación, es decir, un error en el código. Un producto que está agotado es parte de la lógica comercial ordinaria y no un error. No puede ni debe intentar traducir la detección de un error a un mensaje normal en la interfaz de usuario. Bloquear de inmediato y no ejecutar más código es el curso de acción preferido, especialmente si es una aplicación importante (por ejemplo, una que realiza transacciones financieras reales).
Christian Hackl
1
@EricDuminil Queremos eliminar (o reducir) la posibilidad de cometer un error, no eliminarlo nullpor completo, incluso desde el fondo.
Gherman
4

Además de todas las muy buenas respuestas dadas anteriormente, agregaría que cada vez que tenga la tentación de dejar un campo nulo, piense detenidamente si podría ser un tipo de lista. Un tipo anulable es equivalentemente una lista de 0 o 1 elementos, y a menudo esto puede generalizarse a una lista de N elementos. Específicamente en este caso, es posible que desee considerar lastChangePasswordTimeser una lista de passwordChangeTimes.

Joel
fuente
Ese es un excelente punto. Lo mismo es a menudo cierto para los tipos de opciones; Una opción puede verse como un caso especial de una secuencia.
Christian Hackl
2

Pregúntese esto: ¿qué comportamiento requiere el campo lastChangePasswordTime?

Si necesita ese campo para un método IsPasswordExpired () para determinar si se debe solicitar a un Miembro que cambie su contraseña cada cierto tiempo, establecería el campo en el momento en que se creó inicialmente. La implementación IsPasswordExpired () es la misma para miembros nuevos y existentes.

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Si tiene un requisito por separado de que los miembros recién creados tengan que actualizar su contraseña, agregaría un campo booleano separado llamado passwordShouldBeChanged y lo establecería en verdadero después de la creación. Luego, cambiaría la funcionalidad del método IsPasswordExpired () para incluir una verificación para ese campo (y cambiaría el nombre del método a ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Haz explícitas tus intenciones en el código.

Rik D
fuente
2

Primero, que los nulos sean malvados es un dogma y, como es habitual con el dogma, funciona mejor como guía y no como prueba de aprobación / no aprobación.

En segundo lugar, puede redefinir su situación de manera que tenga sentido que el valor nunca pueda ser nulo. InititialPasswordChanged es un valor booleano inicialmente establecido en falso, PasswordSetTime es la fecha y hora en que se configuró la contraseña actual.

Tenga en cuenta que si bien esto tiene un costo leve, ahora SIEMPRE puede calcular cuánto tiempo ha pasado desde la última vez que se configuró una contraseña.

jmoreno
fuente
1

Ambos son 'seguros / sanos / correctos' si la persona que llama verifica antes de usar. El problema es qué sucede si la persona que llama no marca. ¿Qué es mejor, algún sabor de error nulo o el uso de un valor no válido?

No hay una única respuesta correcta. Depende de lo que te preocupe.

Si los bloqueos son realmente malos pero la respuesta no es crítica o tiene un valor predeterminado aceptado, entonces quizás sea mejor usar un booleano como indicador. Si usar la respuesta incorrecta es un problema peor que estrellarse, entonces usar nulo es mejor.

Para la mayoría de los casos "típicos", la falla más rápida y obligar a las personas que llaman a verificar es la forma más rápida de obtener un código correcto y, por lo tanto, creo que nulo debería ser la opción predeterminada. Sin embargo, no confiaría demasiado en los evangelistas "X es la raíz de todo mal"; normalmente no han anticipado todos los casos de uso.

Alguien
fuente