En Objective-C, ¿por qué debería verificar si self = [super init] no es nulo?

165

Tengo una pregunta general sobre cómo escribir métodos init en Objective-C.

Veo en todas partes (código de Apple, libros, código fuente abierto, etc.) que un método init debe verificar si self = [super init] no es nulo antes de continuar con la inicialización.

La plantilla predeterminada de Apple para un método init es:

- (id) init
{
    self = [super init];

    if (self != nil)
    {
        // your code here
    }

    return self;
}

¿Por qué?

Quiero decir, ¿cuándo va a volver init alguna vez? Si llamé a init en NSObject y obtuve nada, entonces algo debe estar realmente jodido, ¿verdad? Y en ese caso, es mejor que ni siquiera escribas un programa ...

¿Es realmente tan común que el método init de una clase pueda devolver nulo? Si es así, ¿en qué caso y por qué?

Jasarien
fuente
1
Wil Shipley publicó un artículo relacionado con esto hace un tiempo. [self = [stupid init];] ( wilshipley.com/blog/2005/07/self-stupid-init.html ) Lea también los comentarios, algunas cosas buenas.
Ryan Townshend
66
Podrías preguntarle a Wil Shipley o Mike Ash o Matt Gallagher . De cualquier manera, es algo de un tema debatido. Pero generalmente es bueno seguir con los modismos de Apple ... después de todo, son sus Frameworks.
jbrennan
1
Parece que Wil estaba haciendo un caso más por no reasignarse ciegamente durante init, sabiendo que [super init] puede no devolver el receptor.
Jasarien
3
Wil ha cambiado de opinión desde que esa publicación se hizo originalmente.
bbum
55
Había visto esta pregunta hace un tiempo y la encontré nuevamente. Perfecto. +1
Dan Rosenstark

Respuestas:

53

Por ejemplo:

[[NSData alloc] initWithContentsOfFile:@"this/path/doesn't/exist/"];
[[NSImage alloc] initWithContentsOfFile:@"unsupportedFormat.sjt"];
[NSImage imageNamed:@"AnImageThatIsntInTheImageCache"];

... y así. (Nota: NSData podría generar una excepción si el archivo no existe). Hay bastantes áreas en las que el comportamiento esperado es devolver nulo cuando se produce un problema, y ​​debido a esto es una práctica estándar verificar nulo casi todo el tiempo, por razones de coherencia.

iKenndac
fuente
10
Sí, pero esto no está DENTRO del método init de la clase respectiva. NSData hereda de NSObject. ¿NSData verifica si [super init] devuelve nulo? Eso es lo que estoy preguntando aquí. Lo siento si no estaba claro ...
Jasarien
9
Si subclase esas clases, habría una posibilidad bastante real de que [superiniciado] regrese nulo. No todas las clases son subclases directas de NSObject. Nunca hay ninguna garantía de que no devolverá nada. Es solo una práctica de codificación defensiva que generalmente se recomienda.
Chuck
77
Dudo que NSObject init pueda devolver nil en cualquier caso, nunca. Si no tiene memoria, el alloc fallará, pero si tiene éxito, dudo que init pueda fallar; NSObject ni siquiera tiene ninguna variable de instancia, excepto Class. En GNUStep, se implementa simplemente como "return self", y el desmontaje en Mac se ve igual. Todo esto es, por supuesto, irrelevante: solo sigue el idioma estándar y no tendrás que preocuparte de si puede o no.
Peter N Lewis
9
No estoy empeñado en no seguir las mejores prácticas. Sin embargo, me gustaría saber por qué son las mejores prácticas en primer lugar. Es como que te digan que saltes de una torre. No solo continúas y lo haces a menos que sepas por qué. ¿Hay una almohada grande y suave para aterrizar en la parte inferior, con una recompensa en efectivo masiva? Si supiera que saltaría. Si no, no lo haría. No quiero seguir ciegamente una práctica sin saber por qué estoy siguiendo es ...
Jasarien
44
Si alloc devuelve nil, init se envía a nil, lo que siempre dará como resultado nil, y esto terminará siendo self nil.
T.
50

Este modismo particular es estándar porque funciona en todos los casos.

Si bien es poco común, habrá casos en los que ...

[super init];

... devuelve una instancia diferente, lo que requiere la asignación a sí mismo.

Y habrá casos en los que devolverá nil, lo que requerirá la comprobación nil para que su código no intente inicializar una ranura de variable de instancia que ya no existe.

