Objective-C中记录错误的更简洁方式

7
许多Cocoa方法都带有一个可选的NSError **参数,用于报告错误。经常情况下,即使在没有意外运行时条件的情况下,我也会使用这些方法。因此,我不想编写任何用户可见的错误处理代码。在出现错误时,我真正想做的就是记录错误(可能会崩溃),同时使我的代码简洁易读。
问题在于“保持代码简洁”和“记录错误”的目标相互矛盾。我经常需要在这两种方法之间做出选择,但我都不喜欢: 1. 传递NULL作为错误指针参数。
[managedObjectContext save:NULL];
  • 优点:简洁易读,清晰明了地表明不会出现错误。只要我正确地认为这里不可能出现错误,那么这种方式就可以完全接受。
  • 缺点:如果我搞砸了,出现了错误,那么它不会被记录下来,我的调试工作将会更加困难。在某些情况下,我甚至可能都不会注意到发生了错误。

2. 传递一个 NSError ** 并使用相同的样板代码记录结果错误,每次都要这样做。

NSError *error;
[managedObjectContext save:&error];
if (error) {
    NSLog(@"Error while saving: %@", error);
}
  • 优点:错误不会被默默地忽略 - 我会收到警报并获得调试信息。
  • 缺点:它非常冗长。编写和阅读速度较慢,当它嵌套在某些缩进级别中时,我觉得它使代码变得更难以阅读。经常仅为记录错误而这样做,并习惯于在阅读时跳过样板文件,有时也会导致我没有注意到我正在阅读的某些代码实际上具有针对运行时可能出现的重要错误处理块。

从像Python、Java、PHP和Javascript这样的语言背景中来看,我觉得必须编写4行额外的样板文件才能通知我关于错误类型的信息,而在我习惯的语言中,我可以通过异常或警告了解到这些错误,而无需编写任何显式检查错误的代码。

我理想的情况是有一些巧妙的技巧,我可以使用它们自动记录这些方法创建的错误,而无需在每个方法调用中编写样板文件,从而给我既懒惰的NULL传递方法的好处,又有错误日志样板文件的好处。换句话说,我想写这个:

[managedObjectContext save:&magicAutologgingError];

需要知道,如果这个方法创建了一个NSError对象,那么它会以某种神奇的方式被日志记录。

我不太确定该如何处理。我考虑使用一个NSError的子类,在dealloc时自动记录日志,但我意识到,由于我不负责实例化Cocoa方法创建的错误对象,我的子类也不会被使用。我考虑使用方法交换来使所有NSErrordealloc时自动记录日志,但我不确定是否真的需要这样做。我想过使用一些观察者类来监视我要记录日志的NSError指针所在的内存地址,但据我所知,没有办法像KVO那样观察内存中任意的空间,因此我没有其他实现方法,只能使用一个线程反复检查要记录的错误。

有人能看出实现的方法吗?


1
我觉得你说“唯一出错的方式是由于我的编程错误”很奇怪。在Cocoa中,异常在这种情况下使用,而不是错误。在你的特定示例中,如果用户决定将Core Data存储放在无法访问的远程位置,那么-save:可能会返回NO - user557219
1
就此而言,传递 NULL 并不是“明确表示不期望出现错误”,而是表明您并不关心发生的任何错误。 - ipmcc
@Bavarious 我想要表达的意思是,尽管从理论上讲,在调用特定方法时可能会出现由于预期的运行时条件而导致的错误,而不是程序员的错误,但这并不意味着在你的特定程序中,使用该方法的特定调用存在任何这样的可能性。也许我表达得不太好。 - Mark Amery
2
请注意,if (error) { 是不正确的写法;您不能测试 error 来知道是否生成了一个 error。您举例的 NSRegularExpression 稍有出入;该 API 定义错误输入是可恢复的,无论它是否是静态的。因此,它使用 NSError,因为这些封装可恢复的错误,而 NSException 应始终被视为致命的程序员错误。 - bbum
1
@bbum,谢谢你提到if (error)的注意事项。实际上,在我发布这个问题后阅读其他内容时,我看到了在其他地方提出了这个观点,JeremyP在他的答案中也提出了同样的观点。(尽管如此,一些方法文档 - 如NSRegularExpression的init和构造方法 - 指定nil错误表示成功,并且没有指定什么返回值表示失败。您确定您在这里所断言的仍然是Cocoa中普遍适用的吗?) - Mark Amery
显示剩余4条评论
4个回答

1
使用swizzling来记录错误的一个问题是,你仍然需要传递一个指向NSError的指针,否则不能保证错误会被创建。例如,各种框架方法可能是这样实现的:
if (outError)
{
    *outError = [[[NSError alloc] init] autorelease]; // or whatever.
}

你可以创建一个全局指针,例如:

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

...并在您的前缀头中将其声明为extern,然后将ignoredErrorPtr传递给任何您不关心呈现错误的方法,但是这样会失去任何有关错误发生位置的局部性(如果您使用ARC,则确实只能正确工作)。

我想到了一个方法,您真正想要做的是调换指定的初始化程序(或allocWithZone:dealloc,并在该交换/包装方法中调用[NSThread callStackSymbols],并使用objc_setAssociatedObject将返回的数组(或者可能是其-description)附加到NSError实例上。然后在您的交换-dealloc中,您可以记录错误本身它起源的调用堆栈。

但无论如何,如果您只传递NULL,我认为您无法获得任何有用的东西,因为如果您告诉它们您对此不感兴趣(通过传递NULL),则框架可以选择不首先创建NSError。

您可以像这样操作:

@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

请注意,这将记录所有错误,而不仅仅是您未处理的错误。这可能是可以接受的,也可能不可接受,但我很难想出一种方法来在事后区分它们,而不需要在您处理错误的每个位置都进行某些调用。

刚刚花时间仔细看了一下这个问题(抱歉 - 我以前没有玩过swizzling,这对我来说太难了,需要一些时间坐下来思考和尝试)。它似乎几乎可以工作(记录了它应该记录的内容!),但是每当调用originalDealloc(self, deallocSelector);时就会崩溃,并且我不知道为什么。有什么想法吗? - Mark Amery

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");

始终注意重复的代码 :)


我考虑使用方法交换来使所有的 NSErrors 在 dealloc 时记录自己的日志,但我不确定这是否真的是可取的。

这并不可取。


为每个带有错误指针参数的Cocoa方法创建包装器似乎对我所处理的项目规模来说有些过度设计(尽管如果有提供这些包装器的库,我会使用它)。然而,拥有某种“errorLog”函数,可以记录非nil错误并在nil错误时不执行任何操作,可能是一种方便且简单的方式来减少样板代码(尽管@bbum可能会反对此做法,因为对于许多方法而言,非nil错误不能保证表示发生了错误)。 - Mark Amery
1
@MarkAmery,它不必是每个API(而且,如果您选择类别,那将是巨大的)。只需在需要时添加它们-包装所需的额外行数在您使用该函数作为替代品2或3次时就会得到回报。 - justin

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);}

Usage was like this:

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#>);
}

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


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];
}

顺便提一下,我通过测试发送消息的结果来检测错误,而不是查看错误是否为nil。在Objective-C中,惯例是不使用错误指针本身来检测是否存在错误,而是使用返回值。

@Mark Amery,不测试错误参数以查看错误是否存在已经成为一种惯例,这是因为无法保证在正确执行时错误参数将为nil。在方法执行期间,有可能将错误参数设置为某个值,而最终并没有发生错误。 - zaph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接