Jak mogę korzystać z NSError w mojej aplikacji na iPhone'a?

228

Pracuję nad wyłapywaniem błędów w mojej aplikacji i zamierzam używać NSError. Jestem nieco zdezorientowany, jak go używać i jak go wypełnić.

Czy ktoś mógłby podać przykład, w jaki sposób wypełnić, a następnie użyć NSError?

Nic Hubbard
źródło

Odpowiedzi:

473

Cóż, zwykle robię to, aby moje metody, które mogą powodować błędy w czasie wykonywania, odwołują się do NSErrorwskaźnika. Jeśli coś rzeczywiście pójdzie nie tak w tej metodzie, mogę wypełnić NSErrorodwołanie danymi błędu i zwrócić zero z metody.

Przykład:

- (id) endWorldHunger:(id)largeAmountsOfMonies error:(NSError**)error {
    // begin feeding the world's children...
    // it's all going well until....
    if (ohNoImOutOfMonies) {
        // sad, we can't solve world hunger, but we can let people know what went wrong!
        // init dictionary to be used to populate error object
        NSMutableDictionary* details = [NSMutableDictionary dictionary];
        [details setValue:@"ran out of money" forKey:NSLocalizedDescriptionKey];
        // populate the error object with the details
        *error = [NSError errorWithDomain:@"world" code:200 userInfo:details];
        // we couldn't feed the world's children...return nil..sniffle...sniffle
        return nil;
    }
    // wohoo! We fed the world's children. The world is now in lots of debt. But who cares? 
    return YES;
}

Następnie możemy użyć takiej metody. Nie zawracaj sobie nawet głowy sprawdzaniem obiektu błędu, chyba że metoda zwróci zero:

// initialize NSError object
NSError* error = nil;
// try to feed the world
id yayOrNay = [self endWorldHunger:smallAmountsOfMonies error:&error];
if (!yayOrNay) {
   // inspect error
   NSLog(@"%@", [error localizedDescription]);
}
// otherwise the world has been fed. Wow, your code must rock.

Udało nam się uzyskać dostęp do błędów, localizedDescriptionponieważ ustawiliśmy wartość dla NSLocalizedDescriptionKey.

Najlepszym miejscem na więcej informacji jest dokumentacja Apple . To jest naprawdę dobre.

Jest też ładny, prosty samouczek na temat Cocoa Is My Girlfriend .

Alex
źródło
37
Przykładem tego jest najzabawniejsza, kiedykolwiek
Ming Yeow
Jest to dość niesamowite odpowiedź, chociaż istnieją pewne problemy w ARC i odlanie idDo BOOL. Wszelkie drobne zmiany kompatybilne z ARC byłyby bardzo mile widziane.
NSTJ,
6
@TomJowett Byłbym naprawdę wkurzony, gdybyśmy nie byli w stanie położyć kresu głodowi na świecie po prostu dlatego, że Apple zmusiło nas do przejścia do nowego świata opartego wyłącznie na ARC.
Manav,
1
typem zwrotu może być BOOL. Zwróć NOw przypadku błędu i zamiast sprawdzać zwracaną wartość, po prostu sprawdź error. Jeśli nilśmiało, to != nilzałatw to.
Gabriele Petronella,
8
-1: Naprawdę musisz wprowadzić kod, który weryfikuje, że **errornie ma wartości zero. W przeciwnym razie program zgłosi błąd, który jest całkowicie nieprzyjazny i nie pokazuje, co się dzieje.
FreeAsInBeer,
58

Chciałbym dodać kilka sugestii w oparciu o moją najnowszą implementację. Przejrzałem kod firmy Apple i myślę, że mój kod zachowuje się w podobny sposób.

Powyższe posty już wyjaśniają, jak tworzyć obiekty NSError i zwracać je, więc nie będę zawracał sobie głowy tą częścią. Spróbuję zaproponować dobry sposób na integrację błędów (kodów, komunikatów) we własnej aplikacji.


Zalecam utworzenie 1 nagłówka, który będzie przeglądem wszystkich błędów Twojej domeny (tj. Aplikacji, biblioteki itp.). Mój obecny nagłówek wygląda następująco:

FSError.h

FOUNDATION_EXPORT NSString *const FSMyAppErrorDomain;

enum {
    FSUserNotLoggedInError = 1000,
    FSUserLogoutFailedError,
    FSProfileParsingFailedError,
    FSProfileBadLoginError,
    FSFNIDParsingFailedError,
};

FSError.m

#import "FSError.h" 

NSString *const FSMyAppErrorDomain = @"com.felis.myapp";

Teraz, używając powyższych wartości błędów, Apple utworzy podstawowy komunikat o błędzie dla Twojej aplikacji. Błąd może zostać utworzony w następujący sposób:

+ (FSProfileInfo *)profileInfoWithData:(NSData *)data error:(NSError **)error
{
    FSProfileInfo *profileInfo = [[FSProfileInfo alloc] init];
    if (profileInfo)
    {
        /* ... lots of parsing code here ... */

        if (profileInfo.username == nil)
        {
            *error = [NSError errorWithDomain:FSMyAppErrorDomain code:FSProfileParsingFailedError userInfo:nil];            
            return nil;
        }
    }
    return profileInfo;
}

