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

时间:2013-09-28 11:58:27

标签: objective-c cocoa-touch cocoa nserror

许多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方法创建的错误对象,我的子类不会#39}无论如何都要用。我考虑过使用方法调整来使所有 NSError自己在dealloc上自行登录,但我不确定这是否真的可取。我想过使用某种观察者类来监视内存中给定的常量空间,我可以将其用于我想记录的NSError指针,但据我所知,没有办法做像KVO这样可以观察内存中的任意空格,所以我看不到实现这个的方法,而不是让一个线程反复检查要记录的错误。

有人能看到实现这个目标的方法吗?

4 个答案:

答案 0 :(得分:1)

调用-[NSError dealloc]来记录错误的一个问题是你仍然必须将指针传递给NSError,否则无法保证将永远创建错误。例如,似乎有可能实现各种框架方法:

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

你可以制作一个全局指针,比如说:

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

...并在您的前缀标头中将其声明为extern,然后将ignoredErrorPtr传递给您不想出现错误的任何方法,但之后就会失去任何地方发生错误(实际上这只有在你使用ARC时才能正常工作)。

我觉得你真正想要做的就是调整指定的初始值设定项(或allocWithZone: dealloc,然后在调整/包裹的方法中调用{ {1}}并使用[NSThread callStackSymbols]将返回的数组(或者可能是-description)附加到NSError实例。然后在你的混合objc_setAssociatedObject中你可以记录错误本身它产生的调用堆栈。

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

你可以这样做:

NULL

请注意,这将记录所有错误,而不仅仅是您未处理的错误。这可能是可接受的,也可能是不可接受的,但我很难想出一种方法来区分事实,而不是在处理错误的任何地方进行调用。

答案 1 :(得分: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,但我不确定这是否真的是可取的。

这不可取。

答案 2 :(得分: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的详细程度是有意的,特别是在增加阅读和维护代码的难易程度方面,因此更好的解决方案可能是制作custom Xcode snippet。它的写入速度不会像使用上面的宏那样快(但是使用键盘快捷键和自动完成功能它仍然会非常快),但对于未来的读者来说,它将是绝对清晰的。您可以将以下文本拖到代码段库中,并为其定义完成快捷方式。

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

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

答案 3 :(得分:0)

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

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

-(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中的一个约定是不使用错误指针本身来检测是否存在错误,而是使用返回值。