remarks example cref c# java c++ design-by-contract

example - remarks c#



¿Cuántas comprobaciones nulas son suficientes? (18)

Al principio, parecía una pregunta extraña: null controles null son geniales y una herramienta valiosa. Comprobar que los new resultados sean null definitivamente es una tontería. Voy a ignorar el hecho de que hay idiomas que lo permiten. Estoy seguro de que hay razones válidas, pero realmente no creo que pueda manejar la vida en esa realidad :) Bromas aparte, parece que al menos deberías especificar que lo new debería volverse null cuando no haya suficiente memoria

De todos modos, la comprobación de null en su caso conduce a un código más limpio. Me atrevería a decir que nunca asignar valores predeterminados a los parámetros de función es el siguiente paso lógico. Para ir más allá, devolver matrices vacías, etc., donde sea apropiado, conduce a un código aún más limpio. Es bueno no tener que preocuparse por obtener null , excepto cuando sean lógicamente significativos. Los nulos como valores de error se evitan mejor.

Usar aseveraciones es una gran idea. Especialmente si te da la opción de apagarlos en tiempo de ejecución. Además, es un estilo más explícitamente contractual :)

¿Cuáles son algunas pautas para cuando no es necesario verificar un nulo?

Gran parte del código heredado en el que he estado trabajando últimamente tiene null-checks ad nauseam. Verificaciones nulas en funciones triviales, comprobaciones nulas en las llamadas API que indican retornos no nulos, etc. En algunos casos, las comprobaciones nulas son razonables, pero en muchos lugares un nulo no es una expectativa razonable.

He escuchado una serie de argumentos que van desde "No se puede confiar en otro código" hasta "SIEMPRE programar a la defensiva" hasta "Hasta que el lenguaje me garantice un valor no nulo, siempre voy a verificar". Ciertamente, estoy de acuerdo con muchos de esos principios hasta cierto punto, pero he encontrado que la verificación nula excesiva causa otros problemas que generalmente violan esos principios. ¿Vale la pena la tenaz comprobación nula?

Con frecuencia, he observado que los códigos con exceso de verificación nula son en realidad de peor calidad, no de mayor calidad. Gran parte del código parece estar tan enfocado en las comprobaciones nulas que el desarrollador ha perdido de vista otras cualidades importantes, como la legibilidad, la corrección o el manejo de excepciones. En particular, veo una gran cantidad de código ignorar la excepción std :: bad_alloc, pero hacer una verificación nula en una new .

En C ++, entiendo esto hasta cierto punto debido al comportamiento impredecible de eliminar referencias a un puntero nulo; la desreferencia nula se maneja con mayor gracia en Java, C #, Python, etc. ¿Acabo de ver ejemplos pobres de vigilancia nula vigilante o hay realmente algo en esto?

Esta pregunta está destinada a ser independiente del lenguaje, aunque estoy interesado principalmente en C ++, Java y C #.

Algunos ejemplos de verificación nula que he visto que parecen ser excesivos incluyen los siguientes:

Este ejemplo parece estar contabilizando compiladores no estándar, ya que la especificación de C ++ dice que una nueva falla arroja una excepción. A menos que esté apoyando explícitamente compiladores no conformes, ¿tiene sentido esto? ¿Tiene esto sentido en un lenguaje administrado como Java o C # (o incluso C ++ / CLR)?

