NSException in Swift abfangen

74

Der folgende Code in Swift löst eine NSInvalidArgumentException-Ausnahme aus:

task = NSTask()
task.launchPath = "/SomeWrongPath"
task.launch()

Wie kann ich die Ausnahme abfangen? Soweit ich weiß, bezieht sich try / catch in Swift auf Fehler, die in Swift ausgelöst werden, nicht auf NSExceptions, die von Objekten wie NSTask (die vermutlich in ObjC geschrieben wurden) ausgelöst wurden. Ich bin neu bei Swift, also vermisse ich vielleicht etwas Offensichtliches ...

Bearbeiten : Hier ist ein Radar für den Fehler (speziell für NSTask): openradar.appspot.com/22837476

Silyevsk
quelle
Leider können Sie in Swift keine Objective-C-Ausnahmen abfangen, siehe zum Beispiel stackoverflow.com/questions/24023112/… . Man könnte es als Fehler betrachten, dass NSTask Ausnahmen auslöst, anstatt Fehler zurückzugeben, und Sie könnten einen Fehlerbericht einreichen, aber ich bezweifle, dass Apple die API ändern wird.
Martin R
Danke @MartinR. Ich denke, es ist entweder ein Fehler in der API, oder Swift sollte einen Mechanismus zum Abfangen von ObjC-Ausnahmen (oder besser die beiden) bereitstellen ... Wie auch immer, ich habe einen Fehler geöffnet ( openradar.appspot.com/22837476 ), obwohl ich denke Es gibt viele weitere API-Methoden mit dem gleichen Problem
Silyevsk
Das Beispiel für mich war NSPredicate(fromMetadataQueryString:). Dies soll ein sein init?. Wenn der String also schlecht ist, soll er wahrscheinlich zurückkehren nil, aber tatsächlich stürzt er nur mit einer NSException ab.
Matt

Antworten:

135

Hier ist ein Code, der NSExceptions in Swift 2-Fehler konvertiert.

Jetzt können Sie verwenden

do {
    try ObjC.catchException {

       /* calls that might throw an NSException */
    }
}
catch {
    print("An error ocurred: \(error)")
}

Objekt:

#import <Foundation/Foundation.h>

@interface ObjC : NSObject

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;

@end

ObjC.m

#import "ObjC.h"

@implementation ObjC 

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
    @try {
        tryBlock();
        return YES;
    }
    @catch (NSException *exception) {
        *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
        return NO;
    }
}

@end

Vergessen Sie nicht, dies zu Ihrem "* -Bridging-Header.h" hinzuzufügen:

#import "ObjC.h"
freytag
quelle
1
Herausgefunden! Sie müssen etwas zurückgeben, nachdem tryBlock () erfolgreich ausgeführt wurde, sonst trifft es immer den catch-Block. Ich habe zweimal überprüft, ob es bei einer echten Ausnahme immer noch richtig ausgelöst wird. Ich werde Ihre Antwort bearbeiten, um diese Änderung aufzunehmen.
Ben Baron
1
Klappt wunderbar. Vielen Dank!
Vitalii
4
Wow, ist dies immer noch die einzige Möglichkeit, eine NSException in Swift 2.x zu fangen? Ist das in 3.0 geändert?
Valheru
4
Es hat bei mir nicht funktioniert, bis ich eine explizite returnin den @catchBlock eingefügt habe ; bearbeitet, um das zu tun (und auch das Unnötige entfernt let error, da der Fehler immer im allgemeinen catch-Block als ankommt error)
matt
3
FWIW, in Swift 3 können Sie auch __attribute__((noescape))den Typ hinzufügen , tryBlocksodass Sie Methoden aufrufen können, selfwenn Sie ihn verwenden, ohne ihn selfexplizit einschließen zu müssen . Dies macht die vollständige Erklärung + (BOOL)catchException:(__attribute__((noescape)) void(^)())tryBlock error:(__autoreleasing NSError **)error;.
Tgaul
12

Ich schlage vor, eine C-Funktion zu erstellen, die die Ausnahme abfängt und stattdessen einen NSError zurückgibt. Verwenden Sie dann diese Funktion.

Die Funktion könnte folgendermaßen aussehen:

NSError *tryCatch(void(^tryBlock)(), NSError *(^convertNSException)(NSException *))
{
    NSError *error = nil;
    @try {
        tryBlock();
    }
    @catch (NSException *exception) {
        error = convertNSException(exception);
    }
    @finally {
        return error;
    }
}

Und mit ein wenig Überbrückungshilfe müssen Sie nur anrufen:

if let error = tryCatch(task.launch, myConvertFunction) {
    print("An exception happened!", error.localizedDescription)
    // Do stuff
}
// Continue task

Hinweis: Ich habe es nicht wirklich getestet, ich konnte keinen schnellen und einfachen Weg finden, um Objective-C und Swift auf einem Spielplatz zu haben.

BPCorp
quelle
9

TL; DR: Verwenden Sie Karthago, um https://github.com/eggheadgames/SwiftTryCatch einzuschließen, oder CocoaPods, um https://github.com/ravero/SwiftTryCatch einzuschließen .

Dann können Sie Code wie diesen verwenden, ohne befürchten zu müssen, dass Ihre App abstürzt:

import Foundation
import SwiftTryCatch

class SafeArchiver {

    class func unarchiveObjectWithFile(filename: String) -> AnyObject? {

        var data : AnyObject? = nil

        if NSFileManager.defaultManager().fileExistsAtPath(filename) {
            SwiftTryCatch.tryBlock({
                data = NSKeyedUnarchiver.unarchiveObjectWithFile(filename)
                }, catchBlock: { (error) in
                    Logger.logException("SafeArchiver.unarchiveObjectWithFile")
                }, finallyBlock: {
            })
        }
        return data
    }

    class func archiveRootObject(data: AnyObject, toFile : String) -> Bool {
        var result: Bool = false

        SwiftTryCatch.tryBlock({
            result =  NSKeyedArchiver.archiveRootObject(data, toFile: toFile)
            }, catchBlock: { (error) in
                Logger.logException("SafeArchiver.archiveRootObject")
            }, finallyBlock: {
        })
        return result
    }
}

Die von @BPCorp akzeptierte Antwort funktioniert wie beabsichtigt, aber wie wir festgestellt haben, werden die Dinge ein wenig interessant, wenn Sie versuchen, diesen Objective C-Code in ein mehrheitliches Swift-Framework zu integrieren und dann Tests ausführen. Wir hatten Probleme damit, dass die Klassenfunktion nicht gefunden wurde ( Fehler: Verwendung eines nicht aufgelösten Bezeichners ). Aus diesem Grund und nur zur allgemeinen Benutzerfreundlichkeit haben wir es als Karthago-Bibliothek für den allgemeinen Gebrauch verpackt.

Seltsamerweise konnten wir das Swift + ObjC-Framework problemlos an anderer Stelle verwenden. Es waren nur die Komponententests für das Framework, die Probleme hatten.

PRs angefordert! (Es wäre schön, wenn es eine Kombination aus CocoaPod und Karthago wäre und einige Tests hätte).

mm2001
quelle
5
Für STC würde ich empfehlen, nur die Dateien in Ihr Projekt zu kopieren. Ich habe viel zu viele Probleme beim Versuch, STC über Paketmanager zu importieren und zu aktualisieren. Es gibt ungefähr 173 verschiedene Gabeln davon. Sie unterstützen Swift 1, Swift 2 und / oder Swift 3. Sie unterstützen Karthago, CocoaPods und / oder SPM. Sie unterstützen iOS, macOS, tvOS und / oder watchOS. Aber keine Gabel unterstützt die bestimmte Kombination von Dingen, die ich brauche (oder die in einem Jahr aktualisiert wurden), und ich habe viel zu viel Zeit damit verschwendet, nachzuschauen. Wählen Sie eine aus, kopieren Sie sie in Ihr Projekt und beginnen Sie mit nützlicherer Arbeit.
Ssswift
3

Wie in den Kommentaren erwähnt, ist es ein Fehler, dass diese API Ausnahmen für ansonsten wiederherstellbare Fehlerbedingungen auslöst. Legen Sie es ab und fordern Sie eine NSError-basierte Alternative an. Der aktuelle Stand der Dinge ist größtenteils ein Anachronismus, da die NSTaskDaten vor Apple standardisiert wurden, dass Ausnahmen nur für Programmiererfehler gelten.

In der Zwischenzeit können Sie zwar einen der Mechanismen aus anderen Antworten verwenden, um Ausnahmen in ObjC abzufangen und an Swift weiterzuleiten. Beachten Sie jedoch, dass dies nicht sehr sicher ist. Der Mechanismus zum Abwickeln von Stapeln hinter Ausnahmen von ObjC (und C ++) ist fragil und grundsätzlich nicht mit ARC kompatibel. Dies ist einer der Gründe, warum Apple Ausnahmen nur für Programmiererfehler verwendet. Die Idee ist, dass Sie (zumindest theoretisch) alle Ausnahmefälle in Ihrer App während der Entwicklung aussortieren können und keine Ausnahmen in Ihrem Produktionscode auftreten. (Schnelle Fehler oder NSErrors können andererseits auf behebbare Situations- oder Benutzerfehler hinweisen.)

