7

许多 Cocoa 方法采用一个可选NSError **参数,用于报告错误。我经常发现自己使用这样的方法,即使在可能发生错误的唯一方法是通过我的编程错误而不是意外的运行时条件的情况下。因此,我不想编写任何错误处理代码来执行任何用户可见的事情。我真正想做的就是记录错误(可能还有崩溃),同时让我的代码尽可能简洁易读。

问题在于“保持代码简洁”和“记录错误”目标相互矛盾。我经常在这两种方法之间做出选择,我都不喜欢这两种方法:

1. 为错误指针参数传递 NULL。

[managedObjectContext save:NULL];
  • 优点:简洁、易读,并且清楚地表明不会出现错误。只要我正确地相信这里的错误在逻辑上是不可能的,那就完全没问题了。
  • 缺点:如果我搞砸了并且确实发生了错误,它不会被记录下来,我的调试会更加困难。在某些情况下,我什至可能没有注意到错误发生了。

NSError **2.每次都使用相同的样板代码传递并记录产生的错误。

NSError *error;
[managedObjectContext save:&error];
if (error) {
    NSLog(@"Error while saving: %@", error);
}
  • 优点:错误不会静默传递——我会收到警告并提供调试信息。
  • 缺点:它非常冗长。它写起来更慢,读起来更慢,而且当它已经嵌套在某些级别的缩进中时,我觉得它会使代码的可读性降低。通常这样做只是为了记录错误,并且习惯于在阅读时跳过样板文件,这也让我有时无法注意到我正在阅读的某些代码实际上对预期在运行时发生的错误有重要的错误处理块。

来自 Python、Java、PHP 和 Javascript 等语言的背景,我发现不得不编写 4 行额外的样板代码来获得通知类型的错误有点麻烦,在我习惯的语言中,我可以通过异常或警告找出相关信息,而无需编写任何明确检查错误的代码。

我最喜欢的是一些巧妙的 hack,我可以使用它来自动记录这些方法创建的错误,而无需在每个方法调用上编写样板,从而为我提供惰性 NULL 传递方法和错误的好处-记录样板。换句话说,我想这样写:

[managedObjectContext save:&magicAutologgingError];

并且知道如果该方法创建了一个NSError,它会以某种方式神奇地被记录下来。

我不太确定该怎么做。我考虑过使用一个NSError记录自身的子类dealloc,但意识到由于我不负责实例化 Cocoa 的方法创建的错误对象,所以无论如何我的子类都不会被使用。我考虑过使用方法调配让所有 NSErrors 都像这样登录dealloc,但我不确定这是否真的可取。我考虑过使用某种观察者类来监视内存中给定的常量空间,我可以将其用于NSError要记录的指针,但据我所知,没有办法像 KVO 那样观察内存中的任意空间,所以除了有一个重复检查要记录的错误的线程之外,我看不到实现这一点的方法。

谁能看到实现这一目标的方法?

4

4 回答 4

1

swizzling-[NSError dealloc]记录错误的一个问题是您仍然必须传递一个指向 NSError 的指针,否则无法保证永远会创建错误。例如,似乎可以像这样实现各种框架方法:

if (outError)
{
    *outError = [[[NSError alloc] init] autorelease]; // or whatever.
}

您可以制作一个全局指针,例如:

NSError* gErrorIDontCareAbout = nil; 
NSError** const ignoredErrorPtr = &gErrorIDontCareAbout;

...并将其声明为extern在您的前缀标头中,然后传递ignoredErrorPtr给您不想呈现其错误的任何方法,但是您会丢失任何发生错误的位置(实际上,这只有在您使用弧)。

我突然想到,您真正想要做的是调配指定的初始化程序(或allocWithZone: 并在该调配dealloc/包装的方法中,使用. 然后在您的 swizzled 中,您可以记录错误本身以及它起源的调用堆栈。[NSThread callStackSymbols]-descriptionobjc_setAssociatedObject-dealloc

但是不管你怎么做,我认为如果你只是通过NULL,你不会得到任何有用的东西,因为如果你告诉他们你对它不感兴趣,框架可以自由地不首先创建 NSError(通过通过NULL)。

你可以这样做:

@implementation MyAppDelegate

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Stash away the callstack
        IMP originalIMP = class_getMethodImplementation([NSError class], @selector(initWithDomain:code:userInfo:));
        IMP newIMP = imp_implementationWithBlock(^id(id self, NSString* domain, NSInteger code, NSDictionary* dict){
            self = originalIMP(self, @selector(initWithDomain:code:userInfo:), domain, code, dict);
            NSString* logString = [NSString stringWithFormat: @"%@ Call Stack: \n%@", self, [NSThread callStackSymbols]];
            objc_setAssociatedObject(self, &onceToken, logString, OBJC_ASSOCIATION_RETAIN);
            return self;
        });
        method_setImplementation(class_getInstanceMethod([NSError class], @selector(initWithDomain:code:userInfo:)), newIMP);

        // Then on dealloc... (Note: this assumes that NSError implements -dealloc. To be safer you would want to double check that.)
        SEL deallocSelector = NSSelectorFromString(@"dealloc"); // STFU ARC
        IMP originalDealloc = class_getMethodImplementation([NSError class], deallocSelector);
        IMP newDealloc = imp_implementationWithBlock(^void(id self){
            NSString* logString = objc_getAssociatedObject(self, &onceToken);
            if (logString.length) NSLog(@"Logged error: %@", logString);
            originalDealloc(self, deallocSelector); // STFU ARC
        });
        method_setImplementation(class_getInstanceMethod([NSError class], deallocSelector), newDealloc);
    });
}

