Enviar un mensaje a cero en Objective-C

107

Como desarrollador de Java que está leyendo la documentación de Objective-C 2.0 de Apple: me pregunto qué significa " enviar un mensaje a cero ", y mucho menos cómo es realmente útil. Tomando un extracto de la documentación:

Hay varios patrones en Cocoa que aprovechan este hecho. El valor devuelto de un mensaje a nil también puede ser válido:

  • Si el método devuelve un objeto, cualquier tipo de puntero, cualquier escalar entero de tamaño menor o igual que sizeof (void *), un flotante, un doble, un doble largo o un largo largo, entonces un mensaje enviado a nil devuelve 0 .
  • Si el método devuelve una estructura, según lo definido por la Guía de llamadas a funciones ABI de Mac OS X para que se devuelva en los registros, un mensaje enviado a nil devuelve 0.0 para cada campo en la estructura de datos. Otros tipos de datos de estructura no se rellenarán con ceros.
  • Si el método devuelve algo diferente a los tipos de valor mencionados anteriormente, el valor de retorno de un mensaje enviado a nil no está definido.

¿Java ha hecho que mi cerebro sea incapaz de asimilar la explicación anterior? ¿O hay algo que me falta y que lo dejaría tan claro como el vidrio?

Tengo la idea de mensajes / receptores en Objective-C, simplemente estoy confundido acerca de un receptor que resulta ser nil.

Ryan Delucchi
fuente
2
También tenía experiencia en Java y estaba aterrorizado por esta buena característica al principio, ¡pero ahora la encuentro absolutamente ENCANTADORA !;
Valentin Radu
1
Gracias, esa es una gran pregunta. ¿Ha visto los beneficios de eso? Me parece que "no es un error, es una característica". Sigo recibiendo errores en los que Java simplemente me abofetea con una excepción, así que sabía dónde estaba el problema. No me complace intercambiar la excepción del puntero nulo para guardar una o dos líneas de código trivial aquí y allá.
Maciej Trybiło

Respuestas:

92

Bueno, creo que se puede describir con un ejemplo muy elaborado. Digamos que tiene un método en Java que imprime todos los elementos en una ArrayList:

void foo(ArrayList list)
{
    for(int i = 0; i < list.size(); ++i){
        System.out.println(list.get(i).toString());
    }
}

Ahora, si llama a ese método así: someObject.foo (NULL); probablemente obtendrá una NullPointerException cuando intente acceder a la lista, en este caso en la llamada a list.size (); Ahora, probablemente nunca llamarías a someObject.foo (NULL) con el valor NULL así. Sin embargo, es posible que haya obtenido su ArrayList de un método que devuelve NULL si se encuentra con algún error al generar ArrayList como someObject.foo (otherObject.getArrayList ());

Por supuesto, también tendrás problemas si haces algo como esto:

ArrayList list = NULL;
list.size();

Ahora, en Objective-C, tenemos el método equivalente:

