objective c - Objective-C y KeyValueCoding: cómo evitar una excepción con valueForKeyPath:?
ios5 kvc (6)
Tengo un objeto de id
de tipo y me gustaría saber si contiene un valor para una keyPath
:
[myObject valueForKeyPath:myKeyPath];
Ahora, lo @try{ } @catch{}
en un @try{ } @catch{}
para evitar excepciones cuando no se encuentra el keypath dado. ¿Hay alguna manera más agradable de hacer esto? Verifique si el keypath dado existe sin manejar excepciones.
Muchas gracias,
Stefan
Si el objeto es una clase personalizada suya, puede anular valueForUndefinedKey:
en su objeto, para definir lo que se devuelve cuando un keypath no existe.
Puedes intentar esto:
if ([myObject respondsToSelector:NSSelectorFromString(myKeyPath)])
{
}
Sin embargo, es posible que no corresponda al getter que tiene, especialmente si es un valor booleano. Si esto no funciona para usted, avíseme y le escribiré algo usando reflexión.
Debería ser posible injertar este comportamiento en clases arbitrarias de forma razonablemente simple. Presento con confianza, pero sin garantía, el siguiente código que usted debería poder usar para agregar una implementación que no valueForUndefinedKey:
excepciones de valueForUndefinedKey:
a cualquier clase, con una línea centralizada de código por clase al momento de inicio de la aplicación. Si desea guardar aún más código, puede hacer que todas las clases que desea tener hereden este comportamiento de una subclase común de NSManagedObject y luego aplicar esto a esa clase común y todas sus subclases heredarán el comportamiento. Más detalles después, pero aquí está el código:
Encabezado ( NSObject+ValueForUndefinedKeyAdding.h
):
@interface NSObject (ValueForUndefinedKeyAdding)
+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler;
@end
Implementación ( NSObject+ValueForUndefinedKeyAdding.m
):
#import "NSObject+ValueForUndefinedKeyAdding.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation NSObject (ValueForUndefinedKeyAdding)
+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler
{
Class clazz = self;
if (clazz == nil)
return;
if (clazz == [NSObject class] || clazz == [NSManagedObject class])
{
NSLog(@"Don''t try to do this to %@; Really.", NSStringFromClass(clazz));
return;
}
SEL vfuk = @selector(valueForUndefinedKey:);
@synchronized([NSObject class])
{
Method nsoMethod = class_getInstanceMethod([NSObject class], vfuk);
Method nsmoMethod = class_getInstanceMethod([NSManagedObject class], vfuk);
Method origMethod = class_getInstanceMethod(clazz, vfuk);
if (origMethod != nsoMethod && origMethod != nsmoMethod)
{
NSLog(@"%@ already has a custom %@ implementation. Replacing that would likely break stuff.",
NSStringFromClass(clazz), NSStringFromSelector(vfuk));
return;
}
if(!class_addMethod(clazz, vfuk, handler, method_getTypeEncoding(nsoMethod)))
{
NSLog(@"Could not add valueForUndefinedKey: method to class: %@", NSStringFromClass(clazz));
}
}
}
@end
Luego, en su clase AppDelegate (o en cualquier lugar, pero probablemente tenga sentido colocarlo en algún lugar central, para que sepa dónde encontrarlo cuando quiera agregar o eliminar clases de la lista) ponga este código que agrega esta funcionalidad a las clases de su elección en el momento del inicio:
#import "MyAppDelegate.h"
#import "NSObject+ValueForUndefinedKeyAdding.h"
#import "MyOtherClass1.h"
#import "MyOtherClass2.h"
#import "MyOtherClass3.h"
static id ExceptionlessVFUKIMP(id self, SEL cmd, NSString* inKey)
{
NSLog(@"Not throwing an exception for undefined key: %@ on instance of %@", inKey, [self class]);
return nil;
}
@implementation MyAppDelegate
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[MyOtherClass1 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
[MyOtherClass2 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
[MyOtherClass3 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
});
}
// ... rest of app delegate class ...
@end
Lo que estoy haciendo aquí es agregar una implementación personalizada para valueForUndefinedKey:
a las clases MyOtherClass1, 2 y 3. La implementación de ejemplo He proporcionado solo NSLog
y devuelve nil, pero puede cambiar la implementación para hacer lo que quiera, por cambiando el código en ExceptionlessVFUKIMP
. Si elimina el NSLog
y solo devuelve cero, sospecho que obtendrá lo que desea, de acuerdo con su pregunta.
Este código NUNCA swizzles métodos, solo agrega uno si no está allí. He puesto comprobaciones para evitar que esto se use en clases que ya tienen sus propias implementaciones personalizadas de valueForUndefinedKey:
porque si alguien pone ese método en su clase, habrá una expectativa de que seguirá llamándose. También tenga en cuenta que puede haber código de AppKit que EXPECTA las excepciones de las implementaciones NSObject / NSManagedObject que se lanzarán. (No lo sé con certeza, pero es una posibilidad para considerar).
Algunas notas:
NSManagedObject
proporciona una implementación personalizada para valueForUndefinedKey:
por su ensamblado en el depurador, todo lo que parece hacer es lanzar aproximadamente la misma excepción con un mensaje ligeramente diferente. Sobre la base de esa investigación de depuración de 5 minutos, creo que debería ser seguro usar esto con NSManagedObject
subclases NSManagedObject
, pero no estoy 100% seguro, podría haber algún comportamiento allí que no pude detectar. Tener cuidado.
Además, tal como está, si usa este enfoque, no tiene una buena forma de saber si valueForKey:
devuelve nada porque el keyPath es válido y el estado pasó a ser nil
, o si devuelve nil
porque el keyPath es inválido y el controlador injertado devuelto nil. Para hacer eso, necesitarías hacer algo diferente, e implementación específica. (Tal vez devuelva [NSNull null]
o algún otro valor de centinela, o establezca alguna bandera en el almacenamiento local de subprocesos que pueda verificar, pero en este punto es realmente mucho más fácil que @try/@catch
?) Solo algo para ser conciente de.
Esto parece funcionar bastante bien para mí; Espero que sea útil para ti.
No creo que esto sea posible de una manera segura (es decir, sin -valueForUndefinedKey:
con -valueForUndefinedKey:
o algo similar en las clases de otras personas). Lo digo porque, en el lado Mac de las cosas, Cocoa Bindings, que se puede establecer para sustituir un valor predeterminado para las rutas clave no válidas, simplemente detecta las excepciones que resultan de las rutas clave erróneas. Si incluso los ingenieros de Apple no tienen una forma de comprobar si una ruta clave es válida sin probarla y sin detectar la excepción, tengo que suponer que esa forma no existe.
No hay una manera fácil de resolver esto. Key Value Coding (KVC) no está destinado a ser utilizado de esa manera.
Una cosa es segura: usar @try-@catch
es realmente malo, ya que es muy probable que se pierda la memoria, etc. Las excepciones en ObjC / iOS no están destinadas al flujo normal del programa. También son muy caros (lanzando y configurando el @try-@catch
IIRC).
Si mira el encabezado Foundation/NSKeyValueCoding.h
, el comentario / documentación de
- (id)valueForKey:(NSString *)key;
indica claramente qué métodos deben implementarse para -valueForKey:
para que funcionen. Esto incluso puede usar acceso directo a ivar. Debería verificar cada una en el orden descrito allí. Debes tomar la ruta clave, dividirla según .
y verifica cada parte en cada objeto subsiguiente. Para acceder a ivars, necesitas usar el tiempo de ejecución ObjC. Mira objc/runtime.h
.
Sin embargo, todo esto es diferente. Lo que probablemente desee es que sus objetos implementen algún protocolo formal y luego verifiquen -conformsToProtocol:
antes de llamar.
¿ Sus cadenas de teclas son cadenas aleatorias o esas cadenas están bajo su control? ¿Qué estás intentando lograr? ¿Estás resolviendo el problema equivocado?
Para NSManagedObjects
, una solución fácil es observar la descripción de la entidad del objeto y ver si hay un atributo con ese nombre de clave. Si lo hay, también puede llevarlo al siguiente paso y ver qué tipo de atributo es el valor.
Aquí hay un método simple que, dado cualquier NSManagedObject
y cualquier NSString
como clave, siempre devolverá un NSString
:
- (NSString *)valueOfItem:(NSManagedObject *)item asStringForKey:(NSString *)key {
NSEntityDescription *entity = [item entity];
NSDictionary *attributesByName = [entity attributesByName];
NSAttributeDescription *attribute = attributesByName[key];
if (!attribute) {
return @"---No Such Attribute Key---";
}
else if ([attribute attributeType] == NSUndefinedAttributeType) {
return @"---Undefined Attribute Type---";
}
else if ([attribute attributeType] == NSStringAttributeType) {
// return NSStrings as they are
return [item valueForKey:key];
}
else if ([attribute attributeType] < NSDateAttributeType) {
// this will be all of the NSNumber types
// return them as strings
return [[item valueForKey:key] stringValue];
}
// add more "else if" cases as desired for other types
else {
return @"---Unacceptable Attribute Type---";
}
}
Si la clave no es válida o el valor no se puede convertir en una cadena, el método devuelve un mensaje de error NSString
(cambie esos bloques para hacer lo que quiera para esos casos).
Todos los tipos de atributo NSNumber
se devuelven como sus representaciones stringValue
. Para manejar otros tipos de atributos (p. Ej .: fechas), simplemente agregue bloques adicionales "else if". (ver NSAttributeDescription
Class Reference para más información).