Die sicherere Lösung besteht darin, die wahrscheinlichen Bedingungen vorauszusehen, unter denen eine API Ausnahmen auslösen und diese behandeln kann, bevor die API aufgerufen wird. Wenn Sie in ein indizieren NSArray, überprüfen Sie countzuerst dessen . Wenn Sie launchPathon NSTaskauf etwas setzen, das möglicherweise nicht vorhanden oder nicht ausführbar ist NSFileManager, überprüfen Sie dies, bevor Sie die Aufgabe starten.

Rickster
quelle
Ich habe tatsächlich einen Fehler im September geöffnet (es erscheint in den Kommentaren, ich werde es jetzt zur Frage selbst hinzufügen)
Silyevsk
1

Sie können in Swift keine Objective-C-Ausnahme abfangen. Sie können dies jedoch umgehen, indem Sie einen Objective-C-Wrapper erstellen, den Sie dann in Swift importieren. Ich habe diese Arbeit erledigt und daraus ein wiederverwendbares Swift Package Manager-Paket gemacht. Fügen Sie dieses Paket einfach in Xcode hinzu und verwenden Sie es dann wie folgt:

import Foundation
import ExceptionCatcher

final class Foo: NSObject {}

do {
    let value = try ExceptionCatcher.catch {
        return Foo().value(forKey: "nope")
    }

    print("Value:", value)
} catch {
    print("Error:", error.localizedDescription)
    //=> Error: [valueForUndefinedKey:]: this class is not key value coding-compliant for the key nope.
}
Sindre Sorhus
quelle
0

Die Version, die Fehlerinformationen etwas besser überträgt.

@implementation ObjCExceptionHandler

+ (BOOL)tryExecute:(nonnull void(NS_NOESCAPE^)(void))tryBlock error:(__autoreleasing NSError * _Nullable * _Nullable)error {
   @try {
      tryBlock();
      return YES;
   }
   @catch (NSException *exception) {
      NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
      if (exception.userInfo != NULL) {
         userInfo = [[NSMutableDictionary alloc] initWithDictionary:exception.userInfo];
      }
      if (exception.reason != nil) {
         if (![userInfo.allKeys containsObject:NSLocalizedFailureReasonErrorKey]) {
            [userInfo setObject:exception.reason forKey:NSLocalizedFailureReasonErrorKey];
         }
      }
      *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:userInfo];
      return NO;
   }
}

@end

Anwendungsbeispiel:

      let c = NSColor(calibratedWhite: 0.5, alpha: 1)
      var r: CGFloat = 0
      var g: CGFloat = 0
      var b: CGFloat = 0
      var a: CGFloat = 0
      do {
         try ObjC.perform {
            c.getRed(&r, green: &g, blue: &b, alpha: &a)
         }
      } catch {
         print(error)
      }

Vor:

Error Domain=NSInvalidArgumentException Code=0 "(null)"

Nach:

Error Domain=NSInvalidArgumentException Code=0 
"*** -getRed:green:blue:alpha: not valid for the NSColor NSCalibratedWhiteColorSpace 0.5 1; need to first convert colorspace." 
UserInfo={NSLocalizedFailureReason=*** -getRed:green:blue:alpha: not valid for the NSColor NSCalibratedWhiteColorSpace 0.5 1; need to first convert colorspace.}
Vlad
quelle
-3

Wie in der Dokumentation angegeben , ist dies eine einfache und einfache Möglichkeit:

do {
    try fileManager.moveItem(at: fromURL, to: toURL)
} catch let error as NSError {
    print("Error: \(error.domain)")
}
Rémy Virin
quelle
2
Dies wird nicht helfen, weil , wie in Ihrem Link am Ende des doc beschrieben: " Handle Ausnahmen in Objective-C nur . <...> In Objective-C, Ausnahmen unterscheiden sich von Fehlern Objective-C Ausnahmebehandlung verwendet die @try, @catch, und @throwSyntax, um nicht behebbare Programmiererfehler anzuzeigen. <...> Es gibt jedoch keine sichere Möglichkeit, Objective-C-Ausnahmen in Swift wiederherzustellen . Um Objective-C-Ausnahmen zu behandeln, schreiben Sie Objective-C-Code, der Ausnahmen abfängt, bevor sie Swift- Code erreichen . "
Artem