@end

请注意,这将记录所有错误,而不仅仅是您未处理的错误。这可能可以接受,也可能不可以接受,但我正在努力想办法在事后做出区分,而不是在你处理错误的任何地方都打电话

于 2013-09-28T13:45:25.603 回答
1

只需创建一个包装函数(或类别方法),它可以满足您的要求:

bool MONSaveManagedObjectContext(NSManagedObjectContext * pContext) {
 NSError * error = nil;
 bool result = [pContext save:&error];
 if (!result && nil != error) {
  // handle  the error how you like -- may be different in debug/release
  NSLog(@"Error while saving: %@", error);
 }
 return result;
}

而是调用它。或者您可能更喜欢将错误处理分开:

void MONCheckError(NSError * pError, NSString * pMessage) {
 if (nil != pError) {
  // handle  the error how you like -- may be different in debug/release
  NSLog(@"%@: %@", pMessage, pError);
 }
}

...

NSError * outError = nil;
bool result = [managedObjectContext save:&outError];
MONCheckError(outError, @"Error while saving");

始终注意重复代码:)


我考虑过使用方法 swizzling 让所有 NSErrors 像这样在 dealloc 上记录自己,但我不确定这是否真的是可取的。

这是不可取的。

于 2013-09-28T17:38:22.230 回答
1

一种方法是您可以定义一个带有NSError **参数的块,将产生错误的表达式放在这些块中(将块参数作为错误参数传递给代码),然后编写一个执行该类型块的函数,传递和记录错误参考。例如:

// Definitions of block type and function
typedef void(^ErrorLoggingBlock)(NSError **errorReference);

void ExecuteErrorLoggingBlock(ErrorLoggingBlock block)
{
    NSError *error = nil;
    block(&error);
    if (error) {
        NSLog(@"error = %@", error);
    }
}

...

// Usage:
__block NSData *data1 = nil;
ErrorLoggingBlock block1 = ^(NSError **errorReference) {
    data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.google.com"] options:0 error:errorReference];
};
__block NSData *data2 = nil;
ErrorLoggingBlock block2 = ^(NSError **errorReference) {
    data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://wwwwwlskjdlsdkjk.dsldksjl.sll"] options:0 error:errorReference];
};

ExecuteErrorLoggingBlock(block1);
ExecuteErrorLoggingBlock(block2);

NSLog(@"data1 = %@", data1);
NSLog(@"data2 = %@", data2);

如果这仍然太冗长,您可能会考虑一些预处理器宏或一些 Xcode 代码片段。我发现以下一组宏具有相当的弹性:

#define LazyErrorConcatenatePaste(a,b) a##b
#define LazyErrorConcatenate(a,b) LazyErrorConcatenatePaste(a,b)
#define LazyErrorName LazyErrorConcatenate(lazyError,__LINE__)
#define LazyErrorLogExpression(expr) NSError *LazyErrorName; expr; if (LazyErrorName) { NSLog(@"error: %@", LazyErrorName);}

用法是这样的:

LazyErrorLogExpression(NSData *data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://google.com"]
                                                             options:0
                                                               error:&LazyErrorName])
NSLog(@"data1 = %@", data1);
LazyErrorLogExpression(NSData *data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://sdlkdslkdsslk.alskdj"]
                                                             options:0
                                                               error:&LazyErrorName])
NSLog(@"data2 = %@", data2);

该宏使错误变量名称唯一,并且可以抵抗方法调用换行符。如果您真的坚持在源文件中没有任何额外的代码行,那么这可能是最安全的方法 - 它至少不涉及抛出异常异常或调动框架方法,并且您始终可以查看预处理助手编辑器中的代码。

然而,我认为 Objective-C 的冗长是有意的,特别是在增加阅读和维护代码的便利性方面,因此更好的解决方案可能是制作自定义 Xcode 片段。编写起来不会像使用上面的宏那样快(但使用键盘快捷键和自动完成仍然会非常快),但对于未来的读者来说绝对清楚。您可以将以下文本拖动到代码段库并为其定义完成快捷方式。

NSError *<#errorName#>;
<#expression_writing_back_error#>;
if (<#errorName#>) {
    NSLog(@"error: %@", <#errorName#>);
}

最后一个免责声明:这些模式应该只用于日志记录,返回值应该在错误恢复的情况下实际确定成功或失败。虽然,如果需要一些常见的错误恢复代码,可以很容易地使基于块的方法返回一个指示成功或失败的布尔值。

于 2013-09-28T18:54:13.187 回答
0

如果发生的错误确实只能由编程错误引起,我会抛出异常,因为您可能希望程序无论如何都停止,

我所做的是有一个异常类,它将错误作为其初始化中的参数。然后,您可以使用错误中的内容填充异常信息,例如

-(id) initWithError: (NSError*) error
{
    NSString* name = [NSString stringWithFormat: @"%@:%ld", [error domain], (long)[error code]];
   self = [super initWithName: name
                       reason: [error localizedDescriptionKey]
                     userInfo: [error userInfo]];
   if (self != nil)
   {
       _error = error;
   }
   return self;
}

然后您还可以覆盖-description以打印出一些相关的错误信息。

正确的使用方法是

NSError *error = nil;
if (![managedObject save:&error])
{
    @throw [[ErrorException alloc] initWithError: error];
}

请注意,顺便说一句,我通过测试发送消息的结果而不是通过查看错误是否为零来检测错误。Objective-C 中的一个约定是不使用错误指针本身来检测是否有错误,而是使用返回值。

于 2013-09-28T12:48:08.483 回答