La conclusión es que es el patrón correcto documentado para usar y, si no lo está usando, lo está haciendo mal.

bbum
fuente
3
¿Sigue siendo cierto a la luz de los especificadores de nulabilidad? Si el inicializador de mi superclase no es nulo, ¿realmente vale la pena el desorden adicional para verificar? (Aunque NSObject en sí mismo no parece tener ninguno para su -initadicto ...)
natevw
¿Hay algún caso en el que [super init]devuelva nulo cuando la superclase directa es NSObject? ¿No es este un caso donde "todo está roto?"
Dan Rosenstark
1
@DanRosenstark No si NSObjectes la superclase directa. Pero ... incluso si declaras NSObjectcomo la superclase directa, algo podría haberse modificado en tiempo de ejecución de tal manera que NSObjectla implementación de initno es lo que realmente se llama.
bbum
1
Muchas gracias @bbum, esto realmente me ayudó a orientarme en la corrección de algunos errores. ¡Es bueno descartar algunas cosas!
Dan Rosenstark
26

Creo que, en la mayoría de las clases, si el valor de retorno de [super init] es nulo y lo verifica, como lo recomiendan las prácticas estándar, y luego regresa prematuramente si es nulo, básicamente su aplicación todavía no funcionará correctamente. Si se piensa en ello, a pesar de que si (auto! = Nil) cheque está ahí, para un correcto funcionamiento de la clase, el 99,99% de las veces que realmente haces necesidad de auto para ser no nula. Ahora, supongamos que, por cualquier razón, [súper init] hizo nula rentabilidad, básicamente, su cheque contra nula es, básicamente, pasando la pelota a la persona que llama de la clase, donde sería probable es que no todos modos, ya que, naturalmente, asumir que la llamada era exitoso.

Básicamente, lo que quiero decir es que el 99.99% de las veces, el if (self! = Nil) no te compra nada en términos de mayor robustez, ya que solo estás pasando el dinero a tu invocador. Para realmente poder manejar esto de manera robusta, en realidad necesitaría poner controles en toda su jerarquía de llamadas. E incluso entonces, lo único que te compraría es que tu aplicación fallará de manera más limpia / robusta. Pero aún fallaría.

Si una clase de biblioteca decidió arbitrariamente devolver nil como resultado de un [superiniciado], de todos modos estás bastante jodido, y eso es más una indicación de que el escritor de la clase de biblioteca cometió un error de implementación.

Creo que esto es más una sugerencia de codificación heredada, cuando las aplicaciones se ejecutaron en una memoria mucho más limitada.

Pero para el código de nivel C, todavía normalmente verificaría el valor de retorno de malloc () contra un puntero NULL. Mientras que, para Objective-C, hasta que encuentre evidencia de lo contrario, creo que generalmente omitiré las verificaciones if (self! = Nil). ¿Por qué la discrepancia?

Porque, en los niveles de C y malloc, en algunos casos puede recuperarse parcialmente. Mientras que pienso en Objective-C, en el 99.99% de los casos, si [super init] devuelve nulo, básicamente estás jodido, incluso si intentas manejarlo. También podría dejar que la aplicación se bloquee y lidiar con las consecuencias.

Gino
fuente
66
Bien hablado. Secundo que.
Roger CS Wernersson
3
+1 Estoy totalmente de acuerdo. Solo una pequeña nota: no creo que el patrón sea el resultado de momentos en que la asignación fallaba con mayor frecuencia. La asignación generalmente ya se realizó en el momento en que se llama a init. Si alloc fallara, ni siquiera se llamaría init.
Nikolai Ruhe
8

Este es un resumen de los comentarios anteriores.

Digamos que la superclase regresa nil. ¿Qué va a pasar?

Si no sigues las convenciones

Tu código se bloqueará en medio de tu initmétodo. (a menos initque no haga nada significativo)

Si sigue las convenciones, sin saber que la superclase podría volver nula (la mayoría de las personas terminan aquí)

Es probable que su código se bloquee en algún momento más tarde, porque su instancia es nil, donde esperaba algo diferente. O tu programa se comportará inesperadamente sin fallar. ¡Oh querido! ¿Quieres esto? No lo sé...

Si sigues las convenciones, permites voluntariamente que tu subclase devuelva cero

La documentación de su código (!) Debe indicar claramente: "devuelve ... o nulo", y el resto de su código debe estar preparado para manejar esto. Ahora tiene sentido.