- (void)foo:(NSArray*)anArray
{
    int i;
    for(i = 0; i < [anArray count]; ++i){
        NSLog(@"%@", [[anArray objectAtIndex:i] stringValue];
    }
}

Ahora, si tenemos el siguiente código:

[someObject foo:nil];

tenemos la misma situación en la que Java producirá una NullPointerException. Se accederá primero al objeto nil en [anArray count] Sin embargo, en lugar de lanzar una NullPointerException, Objective-C simplemente devolverá 0 de acuerdo con las reglas anteriores, por lo que el bucle no se ejecutará. Sin embargo, si configuramos el bucle para que se ejecute un número determinado de veces, primero enviaremos un mensaje a anArray en [anArray objectAtIndex: i]; Esto también devolverá 0, pero dado que objectAtIndex: devuelve un puntero, y un puntero a 0 es nil / NULL, NSLog pasará nil cada vez a través del bucle. (Aunque NSLog es una función y no un método, se imprime (nulo) si se le pasa un NSString nulo.

En algunos casos, es mejor tener una NullPointerException, ya que puede saber de inmediato que algo anda mal con el programa, pero a menos que detecte la excepción, el programa se bloqueará. (En C, intentar eliminar la referencia a NULL de esta manera hace que el programa se bloquee). En Objective-C, en cambio, solo causa un comportamiento de tiempo de ejecución posiblemente incorrecto. Sin embargo, si tiene un método que no se rompe si devuelve 0 / nil / NULL / una estructura cero, esto le evita tener que verificar para asegurarse de que el objeto o los parámetros sean nulos.

Michael Buckley
fuente
33
Probablemente valga la pena mencionar que este comportamiento ha sido objeto de mucho debate en la comunidad Objective-C durante las últimas dos décadas. El equilibrio entre "seguridad" y "conveniencia" es evaluado de manera diferente por diferentes personas.
Mark Bessey
3
En la práctica, hay mucha simetría entre pasar mensajes a cero y cómo funciona Objective-C, especialmente en la nueva función de punteros débiles en ARC. Los punteros débiles se ponen a cero automáticamente. Por lo que el diseño de la API para que pueda responder a 0 / nula / NIL / NULL, etc
Cthutu
1
Creo que si lo haces myObject->iVarse bloqueará, independientemente de si es C con o sin objetos. (perdón por
Gravedig
3
@ 11684 Eso es correcto, pero ->ya no es una operación de Objective-C sino más bien un C-ismo genérico.
bbum
1
La reciente raíz OSX explotar / api puerta trasera oculta es accesible para todos los usuarios (no sólo los administradores) a causa de la mensajería Ninguna de obj-c.
dcow
51

Un mensaje que nilno hace nada y vuelve nil, Nil, NULL, 0, o 0.0.

Peter Hosey
fuente
41

Todas las demás publicaciones son correctas, pero tal vez sea el concepto lo importante aquí.

En las llamadas al método Objective-C, cualquier referencia de objeto que pueda aceptar un selector es un destino válido para ese selector.

Esto ahorra MUCHO "¿el objeto de destino es de tipo X?" código: siempre que el objeto receptor implemente el selector, ¡ no importa qué clase sea! niles un NSObject que acepta cualquier selector, simplemente no hace nada. Esto elimina una gran cantidad de código de "verificación nula, no envíe el mensaje si es verdadero" también. (El concepto "si lo acepta, lo implementa" es también lo que le permite crear protocolos , que son algo así como interfaces Java: una declaración de que si una clase implementa los métodos establecidos, entonces se ajusta al protocolo).

La razón de esto es eliminar el código mono que no hace nada más que mantener contento al compilador. Sí, obtiene la sobrecarga de una llamada más al método, pero ahorra tiempo al programador , que es un recurso mucho más caro que el tiempo de la CPU. Además, está eliminando más código y más complejidad condicional de su aplicación.

Aclaración para los votantes negativos: puede pensar que este no es un buen camino a seguir, pero así es como se implementa el lenguaje y es el lenguaje de programación recomendado en Objective-C (consulte las conferencias de programación de iPhone de Stanford).

Joe McMahon
fuente
17

Lo que significa es que el tiempo de ejecución no produce un error cuando se llama a objc_msgSend en el puntero nulo; en su lugar, devuelve algún valor (a menudo útil). Los mensajes que pueden tener un efecto secundario no hacen nada.

Es útil porque la mayoría de los valores predeterminados son más apropiados que un error. Por ejemplo:

[someNullNSArrayReference count] => 0

Es decir, nil parece ser la matriz vacía. Ocultar una referencia nula de NSView no hace nada. Práctico, ¿eh?

Rico
fuente
12

En la cita de la documentación, hay dos conceptos separados; tal vez sería mejor si la documentación lo dejara más claro:

Hay varios patrones en Cocoa que aprovechan este hecho.

El valor devuelto de un mensaje a nil también puede ser válido:

Lo primero es probablemente más relevante aquí: por lo general, poder enviar mensajes a nilhace que el código sea más sencillo; no tiene que buscar valores nulos en todas partes. El ejemplo canónico es probablemente el método de acceso:

- (void)setValue:(MyClass *)newValue {
    if (value != newValue) { 
        [value release];
        value = [newValue retain];
    }
}

Si enviar mensajes a nilno fuera válido, este método sería más complejo: tendría que tener dos comprobaciones adicionales para asegurarse valuey newValueno nilantes de enviarles mensajes.

Sin nilembargo, el último punto (al que los valores devueltos por un mensaje también suelen ser válidos) agrega un efecto multiplicador al primero. Por ejemplo:

if ([myArray count] > 0) {
    // do something...
}

Este código nuevamente no requiere una verificación de nilvalores y fluye naturalmente ...

Dicho todo esto, la flexibilidad adicional a la que poder enviar mensajes niltiene algún costo. Existe la posibilidad de que en algún momento escriba un código que falle de una manera peculiar porque no tuvo en cuenta la posibilidad de que un valor pudiera ser nil.

mmalc
fuente
12

De Greg Parker 's sitio :

Si ejecuta LLVM Compiler 3.0 (Xcode 4.2) o posterior

Mensajes a cero con tipo de retorno | regreso
Enteros hasta 64 bits | 0
Punto flotante hasta doble largo | 0.0
Punteros | nulo
Estructuras | {0}
Cualquier tipo _Complex | {0, 0}
Heath Borders
fuente
9

A menudo significa no tener que buscar ningún objeto en todas partes por seguridad, en particular:

[someVariable release];

o, como se señaló, varios métodos de conteo y longitud devuelven 0 cuando tiene un valor nulo, por lo que no tiene que agregar comprobaciones adicionales para nulo:

if ( [myString length] > 0 )

o esto:

return [myArray count]; // say for number of rows in a table
Kendall Helmstetter Gelner
fuente
Tenga en cuenta que la otra cara de la moneda es la posibilidad de errores como "if ([myString length] == 1)"
hatfinch
¿Cómo es eso un error? [myString length] devuelve cero (nil) si myString es nil ... una cosa que creo que podría ser un problema es [myView frame] que creo que puede darle algo extraño si myView es nil.
Kendall Helmstetter Gelner
Si diseña sus clases y métodos en torno al concepto de que los valores predeterminados (0, nil, NO) significan "no útil", esta es una herramienta poderosa. Nunca tengo que comprobar si mis cadenas son nulas antes de comprobar la longitud. Para mí, una cadena vacía es tan inútil y una cadena nula cuando estoy procesando texto. También soy un desarrollador de Java y sé que los puristas de Java evitarán esto, pero ahorra mucha codificación.
Jason Fuerstenberg
6

No piense en "el receptor es nulo"; Estoy de acuerdo, eso es bastante extraño. Si envía un mensaje a cero, no hay receptor. Estás enviando un mensaje a la nada.

Cómo lidiar con eso es una diferencia filosófica entre Java y Objective-C: en Java, eso es un error; en Objective-C, no es una operación.

benzado
fuente
Hay una excepción a ese comportamiento en Java, si llama a una función estática en un nulo, es equivalente a llamar a la función en la clase de tiempo de compilación de la variable (no importa si es nulo).
Roman A. Taycher
6

Los mensajes ObjC que se envían a cero y cuyos valores de retorno tienen un tamaño mayor que sizeof (void *) producen valores indefinidos en los procesadores PowerPC. Además de eso, estos mensajes hacen que se devuelvan valores indefinidos en campos de estructuras cuyo tamaño sea mayor de 8 bytes en procesadores Intel también. Vincent Gable ha descrito esto muy bien en su publicación de blog.

Nikita Zhuk
fuente
6

No creo que ninguna de las otras respuestas haya mencionado esto claramente: si está acostumbrado a Java, debe tener en cuenta que, si bien Objective-C en Mac OS X tiene soporte para manejo de excepciones, es una característica de lenguaje opcional que puede ser encendido / apagado con un indicador de compilador. Supongo que este diseño de "enviar mensajes a niles seguro" es anterior a la inclusión del soporte de manejo de excepciones en el idioma y se hizo con un objetivo similar en mente: los métodos pueden regresar nilpara indicar errores y, dado que enviar un mensaje a, nilgeneralmente regresanila su vez, esto permite que la indicación de error se propague a través de su código para que no tenga que buscarlo en cada mensaje. Solo tiene que verificarlo en los puntos donde sea importante. Personalmente, creo que la propagación y el manejo de excepciones es una mejor manera de abordar este objetivo, pero no todos pueden estar de acuerdo con eso. (Por otro lado, por ejemplo, no me gusta el requisito de Java de que tengas que declarar qué excepciones puede arrojar un método, lo que a menudo te obliga a propagar sintácticamente declaraciones de excepción a lo largo de tu código; pero esa es otra discusión).

Publiqué una respuesta similar, pero más larga, a la pregunta relacionada "¿Es necesario afirmar que cada creación de objetos tuvo éxito en el Objetivo C?" si quieres más detalles.

Rinzwind
fuente
Nunca lo había pensado así. Esta parece ser una característica muy conveniente.
mk12
2
Buena suposición, pero históricamente inexacta sobre por qué se tomó la decisión. El manejo de excepciones existió en el lenguaje desde el principio, aunque los manejadores de excepciones originales eran bastante primitivos en comparación con el idioma moderno. Nil-eats-message fue una elección de diseño consciente derivada del comportamiento opcional del objeto Nil en Smalltalk. Cuando se diseñaron las API originales de NeXTSTEP, el encadenamiento de métodos era bastante común y se nilusaba un retorno para cortocircuitar una cadena en una operación NO.
bbum
2

C no representa nada como 0 para valores primitivos y NULL para punteros (que es equivalente a 0 en un contexto de puntero).

Objective-C se basa en la representación de C de nada agregando nil. nil es un puntero de objeto a nada. Aunque semánticamente distintos de NULL, son técnicamente equivalentes entre sí.

Los NSObjects recién asignados comienzan su vida con su contenido establecido en 0. Esto significa que todos los punteros que el objeto tiene a otros objetos comienzan como nil, por lo que no es necesario, por ejemplo, establecer self. (Association) = nil en los métodos init.

Sin embargo, el comportamiento más notable de nil es que puede recibir mensajes.

En otros lenguajes, como C ++ (o Java), esto bloquearía su programa, pero en Objective-C, invocar un método en nil devuelve un valor cero. Esto simplifica enormemente las expresiones, ya que evita la necesidad de verificar nil antes de hacer algo:

// For example, this expression...
if (name != nil && [name isEqualToString:@"Steve"]) { ... }

// ...can be simplified to:
if ([name isEqualToString:@"Steve"]) { ... }

Ser consciente de cómo funciona nil en Objective-C permite que esta conveniencia sea una característica y no un error al acecho en su aplicación. Asegúrese de protegerse contra los casos en los que los valores nulos no sean deseados, ya sea verificando y regresando temprano para fallar silenciosamente o agregando un NSParameterAssert para lanzar una excepción.

Fuente: http://nshipster.com/nil/ https://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocObjectsClasses.html (Enviar mensaje a nil).

Zee
fuente