objective-c - infusion - kvo español
Mejores prácticas para el parámetro de contexto en addObserver(KVO) (3)
Me preguntaba qué debería establecer el puntero de contexto en KVO cuando está observando una propiedad. Estoy empezando a usar KVO y no he extraído demasiado de la documentación. Veo en esta página: http://www.jakeri.net/2009/12/custom-callout-bubble-in-mkmapview-final-solution/ el autor hace esto:
[annView addObserver:self
forKeyPath:@"selected"
options:NSKeyValueObservingOptionNew
context:GMAP_ANNOTATION_SELECTED];
Y luego en la devolución de llamada, hace esto:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context{
NSString *action = (NSString*)context;
if([action isEqualToString:GMAP_ANNOTATION_SELECTED]){
Supongo que en este escenario, el autor simplemente crea una cadena que se identificará más adelante en la devolución de llamada.
Luego en el libro iOS 5 Pushing the Limits, veo que hace esto:
[self.target addObserf:self forKeyPath:self.property options:0 context:(__bridge void *)self];
llamar de vuelta:
if ((__bridge id)context == self) {
}
else {
[super observeValueForKeyPath .......];
}
Me preguntaba si hay un estándar o mejores prácticas para pasar al puntero de contexto.
Creo que la mejor manera sería implementarlo como dice el doc. De Apple:
La dirección de una variable estática con nombre único dentro de su clase constituye un buen contexto.
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
ver documentation
El contexto KVO debe ser un puntero a una variable estática, como lo demuestra esta esencia . Típicamente, me encuentro haciendo lo siguiente:
Cerca de la parte superior de mi archivo ClassName.m
tendré la línea
static char ClassNameKVOContext = 0;
Cuando comience a observar la propiedad de aspect
en targetObject
(una instancia de TargetClass
) tendré
[targetObject addObserver:self
forKeyPath:PFXKeyTargetClassAspect
options://...
context:&ClassNameKVOContext];
donde PFXKeyTargetClassAspect es un NSString *
definido en TargetClass.m
para ser igual a @"aspect"
y declarado extern
en TargetClass.h
. (Por supuesto, PFX es solo un marcador de posición para el prefijo que estás usando en tu proyecto). Esto me da la ventaja de autocompletar y me protege de errores tipográficos.
Cuando termine de observar el aspect
en targetObject
tendré
[targetObject removeObserver:self
forKeyPath:PFXKeyTargetClassAspect
context:&ClassNameKVOContext];
Para evitar demasiada sangría en mi implementación de -observeValueForKeyPath:ofObject:change:context:
me gusta escribir
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context != &ClassNameKVOContext) {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if ([object isEqual:targetObject]) {
if ([keyPath isEqualToString:PFXKeyTargetClassAspect]) {
//targetObject has changed the value for the key @"aspect".
//do something about it
}
}
}
Lo importante es (en términos generales) que uses algo (en lugar de nada) y que lo que uses sea único y privado al usarlo .
La falla principal aquí ocurre cuando tiene una observación en una de sus clases, y luego alguien asigna subclases a su clase, y agregan otra observación del mismo objeto observado y la misma ruta de acceso. Si su observeValueForKeyPath:...
original observeValueForKeyPath:...
solo verificó keyPath
, o el object
observado, o incluso ambos, eso podría no ser suficiente para saber que se está devolviendo la observación. El uso de un context
cuyo valor es único y privado para usted le permite estar mucho más seguro de que una llamada determinada para observeValueForKeyPath:...
es la llamada que espera que sea.
Esto importaría si, por ejemplo, se registró solo para didChange
notificaciones de didChange
, pero una subclase se registra para el mismo objeto y keyPath con la opción NSKeyValueObservingOptionPrior
. Si no estuviera filtrando llamadas para observeValueForKeyPath:...
usando un context
(o verificando el diccionario de cambios), su controlador se ejecutaría varias veces, cuando solo esperaba que se ejecutara una vez. No es difícil imaginar cómo esto podría causar problemas.
El patrón que uso es:
static void * const MyClassKVOContext = (void*)&MyClassKVOContext;
Este puntero apuntará a su propia ubicación, y esa ubicación es única (ninguna otra variable estática o global puede tener esta dirección, ni ningún objeto de pila o pila asignado puede tener esta dirección; es bastante fuerte, aunque es cierto que no es absoluto . Garantía), gracias al enlazador. La const
hace para que el compilador nos avise si alguna vez intentamos escribir código que cambie el valor del puntero y, por último, la static
hace que sea privado para este archivo, de modo que nadie fuera de este archivo pueda obtener una referencia al mismo. (de nuevo, haciendo que sea más probable evitar las colisiones).
Un patrón que advierto específicamente contra el uso es el que apareció en la pregunta:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSString *action = (NSString*)context;
if([action isEqualToString:GMAP_ANNOTATION_SELECTED]) {
se declara que el context
es un void*
, lo que significa que esa es toda la garantía que se puede ofrecer sobre lo que es. Al NSString*
a una NSString*
estás abriendo una gran caja de maldad potencial. Si resulta que alguien más tiene un registro que no usa un NSString*
para el parámetro de context
, este enfoque se bloqueará cuando pase el valor de no objeto a isEqualToString:
La igualdad del puntero (o, alternativamente, la igualdad intptr_t
o uintptr_t
) son las únicas comprobaciones seguras que se pueden utilizar con un valor de context
.
Usar el self
como context
es un enfoque común. Es mejor que nada, pero tiene uniquing y privacidad mucho más débiles, ya que otros objetos (sin mencionar las subclases) tienen acceso al valor de self
y podrían usarlo como context
(causando ambigüedad), a diferencia del enfoque que sugerí anteriormente.
También recuerde, no solo las subclases pueden causar escollos aquí; Aunque podría decirse que es un patrón raro, no hay nada que impida que otro objeto registre su objeto para nuevas observaciones KVO.
Para mejorar la legibilidad, también podría envolver esto en una macro de preprocesador como:
#define MyKVOContext(A) static void * const A = (void*)&A;