Standardowy komunikat błędu wygenerowany przez Apple ( error.localizedDescription) dla powyższego kodu będzie wyglądał następująco:

Error Domain=com.felis.myapp Code=1002 "The operation couldn’t be completed. (com.felis.myapp error 1002.)"

Powyższe jest już bardzo pomocne dla programisty, ponieważ komunikat wyświetla domenę, w której wystąpił błąd i odpowiedni kod błędu. Użytkownicy końcowi nie będą mieli pojęcia, co 1002oznacza kod błędu , dlatego teraz musimy zaimplementować kilka miłych komunikatów dla każdego kodu.

W przypadku komunikatów o błędach należy pamiętać o lokalizacji (nawet jeśli od razu nie wdrażamy zlokalizowanych komunikatów). W moim obecnym projekcie zastosowałem następujące podejście:


1) utwórz stringsplik, który będzie zawierał błędy. Pliki ciągów można łatwo zlokalizować. Plik może wyglądać następująco:

FSError.strings

"1000" = "User not logged in.";
"1001" = "Logout failed.";
"1002" = "Parser failed.";
"1003" = "Incorrect username or password.";
"1004" = "Failed to parse FNID."

2) Dodaj makra w celu konwersji kodów liczb całkowitych na zlokalizowane komunikaty o błędach. Użyłem 2 makr w moim pliku Constants + Macros.h. MyApp-Prefix.pchDla wygody zawsze dołączam ten plik do nagłówka prefiksu ( ).

Stałe + Makra. H

// error handling ...

#define FS_ERROR_KEY(code)                    [NSString stringWithFormat:@"%d", code]
#define FS_ERROR_LOCALIZED_DESCRIPTION(code)  NSLocalizedStringFromTable(FS_ERROR_KEY(code), @"FSError", nil)

3) Teraz łatwo jest wyświetlić przyjazny dla użytkownika komunikat o błędzie oparty na kodzie błędu. Przykład:

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" 
            message:FS_ERROR_LOCALIZED_DESCRIPTION(error.code) 
            delegate:nil 
            cancelButtonTitle:@"OK" 
            otherButtonTitles:nil];
[alert show];
Wolfgang Schreurs
źródło
9
Świetna odpowiedź! Ale dlaczego nie umieścić zlokalizowanego opisu w słowniku informacyjnym użytkownika, do którego należy? [NSError errorWithDomain: FSMyAppErrorDomain kod: FSProfileParsingFailedError userInfo: @ {NSLocalizedDescriptionKey: FS_ERROR_LOCALIZED_DESCRIPTION (kod błędu)}];
Richard Venable,
1
Czy jest jakieś szczególne miejsce, w którym powinienem umieścić plik ciągu? Od FS_ERROR_LOCALIZED_DESCRIPTION () Otrzymuję tylko numer (kod błędu).
huggie
@huggie: nie bardzo wiem, co masz na myśli. Zwykle umieszczam te makra, których używam w całej aplikacji, w pliku o nazwie Constants+Macros.hi importuję ten plik do nagłówka ( .pchpliku) prefiksu, aby był dostępny wszędzie. Jeśli masz na myśli, że używasz tylko 1 z 2 makr, może to działać. Być może konwersja z intna NSStringnie jest naprawdę konieczna, chociaż tego nie testowałem.
Wolfgang Schreurs,
@huggie: ow, chyba cię teraz rozumiem. Ciągi powinny znajdować się w pliku ( .stringspliku), który można zlokalizować , ponieważ tam właśnie będzie wyglądać makro Apple. Przeczytaj o korzystaniu NSLocalizedStringFromTabletutaj: developer.apple.com/library/mac/documentation/cocoa/conceptual/…
Wolfgang Schreurs
1
@huggie: Tak, użyłem zlokalizowanych tabel ciągów. Kod w makrze FS_ERROR_LOCALIZED_DESCRIPTIONsprawdza ciąg lokalizowalny w pliku o nazwie FSError.strings. Możesz sprawdzić przewodnik lokalizacyjny Apple dla .stringsplików, jeśli jest to dla Ciebie obce.
Wolfgang Schreurs,
38

Świetna odpowiedź Alex. Jednym potencjalnym problemem jest dereferencja NULL. Referencje Apple na temat tworzenia i zwracania obiektów NSError

...
[details setValue:@"ran out of money" forKey:NSLocalizedDescriptionKey];

if (error != NULL) {
    // populate the error object with the details
    *error = [NSError errorWithDomain:@"world" code:200 userInfo:details];
}
// we couldn't feed the world's children...return nil..sniffle...sniffle
return nil;
...
jlmendezbonini
źródło
30

Cel C

NSError *err = [NSError errorWithDomain:@"some_domain"
                                   code:100
                               userInfo:@{
                                           NSLocalizedDescriptionKey:@"Something went wrong"
                               }];

Szybki 3

let error = NSError(domain: "some_domain",
                      code: 100,
                  userInfo: [NSLocalizedDescriptionKey: "Something went wrong"])