try { MyObject* obj = new MyObject(); if(obj!=NULL) { //do something } else { //??? most code I see has log-it and move on //or it repeats what''s in the exception handler } } catch(std::bad_alloc) { //Do something? normally--this code is wrong as it allocates //more memory and will likely fail, such as writing to a log file. }

Otro ejemplo es cuando se trabaja en código interno. Particularmente, si se trata de un equipo pequeño que puede definir sus propias prácticas de desarrollo, esto parece innecesario. En algunos proyectos o códigos heredados, confiar en la documentación puede no ser razonable ... pero para el nuevo código que usted o su equipo controlan, ¿es esto realmente necesario?

Si un método, que puede ver y puede actualizar (o puede gritarle al desarrollador que es responsable) tiene un contrato, ¿todavía es necesario verificar los nulos?

//X is non-negative. //Returns an object or throws exception. MyObject* create(int x) { if(x<0) throw; return new MyObject(); } try { MyObject* x = create(unknownVar); if(x!=null) { //is this null check really necessary? } } catch { //do something }

Al desarrollar una función privada o de otro tipo interna, ¿es realmente necesario manejar explícitamente un valor nulo cuando el contrato solo requiere valores no nulos? ¿Por qué sería preferible una verificación nula a una afirmación?

(obviamente, en su API pública, los chequeos nulos son vitales ya que se considera de mala educación gritar a sus usuarios por usar incorrectamente la API)

//Internal use only--non-public, not part of public API //input must be non-null. //returns non-negative value, or -1 if failed int ParseType(String input) { if(input==null) return -1; //do something magic return value; }

Comparado con:

//Internal use only--non-public, not part of public API //input must be non-null. //returns non-negative value int ParseType(String input) { assert(input!=null : "Input must be non-null."); //do something magic return value; }


Cuando puede especificar qué compilador se está utilizando, para las funciones del sistema, como "nuevo", la comprobación de nulo es un error en el código. Significa que va a duplicar el código de manejo de errores. El código duplicado suele ser una fuente de errores porque a menudo uno se cambia y el otro no. Si no puede especificar las versiones del compilador o el compilador, debería estar más a la defensiva.

En cuanto a las funciones internas, debe especificar el contrato y asegurarse de que el contrato se haga cumplir a través de pruebas unitarias. Tuvimos un problema en nuestro código hace un tiempo en el que lanzamos una excepción o devolvimos el valor nulo en caso de que faltara un objeto en nuestra base de datos. Esto simplemente hizo las cosas confusas para la persona que llama de la API, así que lo revisamos y lo hicimos consistente a lo largo de todo el código base y quitamos los cheques duplicados.

Lo importante (en mi humilde opinión) es no tener una lógica de error duplicada donde nunca se invocará una rama. Si nunca puede invocar el código, no puede probarlo, y nunca sabrá si está roto o no.


Depende de la situación. El resto de mi respuesta asume C ++.

  • Nunca pruebo el valor de retorno de nuevo ya que todas las implementaciones que uso arrojan bad_alloc en caso de falla. Si veo una prueba heredada para un nuevo retorno nulo en cualquier código en el que estoy trabajando, lo recorté y no me molesto en reemplazarlo con nada.
  • A menos que las normas de codificación de mente pequeña lo prohíban, afirmo precondiciones documentadas. El código roto que infringe un contrato publicado debe fallar de manera inmediata y dramática.
  • Si el nulo surge de una falla en el tiempo de ejecución que no se debe a un código roto, arrojo. fopen failure y malloc failure (aunque rara vez los uso en C ++) entrarían en esta categoría.
  • No intento recuperarme de una falla de asignación. Bad_alloc queda atrapado en main ().
  • Si la prueba nula es para un objeto que es colaborador de mi clase, reescribo el código para tomarlo como referencia.
  • Si el colaborador realmente no existe, utilizo el patrón de diseño Objeto nulo para crear un marcador de posición que falle en formas bien definidas.

El código de nivel inferior debe verificar el uso del código de nivel superior. Por lo general, esto significa verificar argumentos, pero puede significar verificar valores devueltos desde llamadas ascendentes. Los argumentos de llamada ascendente no necesitan ser verificados.

El objetivo es detectar errores de manera inmediata y obvia, así como documentar el contrato en código que no mienta.


Es ampliamente conocido que hay personas orientadas a los procedimientos (se centran en hacer las cosas de la manera correcta) y personas orientadas a los resultados (obtienen la respuesta correcta). La mayoría de nosotros miente en algún lugar en el medio. Parece que has encontrado un valor atípico para orientado a procedimientos. Estas personas dirían "todo es posible a menos que entiendas las cosas perfectamente, así que prepárate para cualquier cosa". Para ellos, lo que ves se hace correctamente. Para ellos, si lo cambias, se preocuparán porque los patos no están alineados.

Cuando trabajo en el código de otra persona, trato de asegurarme de que sé dos cosas.
1. Qué pretendía el programador
2. ¿Por qué escribieron el código de la manera en que lo hicieron?

Para el seguimiento de los programadores tipo A, tal vez esto ayude.

Entonces, "¿cuánto es suficiente?" Termina siendo una cuestión social tanto como una cuestión técnica; no hay una forma acordada de medirlo.

(Me vuelve loco también)


La verificación NULL en general es mala ya que agrega un pequeño token negativo a la capacidad de prueba del código. Con los controles NULL en todos lados, no puedes usar la técnica "pasar nulo" y te golpeará cuando pruebes la unidad. Es mejor tener una unidad de prueba para el método que la verificación nula.

Consulte la presentación decente sobre ese tema y las pruebas unitarias en general de Misko Hevery en http://www.youtube.com/watch?v=wEhu57pih5w&feature=channel


Las versiones anteriores de Microsoft C ++ (y probablemente otras) no arrojaron una excepción para las asignaciones fallidas a través de nuevas, pero devolvieron NULL. El código que tenía que ejecutarse tanto en versiones estándar como antiguas tendría la verificación redundante que usted señala en su primer ejemplo.

Sería más claro hacer que todas las asignaciones fallidas sigan la misma ruta de código:

if(obj==NULL) throw std::bad_alloc();


Mi primer problema con esto, es que conduce a un código que está plagado de verificaciones nulas y me gusta. Duele la legibilidad, e incluso diría que daña la capacidad de mantenimiento porque es fácil olvidar un cheque nulo si estás escribiendo un código donde una referencia determinada nunca debería ser nula. Y usted simplemente sabe que los controles nulos se perderán en algunos lugares. Lo que realmente hace que la depuración sea más difícil de lo necesario. Si la excepción original no hubiera sido capturada y reemplazada con un valor de retorno defectuoso, entonces habríamos obtenido un valioso objeto de excepción con una pila de información informativa. ¿Qué le da un cheque nulo faltante? Una NullReferenceException en un fragmento de código que te hace ir: wtf? esta referencia nunca debe ser nula!

Entonces, debe comenzar a averiguar cómo se llamó el código y por qué la referencia podría ser nula. Esto puede llevar mucho tiempo y realmente perjudica la eficacia de sus esfuerzos de depuración. Eventualmente, descubrirás el verdadero problema, pero lo más probable es que esté oculto muy profundamente y hayas pasado mucho más tiempo buscándolo de lo que deberías.

Otro problema con las comprobaciones nulas en todas partes es que algunos desarrolladores realmente no se toman el tiempo para pensar correctamente sobre el problema real cuando obtienen una NullReferenceException. De hecho, he visto bastantes desarrolladores simplemente agregar un cheque nulo por encima del código donde se produjo la excepción NullReferenceException. ¡Genial, la excepción ya no ocurre! ¡Viva! ¡Podemos irnos a casa ahora! Umm ... ¿qué tal ''no, no puedes y te mereces un codazo en la cara''? El error real ya no podría causar una excepción, pero ahora es probable que tenga un comportamiento faltante o defectuoso ... ¡y no hay excepción! Lo cual es aún más doloroso y toma aún más tiempo para depurar.


No creo que sea un código malo . Una buena cantidad de llamadas API de Windows / Linux devuelven NULL en caso de falla de algún tipo. Entonces, por supuesto, verifico si falla de la manera que especifica la API. Normalmente termino pasando flujo de control a un módulo de error de alguna manera en lugar de duplicar el código de manejo de errores.


Parte de esto depende de cómo se usa el código, si es un método disponible solo dentro de un proyecto frente a una API pública, por ejemplo. La comprobación de errores API requiere algo más fuerte que una aserción.

Así que, aunque esto está bien dentro de un proyecto donde es compatible con pruebas de unidades y cosas así:

internal void DoThis(Something thing) { Debug.Assert(thing != null, "Arg [thing] cannot be null."); //... }

en un método donde no tienes control sobre quién lo llama, algo como esto puede ser mejor:

public void DoThis(Something thing) { if (thing == null) { throw new ArgumentException("Arg [thing] cannot be null."); } //... }


Personalmente, creo que las pruebas nulas no son necesarias en la gran mayoría de los casos. Si falla un nuevo programa o falla Malloc, usted tiene problemas mayores y la posibilidad de recuperación es casi nula en los casos en que no está escribiendo un comprobador de memoria. También las pruebas nulas esconden errores en las fases de desarrollo ya que las cláusulas "nulas" frecuentemente están vacías y no hacen nada.


Primero tenga en cuenta que este es un caso especial de verificación de contratos: está escribiendo código que no hace nada más que validar en tiempo de ejecución que se cumple un contrato documentado. El fracaso significa que algún código en alguna parte es defectuoso.

Siempre tengo dudas sobre la implementación de casos especiales de un concepto más útil en general. La verificación de contratos es útil porque detecta errores de programación la primera vez que cruzan un límite API. ¿Qué tiene de especial el valor nulo, lo que significa que es la única parte del contrato que desea verificar? Todavía,

En el tema de validación de entrada:

null es especial en Java: muchas API de Java se escriben de forma tal que null es el único valor no válido que incluso es posible pasar a una llamada de método determinada. En tales casos, una comprobación nula "valida por completo" la entrada, por lo que se aplica el argumento completo a favor de la verificación de contratos.

En C ++, por otro lado, NULL es solo uno de casi 2 ^ 32 (2 ^ 64 en arquitecturas nuevas) valores no válidos que un parámetro de puntero podría tomar, ya que casi todas las direcciones no son de objetos del tipo correcto. No puede "validar completamente" su entrada a menos que tenga una lista en alguna parte de todos los objetos de ese tipo.

Entonces surge la pregunta: ¿NULL es una entrada inválida suficientemente común para obtener un tratamiento especial que (foo *)(-1) no obtiene?

A diferencia de Java, los campos no se inicializan automáticamente en NULL, por lo que un valor no inicializado de basura es tan plausible como NULL. Pero a veces los objetos C ++ tienen miembros de puntero que están explícitamente NULL-inited, lo que significa "todavía no tengo uno". Si la persona que llama hace esto, entonces hay una clase significativa de errores de programación que pueden diagnosticarse con una verificación NULL. Una excepción puede ser más fácil para ellos depurar que un error de página en una biblioteca para la que no tienen la fuente. Entonces, si no te importa el código inflado, podría ser útil. Pero es tu interlocutor el que deberías estar pensando, no tú mismo: no se trata de una codificación defensiva, porque solo ''defiende'' contra NULL, no contra (foo *) (- 1).

Si NULL no es una entrada válida, podría considerar tomar el parámetro por referencia en lugar de puntero, pero muchos estilos de codificación no aprueban los parámetros de referencia no const. Y si la persona que llama le pasa * fooptr, donde fooptr es NULL, entonces de todos modos no le ha servido a nadie. Lo que estás tratando de hacer es exprimir un poco más de documentación en la firma de la función, con la esperanza de que quien llama piense más "hmm, ¿podría ser nulo aquí?" cuando tienen que desreferencia explícitamente, que si simplemente se lo pasan como un puntero. Solo va tan lejos, pero en la medida de lo posible, podría ser útil.

No sé C #, pero entiendo que es como Java en que las referencias tienen valores válidos (en código seguro, al menos), pero a diferencia de Java, no todos los tipos tienen un valor NULL. Así que supongo que los controles nulos rara vez valen la pena: si está en un código seguro, entonces no use un tipo que admite nulos, a menos que null sea una entrada válida, y si está en un código inseguro, entonces se aplica el mismo razonamiento en C ++.

En el tema de validación de salida:

Surge un problema similar: en Java puede "validar completamente" la salida conociendo su tipo, y que el valor no es nulo. En C ++, no se puede "validar por completo" el resultado con una verificación NULL; por lo que sabemos, la función devolvió un puntero a un objeto en su propia pila que acaba de desenrollarse. Pero si NULL es un retorno no válido común debido a los constructos utilizados normalmente por el autor del código de llamada, la comprobación lo ayudará.

En todos los casos:

Use aserciones en lugar de "código real" para verificar los contratos cuando sea posible; una vez que su aplicación esté funcionando, probablemente no desee que el código inflado de cada persona que llama verifique todas sus entradas y que cada persona que llama verifique sus valores de retorno.

En el caso de escribir código que es portable para implementaciones C ++ no estándar, entonces, en lugar del código en la pregunta que busca nulo y también capta la excepción, probablemente tenga una función como esta:

template<typename T> static inline void nullcheck(T *ptr) { #if PLATFORM_TRAITS_NEW_RETURNS_NULL if (ptr == NULL) throw std::bad_alloc(); #endif }

Luego, como parte de la lista de cosas que hace al migrar a un sistema nuevo, define PLATFORM_TRAITS_NEW_RETURNS_NULL (y tal vez algunos PLATFORM_TRAITS) correctamente. Obviamente puedes escribir un encabezado que hace esto para todos los compiladores que conoces. Si alguien toma su código y lo compila en una implementación C ++ no estándar de la que no sabe nada, están fundamentalmente solos por razones más importantes que esta, por lo que tendrán que hacerlo ellos mismos.


Si escribe el código y su contrato, usted es responsable de usarlo en términos de su contrato y asegurarse de que el contrato sea correcto. Si dice "devuelve un x no nulo", entonces la persona que llama no debe verificar nulo. Si se produce una excepción de puntero nulo con esa referencia / puntero, su contrato es incorrecto.

La verificación nula solo debe llegar al extremo cuando se utiliza una biblioteca que no es de confianza, o no tiene un contrato adecuado. Si es el código de su equipo de desarrollo, enfatice que los contratos no deben romperse, y localice a la persona que usa el contrato incorrectamente cuando ocurran errores.


Si para comprobar nulo o no depende en gran medida de las circunstancias.

Por ejemplo, en nuestra tienda verificamos los parámetros de los métodos que creamos para null dentro del método. La razón simple es que, como programador original, tengo una buena idea de exactamente qué debería hacer el método. Entiendo el contexto incluso si la documentación y los requisitos son incompletos o no son satisfactorios. Un programador posterior encargado de mantenimiento puede no entender el contexto y puede suponer, erróneamente, que pasar nulo es inofensivo. Si sé que nulo sería dañino y puedo anticipar que alguien puede pasar el nulo, debería tomar el simple paso de asegurarme de que el método reaccione de una manera elegante.

public MyObject MyMethod(object foo) { if (foo == null) { throw new ArgumentNullException("foo"); } // do whatever if foo was non-null }


Si recibo un puntero que no está garantizado por el lenguaje para que no sea nulo, y voy a des-referenciarlo de una manera que null me rompa, o desmayo pongo mi función donde dije que no produciría NULLs, verifica NULL.

No se trata solo de NULL, una función debe verificar las condiciones previas y posteriores si es posible.

No importa en absoluto si un contrato de la función que me dio el puntero dice que nunca producirá nulos. Todos cometemos errores. Existe una buena regla de que un programa fallará temprano y con frecuencia, por lo que en lugar de pasar el error a otro módulo y hacer que falle, fallaré en su lugar. Hace que las cosas sean mucho más fáciles de depurar cuando se prueban. También en sistemas críticos hace que sea más fácil mantener el sistema en forma.

Además, si una excepción escapa a main, es posible que la pila no se despliegue, impidiendo que se ejecuten los destructores (consulte C ++ estándar en terminate ()). Que puede ser serio. Así que dejar bad_alloc sin marcar puede ser más peligroso de lo que parece.

Fail with assert vs. fail con un error de tiempo de ejecución es un tema bastante diferente.

La comprobación de NULL después de new () si el comportamiento new () estándar no se ha modificado para devolver NULL en lugar de throwing parece obsoleto.

Hay otro problema, que es que incluso si malloc devolvió un puntero válido, aún no significa que haya asignado memoria y pueda usarla. Pero esa es otra historia.


Solo compruebo NULL cuando sé qué hacer cuando veo NULL. "Saber qué hacer" aquí significa "saber cómo evitar un bloqueo" o "saber qué decirle al usuario además de la ubicación del bloqueo". Por ejemplo, si malloc () devuelve NULL, normalmente no tengo más opción que abortar el programa. Por otro lado, si fopen () devuelve NULL, puedo informar al usuario el nombre del archivo que no se pudo abrir y puede ser erróneo. Y si find () devuelve end (), generalmente sé cómo continuar sin fallar.


Una cosa para recordar es que su código que usted escribe hoy, aunque puede ser un equipo pequeño y puede tener una buena documentación, se convertirá en un código heredado que alguien más tendrá que mantener. Yo uso las siguientes reglas:

  1. Si estoy escribiendo una API pública que estará expuesta a otros, haré controles nulos en todos los parámetros de referencia.

  2. Si estoy escribiendo un componente interno en mi aplicación, escribo verificaciones nulas cuando necesito hacer algo especial cuando existe un nulo, o cuando quiero dejarlo muy claro. De lo contrario, no me importa obtener la excepción de referencia nula ya que también está bastante claro lo que está sucediendo.

  3. Cuando trabajo con datos de retorno de marcos de trabajo de otras personas, solo verifico nulo cuando es posible y válido devolver un nulo. Si su contrato dice que no devuelve nulos, no haré el cheque.


Yo diría que depende un poco de tu idioma, pero utilizo Resharper con C # y básicamente se va de su camino para decirme "esta referencia podría ser nula", en cuyo caso agrego un cheque, si me dice "esto siempre será verdadero "para" si (null! = oMyThing && ....) "luego lo escucho y no pruebo nulo.