objective-c ios memory-management key-value-observing ivar

objective c - ¿Por qué usarías un ivar?



objective-c ios (7)

Suelo ver esta pregunta a la inversa, como ¿debe ser cada ivar una propiedad? (y me gusta la respuesta del bbum a esta Q).

Uso propiedades casi exclusivamente en mi código. De vez en cuando, sin embargo, trabajo con un contratista que ha estado desarrollando en iOS durante mucho tiempo y es un programador de juegos tradicional. Escribe código que casi no declara propiedades y se apoya en ivars. Supongo que lo hace porque 1.) está acostumbrado, ya que las propiedades no siempre existieron hasta el Objetivo C 2.0 (oct ''07) y 2.) para la ganancia de rendimiento mínima de no pasar por un eliminador / instalador.

Mientras escribe código que no se filtre, aún preferiría que usara propiedades sobre ivars. Hablamos al respecto y él, más o menos, no ve razones para usar propiedades, ya que no estábamos usando KVO y tiene experiencia en resolver los problemas de memoria.

Mi pregunta es más ... ¿Por qué querrías usar un período ivar - experimentado o no? ¿Existe realmente una gran diferencia de rendimiento que usar un ivar estaría justificado?

También como punto de aclaración, anulo setters y getters según sea necesario y uso el ivar que se correlaciona con esa propiedad dentro del getter / setter. Sin embargo, fuera de getter / setter o init, siempre uso la sintaxis self.myProperty .

Editar 1

Aprecio todas las buenas respuestas. Una que me gustaría abordar que parece incorrecta es que con un ivar se obtiene una encapsulación donde con una propiedad no es así. Simplemente defina la propiedad en una continuación de clase. Esto ocultará la propiedad a los de afuera. También puede declarar la propiedad de solo lectura en la interfaz y redefinirla como readwrite en la implementación como:

// readonly for outsiders @property (nonatomic, copy, readonly) NSString * name;

y tener en la continuación de la clase:

// readwrite within this file @property (nonatomic, copy) NSString * name;

Para que sea completamente "privado", solo denúncielo en la continuación de la clase.


Encapsulación

Si el ivar es privado, las otras partes del programa no pueden acceder a él tan fácilmente. Con una propiedad declarada, las personas inteligentes pueden acceder y mudar con bastante facilidad a través de los accesorios.

Actuación

Sí, esto puede hacer la diferencia en algunos casos. Algunos programas tienen limitaciones donde no pueden usar ningún mensaje objc en ciertas partes del programa (piense en tiempo real). En otros casos, es posible que desee acceder directamente a la velocidad. En otros casos, es porque objc mensajería actúa como un firewall de optimización. Finalmente, puede reducir sus operaciones de recuento de referencias y minimizar el uso máximo de la memoria (si se hace correctamente).

Tipos no triviales

Ejemplo: si tiene un tipo de C ++, el acceso directo es a veces el mejor enfoque. Es posible que el tipo no se pueda copiar o que no sea trivial copiar.

Multihilo

Muchos de tus ivars son codependientes. Debe garantizar su integridad de datos en contexto multiproceso. Por lo tanto, puede favorecer el acceso directo a múltiples miembros en secciones críticas. Si se queda con los descriptores de acceso para los datos codependientes, sus bloqueos generalmente deben reentrancarse y con frecuencia terminará realizando muchas adquisiciones (significativamente más a veces).

Corrección del programa

Como las subclases pueden anular cualquier método, eventualmente puede ver que hay una diferencia semántica entre escribir en la interfaz y administrar su estado de manera apropiada. El acceso directo para la corrección del programa es especialmente común en estados parcialmente construidos: en sus inicializadores y en dealloc , lo mejor es usar acceso directo. También puede encontrar esto común en las implementaciones de un accesorio, un constructor de conveniencia, copy , mutableCopy y las implementaciones de archivado / serialización.

También es más frecuente a medida que uno se mueve desde el todo tiene una mentalidad de acceso de lectura de lectura pública a una que oculta bien sus datos / datos de implementación. A veces es necesario pasar correctamente los efectos secundarios que una anulación de subclase puede introducir para hacer lo correcto.

Tamaño binario