AlBeebe
źródło
9

Zapoznaj się z następującym samouczkiem

mam nadzieję, że będzie to dla ciebie pomocne, ale zanim będziesz musiał przeczytać dokumentację NSError

To bardzo interesujący link, który niedawno znalazłem ErrorHandling

Tirth
źródło
3

Spróbuję streścić świetną odpowiedź Alexa i punkt jlmendezbonini, dodając modyfikację, która sprawi, że wszystko będzie kompatybilne z ARC (do tej pory nie jest tak, ponieważ ARC będzie narzekać, ponieważ powinieneś powrócić id, co oznacza „dowolny obiekt”, ale BOOLnie jest przedmiotem rodzaj).

- (BOOL) endWorldHunger:(id)largeAmountsOfMonies error:(NSError**)error {
    // begin feeding the world's children...
    // it's all going well until....
    if (ohNoImOutOfMonies) {
        // sad, we can't solve world hunger, but we can let people know what went wrong!
        // init dictionary to be used to populate error object
        NSMutableDictionary* details = [NSMutableDictionary dictionary];
        [details setValue:@"ran out of money" forKey:NSLocalizedDescriptionKey];
        // populate the error object with the details
        if (error != NULL) {
             // populate the error object with the details
             *error = [NSError errorWithDomain:@"world" code:200 userInfo:details];
        }
        // we couldn't feed the world's children...return nil..sniffle...sniffle
        return NO;
    }
    // wohoo! We fed the world's children. The world is now in lots of debt. But who cares? 
    return YES;
}

Teraz zamiast sprawdzania wartości zwracanej przez nasze wywołanie metody, sprawdzamy, czy errornadal jest nil. Jeśli nie, mamy problem.

// initialize NSError object
NSError* error = nil;
// try to feed the world
BOOL success = [self endWorldHunger:smallAmountsOfMonies error:&error];
if (!success) {
   // inspect error
   NSLog(@"%@", [error localizedDescription]);
}
// otherwise the world has been fed. Wow, your code must rock.
Gabriele Petronella
źródło
3
@Gabriela: Apple stwierdza, że ​​podczas używania zmiennych pośrednich do zwracania błędów, sama metoda powinna zawsze mieć pewną wartość zwracaną w przypadku powodzenia lub niepowodzenia. Apple zaleca programistom, aby najpierw sprawdzili zwracaną wartość i tylko w przypadku, gdy zwracana wartość jest w jakiś sposób nieprawidłowa, sprawdzają błędy. Zobacz następującą stronę: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/...
Wolfgang Schreurs
3

Inny wzorzec projektowy, który widziałem, obejmuje użycie bloków, co jest szczególnie przydatne, gdy metoda jest uruchamiana asynchronicznie.

Powiedzmy, że mamy zdefiniowane następujące kody błędów:

typedef NS_ENUM(NSInteger, MyErrorCodes) {
    MyErrorCodesEmptyString = 500,
    MyErrorCodesInvalidURL,
    MyErrorCodesUnableToReachHost,
};

Zdefiniowałbyś metodę, która może wywoływać taki błąd:

- (void)getContentsOfURL:(NSString *)path success:(void(^)(NSString *html))success failure:(void(^)(NSError *error))failure {
    if (path.length == 0) {
        if (failure) {
            failure([NSError errorWithDomain:@"com.example" code:MyErrorCodesEmptyString userInfo:nil]);
        }
        return;
    }

    NSString *htmlContents = @"";

    // Exercise for the reader: get the contents at that URL or raise another error.

    if (success) {
        success(htmlContents);
    }
}

A kiedy go wywołujesz, nie musisz się martwić o zadeklarowanie obiektu NSError (uzupełnienie kodu zrobi to za Ciebie) lub sprawdzenie zwracanej wartości. Możesz po prostu podać dwa bloki: jeden, który zostanie wywołany, gdy wystąpi wyjątek, i drugi, który zostanie wywołany, gdy się powiedzie:

[self getContentsOfURL:@"http://google.com" success:^(NSString *html) {
    NSLog(@"Contents: %@", html);
} failure:^(NSError *error) {
    NSLog(@"Failed to get contents: %@", error);
    if (error.code == MyErrorCodesEmptyString) { // make sure to check the domain too
        NSLog(@"You must provide a non-empty string");
    }
}];
Rozsądny
źródło
0

Cóż, to trochę poza zakresem pytania, ale jeśli nie masz opcji NSError, zawsze możesz wyświetlić błąd niskiego poziomu:

 NSLog(@"Error = %@ ",[NSString stringWithUTF8String:strerror(errno)]);
Mike.R
źródło
0
extension NSError {
    static func defaultError() -> NSError {
        return NSError(domain: "com.app.error.domain", code: 0, userInfo: [NSLocalizedDescriptionKey: "Something went wrong."])
    }
}

którego mogę użyć, NSError.defaultError()gdy nie mam prawidłowego obiektu błędu.

let error = NSError.defaultError()
print(error.localizedDescription) //Something went wrong.
Hemang
źródło