usuario123444555621
fuente
55
El punto interesante aquí, creo, es que la opción n. ° 1 es claramente preferible a la opción n. ° 2. Si hay circunstancias genuinas en las que es posible que desee que el init de su subclase devuelva nulo, se preferirá el n. ° 3. Si la única razón que alguna vez sucedería es debido a un error en su código, entonces use el # 1. Usar la opción n. ° 2 es solo retrasar la explosión de su aplicación hasta un momento posterior y, por lo tanto, hacer su trabajo cuando se trata de depurar el error mucho más difícil. Es como capturar excepciones en silencio y continuar sin manejarlas.
Mark Amery
O cambia a rápido y solo usa opciones
AndrewSB
7

Por lo general, si su clase se deriva directamente de NSObject, no necesitará hacerlo. Sin embargo, es un buen hábito entrar, ya que si su clase se deriva de otras clases, sus inicializadores pueden regresar nil, y si es así, su inicializador puede capturar eso y comportarse correctamente.

Y sí, para que conste, sigo las mejores prácticas y las escribo en todas mis clases, incluso en aquellas derivadas directamente NSObject.

John Rudy
fuente
1
Con esto en mente, ¿sería una buena práctica verificar nulo después de inicializar una variable y antes de invocar funciones en ella? Por ejemploFoo *bar = [[Foo alloc] init]; if (bar) {[bar doStuff];}
dev_does_software
Heredar de NSObjectno garantiza -initque NSObjecttambién te dé , si se cuentan tiempos de ejecución exóticos como versiones anteriores de GNUstep (que regresa GSObject), así que no importa qué, un cheque y una asignación.
Maxthon Chan el
3

Tienes razón, a menudo podrías escribir [super init], pero eso no funcionaría para una subclase de cualquier cosa. Las personas prefieren simplemente memorizar una línea de código estándar y usarla todo el tiempo, incluso cuando solo es a veces necesario, y así obtenemos el estándar if (self = [super init]), que tiene la posibilidad de que se devuelva nulo y la posibilidad de un objeto que no selfsea ​​devuelto en cuenta.

andyvn22
fuente
3

Un error común es escribir

self = [[super alloc] init];

que devuelve una instancia de la superclase, que NO es lo que desea en un constructor / init de subclase. Obtiene un objeto que no responde a los métodos de la subclase, que puede ser confuso, y genera errores confusos sobre no responder a métodos o identificadores no encontrados, etc.

self = [super init]; 

es necesario si la superclase tiene miembros (variables u otros objetos) para inicializar primero antes de configurar los miembros de las subclases. De lo contrario, el tiempo de ejecución objc los inicializa a 0 o a cero . (a diferencia de ANSI C, que a menudo asigna fragmentos de memoria sin borrarlos en absoluto )

Y sí, la inicialización de la clase base puede fallar debido a errores de falta de memoria, componentes faltantes, fallas de adquisición de recursos, etc., por lo que una comprobación de nulo es inteligente y toma menos de unos pocos milisegundos.

Chris Reid
fuente
2

Esto es para verificar que la intialazation funcionó, la instrucción if devuelve true si el método init no devolvió nil, por lo que es una forma de verificar que la creación del objeto funcionó correctamente. Pocas razones por las que puedo pensar que ese init podría fallar, tal vez sea un método init anulado que la superclase desconoce o algo por el estilo, aunque no creo que sea tan común. Pero si sucede, es mejor que no suceda un accidente, supongo que siempre se verifica ...

Daniel
fuente
lo es, pero si se juntan, ¿qué sucede si falla todo?
Daniel
Me imagino que si alloc falla, init se enviará a nil en lugar de una instancia de cualquier clase a la que esté llamando init. En ese caso, no sucederá nada y no se ejecutará ningún código para probar incluso si [super init] devuelve nil o no.
Jasarien
2
La asignación de memoria no siempre se realiza en + alloc. Considere el caso de los grupos de clases; un NSString no sabe qué subclase específica usar hasta que se invoca el inicializador.
bbum
1

En OS X, no es tan probable -[NSObject init]que falle debido a razones de memoria. No se puede decir lo mismo para iOS.

Además, es una buena práctica escribir cuando se subclasifica una clase que podría regresar nilpor cualquier razón.

MaddTheSane
fuente
2
Tanto en iOS como en Mac OS -[NSObject init]es muy poco probable que falle debido a razones de memoria, ya que no asigna ninguna memoria.
Nikolai Ruhe
Supongo que se refería a alloc, no init :)
Thomas Tempelmann