Si se declara todo lo leído de forma predeterminada, generalmente se obtienen muchos métodos de acceso que nunca se necesitan, cuando se considera la ejecución de su programa por un momento. Por lo tanto, agregará algo de grasa a su programa y también cargará los tiempos.

Minimiza la Complejidad

En algunos casos, es completamente innecesario agregar + escribir + mantener todos los andamios adicionales para una variable simple como un bool privado que está escrito en un método y leído en otro.

Eso no significa en absoluto que el uso de propiedades o accesorios sea malo; cada uno tiene importantes beneficios y restricciones. Al igual que muchos lenguajes OO y enfoques de diseño, también debería favorecer a los usuarios con la visibilidad adecuada en ObjC. Habrá ocasiones en que necesite desviarse. Por esa razón, creo que a menudo es mejor restringir los accesos directos a la implementación que declara el ivar (por ejemplo, declararlo @private ).

re Editar 1:

La mayoría de nosotros hemos memorizado cómo llamar a un accesorio oculto dinámicamente (siempre que sepamos el nombre ...). Mientras tanto, la mayoría de nosotros no hemos memorizado cómo acceder correctamente a los ivars que no son visibles (más allá de KVC). La continuación de clase ayuda , pero introduce vulnerabilidades.

Esta solución es obvia:

if ([obj respondsToSelector:(@selector(setName:)]) [(id)obj setName:@"Al Paca"];

Ahora pruébelo solo con un ivar y sin KVC.


La razón más importante es el concepto OOP de ocultación de información : si expones todo a través de las propiedades y haces que los objetos externos puedan ver las partes internas de otro objeto, entonces harás uso de estas internas y, por lo tanto, complicarás el cambio de la implementación.

La ganancia de "rendimiento mínimo" puede resumirse rápidamente y convertirse en un problema. Lo sé por experiencia; Trabajo en una aplicación que realmente lleva los iDevices a sus límites y, por lo tanto, debemos evitar llamadas a métodos innecesarios (por supuesto, solo cuando sea razonablemente posible). Para ayudar con este objetivo, también estamos evitando la sintaxis de puntos, ya que hace que sea difícil ver el número de llamadas de método a primera vista: por ejemplo, ¿cuántas llamadas a métodos self.image.size.width la expresión self.image.size.width ? Por el contrario, puede decir inmediatamente con [[self image] size].width .

Además, con el nombramiento correcto de ivar, KVO es posible sin propiedades (IIRC, no soy un experto en KVO).


Las propiedades exponen tus variables a otras clases. Si solo necesita una variable que sea relativa a la clase que está creando, use una variable de instancia. Aquí hay un pequeño ejemplo: las clases XML para analizar el RSS y el ciclo similar a través de un grupo de métodos de delegado y tal. Es práctico tener una instancia de NSMutableString para almacenar el resultado de cada pase diferente del análisis sintáctico. No hay ninguna razón para que una clase externa necesite acceder o manipular esa cadena. Entonces, solo lo declara en el encabezado o en privado y accede a él durante toda la clase. Establecer una propiedad solo podría ser útil para asegurarse de que no haya problemas de memoria, utilizando self.mutableString para invocar el getter / setters.


Las propiedades frente a las variables de instancia es una compensación, al final la elección se reduce a la aplicación.

Encapsulación / ocultación de información Esta es una buena cosa (TM) desde una perspectiva de diseño, las interfaces estrechas y la vinculación mínima es lo que hace que el software sea fácil de mantener y comprensible. En Obj-C es bastante difícil ocultar cualquier cosa, pero las variables de instancia declaradas en la implementación se acercan lo más posible.

Rendimiento Mientras que la "optimización prematura" es una cosa mala (TM), escribir un código que funciona mal solo porque puede es al menos tan malo. Es difícil argumentar en contra de que una llamada a un método sea más costosa que una carga o una tienda, y en el código computacional intensivo el costo pronto se suma.

En un lenguaje estático con propiedades, como C #, el compilador puede optimizar las llamadas a setters / getters. Sin embargo, Obj-C es dinámico y eliminar tales llamadas es mucho más difícil.

Abstracción Un argumento contra las variables de instancia en Obj-C ha sido tradicionalmente la gestión de la memoria. Con MRC, las variables de instancia requieren llamadas para retener / liberar / autorrelease para que se extiendan por todo el código, las propiedades (sintetizadas o no) mantienen el código MRC en un solo lugar: el principio de abstracción que es Good Thing (TM). Sin embargo, con GC o ARC este argumento desaparece, por lo que la abstracción para la gestión de la memoria ya no es un argumento contra las variables de instancia.


Para mí, generalmente es rendimiento. El acceso a un ivar de un objeto es tan rápido como acceder a un miembro de estructura en C usando un puntero a la memoria que contiene dicha estructura. De hecho, los objetos de Objective-C son básicamente estructuras C ubicadas en la memoria asignada dinámicamente. Esto suele ser tan rápido como puede obtener su código, ni siquiera el código ensamblado optimizado a mano puede ser más rápido que eso.

Acceder a un ivar a través de un getter / setting implica una llamada al método Objective-C, que es mucho más lenta (al menos 3-4 veces) que una llamada a función C "normal" e incluso una llamada a función C normal sería mucho más lenta que accediendo a un miembro de la estructura. Dependiendo de los atributos de su propiedad, la implementación setter / getter generada por el compilador puede involucrar otra llamada de función C a las funciones objc_getProperty / objc_setProperty , ya que éstas tendrán que retain / copy / autorelease los objetos según sea necesario y realizar más spinlocking para atomic propiedades donde sea necesario. Esto puede ser muy costoso y no estoy hablando de ser un 50% más lento.

Intentemos esto:

CFAbsoluteTime cft; unsigned const kRuns = 1000 * 1000 * 1000; cft = CFAbsoluteTimeGetCurrent(); for (unsigned i = 0; i < kRuns; i++) { testIVar = i; } cft = CFAbsoluteTimeGetCurrent() - cft; NSLog(@"1: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns); cft = CFAbsoluteTimeGetCurrent(); for (unsigned i = 0; i < kRuns; i++) { [self setTestIVar:i]; } cft = CFAbsoluteTimeGetCurrent() - cft; NSLog(@"2: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

Salida:

1: 23.0 picoseconds/run 2: 98.4 picoseconds/run

Esto es 4.28 veces más lento y esta fue una int no primitiva atómica, casi el mejor de los casos ; la mayoría de los otros casos son incluso peores (¡prueba una propiedad atómica NSString * !). Entonces, si puedes vivir con el hecho de que cada acceso ivar es 4-5 veces más lento de lo que podría ser, usar propiedades está bien (al menos en lo que respecta al rendimiento), sin embargo, hay un montón de situaciones donde tal caída del rendimiento es completamente inaceptable.

Actualización 2015-10-20

Algunas personas argumentan que este no es un problema del mundo real, el código anterior es puramente sintético y nunca lo notarás en una aplicación real. Está bien, probemos una muestra del mundo real.

El código siguiente a continuación define los objetos de Account . Una cuenta tiene propiedades que describen el nombre ( NSString * ), el género ( enum ) y la edad ( unsigned ) de su propietario, así como un saldo ( int64_t ). Un objeto cuenta tiene un método init y un método compare: . El método compare: se define como: Pedidos femeninos antes que masculinos, nombres ordenados alfabéticamente, pedidos jóvenes antes que viejos, órdenes de saldo bajas o elevadas.

En realidad, existen dos clases de cuenta, AccountA y AccountB . Si observa su implementación, notará que son casi completamente idénticos, con una excepción: el método compare: . AccountA objetos AccountA acceden a sus propias propiedades por método (getter), mientras que los objetos AccountB acceden a sus propias propiedades mediante ivar. ¡Esa es realmente la única diferencia! Ambos acceden a las propiedades del otro objeto para compararlas con getter (¡no sería seguro acceder a él por ivar! ¿Qué sucede si el otro objeto es una subclase y ha anulado el getter?). También tenga en cuenta que acceder a sus propias propiedades como ivars no rompe la encapsulación (los ivars aún no son públicos).

La configuración de la prueba es realmente simple: crea cuentas de 1 Mio al azar, agrégalas a una matriz y ordena esa matriz. Eso es. Por supuesto, hay dos matrices, una para objetos AccountA y otra para objetos AccountB y ambas matrices están llenas de cuentas idénticas (la misma fuente de datos). Calculamos cuánto tiempo lleva ordenar las matrices.

Aquí está la salida de varias carreras que hice ayer:

runTime 1: 4.827070, 5.002070, 5.014527, 5.019014, 5.123039 runTime 2: 3.835088, 3.804666, 3.792654, 3.796857, 3.871076

Como puede ver, ordenar la matriz de objetos AccountB siempre es significativamente más rápido que ordenar la matriz de objetos AccountA .

Quien afirme que las diferencias de tiempo de ejecución de hasta 1.32 segundos no hacen ninguna diferencia, nunca debería hacer la programación de IU. Si quiero cambiar el orden de clasificación de una tabla grande, por ejemplo, las diferencias de tiempo como estas hacen una gran diferencia para el usuario (la diferencia entre una IU aceptable y una IU lenta).

También en este caso, el código de muestra es el único trabajo real realizado aquí, pero ¿con qué frecuencia su código es solo un engranaje pequeño de un reloj complicado? Y si cada engranaje ralentiza todo el proceso de esta manera, ¿qué significa eso para la velocidad de todo el mecanismo al final? Especialmente si un paso de trabajo depende del resultado de otro, lo que significa que todas las ineficiencias se resumirán. La mayoría de las ineficiencias no son un problema en sí mismas, es su gran suma lo que se convierte en un problema para todo el proceso. Y ese problema no es nada que un perfilador muestre fácilmente porque un perfilador trata de encontrar puntos críticos críticos, pero ninguna de estas ineficiencias son puntos calientes por sí mismos. El tiempo de CPU se distribuye de manera promedio entre ellos, pero cada uno de ellos solo tiene una fracción tan pequeña, parece una pérdida de tiempo total para optimizarlo. Y es verdad, optimizar solo uno de ellos no ayudaría absolutamente nada, optimizarlos a todos podría ayudar de manera espectacular.

E incluso si no piensa en términos de tiempo de CPU, porque cree que perder el tiempo de CPU es totalmente aceptable, después de todo, "es gratis", ¿qué pasa con los costos de alojamiento del servidor causados ​​por el consumo de energía? ¿Qué pasa con el tiempo de ejecución de la batería de los dispositivos móviles? Si escribes la misma aplicación móvil dos veces (por ejemplo, un navegador web móvil), una vez que todas las clases accedan a sus propias propiedades solo por getters y una vez que todas las clases accedan a ellas solo por ivars, usar la primera constantemente la batería es mucho más rápida que con la segunda, a pesar de que son equivalentes funcionales y para el usuario la segunda probablemente incluso se sienta un poco más rápida.

Ahora aquí está el código para su archivo main.m (el código se basa en la habilitación de ARC y asegúrese de usar la optimización al compilar para ver el efecto completo):

#import <Foundation/Foundation.h> typedef NS_ENUM(int, Gender) { GenderMale, GenderFemale }; @interface AccountA : NSObject @property (nonatomic) unsigned age; @property (nonatomic) Gender gender; @property (nonatomic) int64_t balance; @property (nonatomic,nonnull,copy) NSString * name; - (NSComparisonResult)compare:(nonnull AccountA *const)account; - (nonnull instancetype)initWithName:(nonnull NSString *const)name age:(const unsigned)age gender:(const Gender)gender balance:(const int64_t)balance; @end @interface AccountB : NSObject @property (nonatomic) unsigned age; @property (nonatomic) Gender gender; @property (nonatomic) int64_t balance; @property (nonatomic,nonnull,copy) NSString * name; - (NSComparisonResult)compare:(nonnull AccountB *const)account; - (nonnull instancetype)initWithName:(nonnull NSString *const)name age:(const unsigned)age gender:(const Gender)gender balance:(const int64_t)balance; @end static NSMutableArray * allAcocuntsA; static NSMutableArray * allAccountsB; static int64_t getRandom ( const uint64_t min, const uint64_t max ) { assert(min <= max); uint64_t rnd = arc4random(); // arc4random() returns a 32 bit value only rnd = (rnd << 32) | arc4random(); rnd = rnd % ((max + 1) - min); // Trim it to range return (rnd + min); // Lift it up to min value } static void createAccounts ( const NSUInteger ammount ) { NSArray *const maleNames = @[ @"Noah", @"Liam", @"Mason", @"Jacob", @"William", @"Ethan", @"Michael", @"Alexander", @"James", @"Daniel" ]; NSArray *const femaleNames = @[ @"Emma", @"Olivia", @"Sophia", @"Isabella", @"Ava", @"Mia", @"Emily", @"Abigail", @"Madison", @"Charlotte" ]; const NSUInteger nameCount = maleNames.count; assert(maleNames.count == femaleNames.count); // Better be safe than sorry allAcocuntsA = [NSMutableArray arrayWithCapacity:ammount]; allAccountsB = [NSMutableArray arrayWithCapacity:ammount]; for (uint64_t i = 0; i < ammount; i++) { const Gender g = (getRandom(0, 1) == 0 ? GenderMale : GenderFemale); const unsigned age = (unsigned)getRandom(18, 120); const int64_t balance = (int64_t)getRandom(0, 200000000) - 100000000; NSArray *const nameArray = (g == GenderMale ? maleNames : femaleNames); const NSUInteger nameIndex = (NSUInteger)getRandom(0, nameCount - 1); NSString *const name = nameArray[nameIndex]; AccountA *const accountA = [[AccountA alloc] initWithName:name age:age gender:g balance:balance ]; AccountB *const accountB = [[AccountB alloc] initWithName:name age:age gender:g balance:balance ]; [allAcocuntsA addObject:accountA]; [allAccountsB addObject:accountB]; } } int main(int argc, const char * argv[]) { @autoreleasepool { @autoreleasepool { NSUInteger ammount = 1000000; // 1 Million; if (argc > 1) { unsigned long long temp = 0; if (1 == sscanf(argv[1], "%llu", &temp)) { // NSUIntegerMax may just be UINT32_MAX! ammount = (NSUInteger)MIN(temp, NSUIntegerMax); } } createAccounts(ammount); } // Sort A and take time const CFAbsoluteTime startTime1 = CFAbsoluteTimeGetCurrent(); @autoreleasepool { [allAcocuntsA sortedArrayUsingSelector:@selector(compare:)]; } const CFAbsoluteTime runTime1 = CFAbsoluteTimeGetCurrent() - startTime1; // Sort B and take time const CFAbsoluteTime startTime2 = CFAbsoluteTimeGetCurrent(); @autoreleasepool { [allAccountsB sortedArrayUsingSelector:@selector(compare:)]; } const CFAbsoluteTime runTime2 = CFAbsoluteTimeGetCurrent() - startTime2; NSLog(@"runTime 1: %f", runTime1); NSLog(@"runTime 2: %f", runTime2); } return 0; } @implementation AccountA - (NSComparisonResult)compare:(nonnull AccountA *const)account { // Sort by gender first! Females prior to males. if (self.gender != account.gender) { if (self.gender == GenderFemale) return NSOrderedAscending; return NSOrderedDescending; } // Otherwise sort by name if (![self.name isEqualToString:account.name]) { return [self.name compare:account.name]; } // Otherwise sort by age, young to old if (self.age != account.age) { if (self.age < account.age) return NSOrderedAscending; return NSOrderedDescending; } // Last ressort, sort by balance, low to high if (self.balance != account.balance) { if (self.balance < account.balance) return NSOrderedAscending; return NSOrderedDescending; } // If we get here, the are really equal! return NSOrderedSame; } - (nonnull instancetype)initWithName:(nonnull NSString *const)name age:(const unsigned)age gender:(const Gender)gender balance:(const int64_t)balance { self = [super init]; assert(self); // We promissed to never return nil! _age = age; _gender = gender; _balance = balance; _name = [name copy]; return self; } @end @implementation AccountB - (NSComparisonResult)compare:(nonnull AccountA *const)account { // Sort by gender first! Females prior to males. if (_gender != account.gender) { if (_gender == GenderFemale) return NSOrderedAscending; return NSOrderedDescending; } // Otherwise sort by name if (![_name isEqualToString:account.name]) { return [_name compare:account.name]; } // Otherwise sort by age, young to old if (_age != account.age) { if (_age < account.age) return NSOrderedAscending; return NSOrderedDescending; } // Last ressort, sort by balance, low to high if (_balance != account.balance) { if (_balance < account.balance) return NSOrderedAscending; return NSOrderedDescending; } // If we get here, the are really equal! return NSOrderedSame; } - (nonnull instancetype)initWithName:(nonnull NSString *const)name age:(const unsigned)age gender:(const Gender)gender balance:(const int64_t)balance { self = [super init]; assert(self); // We promissed to never return nil! _age = age; _gender = gender; _balance = balance; _name = [name copy]; return self; } @end


La compatibilidad con versiones anteriores fue un factor para mí. No pude usar ninguna característica de Objective-C 2.0 porque estaba desarrollando software y controladores de impresora que tenían que funcionar en Mac OS X 10.3 como parte de un requisito. Sé que su pregunta parecía estar dirigida a iOS, pero pensé que seguiría compartiendo mis razones para no usar las propiedades.


Semántica

  • Lo que @property puede expresar que ivars no puede: no nonatomic y copy .
  • Lo que ivars puede expresar que @property no puede:
    • @protected : público en subclases, privado afuera.
    • @package : público en marcos en 64 bits, privado afuera. Lo mismo que @public en 32 bits. Consulte el Control de acceso de clase y instancia de 64 bits de Apple.
    • Calificadores Por ejemplo, matrices de referencias de objetos fuertes: id __strong *_objs .

Actuación

Cuento corto: los ivars son más rápidos, pero no importa para la mayoría de los usos. nonatomic propiedades no nonatomic no usan bloqueos, pero el índice directo es más rápido porque omite la llamada de acceso. Para detalles, lea el siguiente email de lists.apple.com.

Subject: Re: when do you use properties vs. ivars? From: John McCall <email@hidden> Date: Sun, 17 Mar 2013 15:10:46 -0700

Las propiedades afectan el rendimiento de muchas maneras:

  1. Como ya se mencionó, enviar un mensaje para hacer una carga / almacenamiento es más lento que simplemente cargar / almacenar en línea .

  2. Enviar un mensaje para hacer una carga / almacenamiento también es un poco más código que debe mantenerse en i-cache: incluso si el getter / setter agrega cero instrucciones adicionales más allá de la carga / almacenamiento, habría una mitad sólida -Docena de instrucciones adicionales en la persona que llama para configurar el envío del mensaje y manejar el resultado.

  3. El envío de un mensaje obliga a mantener una entrada para ese selector en la caché del método , y esa memoria generalmente se queda en d-cache. Esto aumenta el tiempo de lanzamiento, aumenta el uso de memoria estática de su aplicación y hace que los cambios de contexto sean más dolorosos. Como el caché de método es específico de la clase dinámica de un objeto, este problema aumenta cuanto más utilice KVO en él.

  4. El envío de un mensaje obliga a todos los valores de la función a derramarse a la pila (o mantenerse en los registros de guardado de llamadas, lo que significa simplemente derramar en un momento diferente).

  5. Enviar un mensaje puede tener efectos secundarios arbitrarios y, por lo tanto,

    • obliga al compilador a restablecer todas sus suposiciones sobre la memoria no local
    • no puede ser izada, hundida, reordenada, fusionada o eliminada.

  6. En ARC, el resultado del envío de un mensaje siempre será retenido , ya sea por el destinatario o por la persona que llama, incluso para devoluciones de +0: incluso si el método no retiene / libera el resultado, la persona que llama no lo sabe y tiene para tratar de tomar medidas para evitar que el resultado sea lanzado de forma automática. Esto nunca puede eliminarse porque los envíos de mensajes no son estadísticamente analizables.

  7. En ARC, como un método setter generalmente toma su argumento en +0, no hay forma de "transferir" un retenido de ese objeto (que, como se discutió anteriormente, ARC usualmente tiene) en el ivar, por lo que el valor generalmente tiene que obtenerse retener / liberar dos veces .

Nada de esto significa que siempre son malas, por supuesto, hay muchas buenas razones para usar propiedades. Solo tenga en cuenta que, como muchas otras características del lenguaje, no son gratuitas.


John.