繁体   English   中英

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

[英]Less verbose way to log errors in Objective-C

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

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

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

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

你可以做一个全局指针,说:

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

...并在你的前缀头中声明它为extern ,然后将ignoredErrorPtr传递给你不想出现错误的任何方法,但是你会丢失任何错误发生位置的地方(实际上这只会正常工作)如果你使用ARC)。

在我看来,你真正想做的就是调整指定的初始化程序(或allocWithZone: dealloc并在那个混合/包装方法中,调用[NSThread callStackSymbols]并将返回的数组(或者它的-description )附加到使用objc_setAssociatedObject NSError实例。 然后在你的swizzled -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

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

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

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上自己登录,但我不确定这是否真的是可取的。

这是不可取的。

一种方法是你可以定义一个接受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#>);
}

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

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

我所做的是有一个异常类,它将错误作为其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中的一个约定是不使用错误指针本身来检测是否存在错误,而是使用返回值。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM