Κατάσταση σφάλματος#

Τέσσερις μεταβλητές αναφέρουν σφάλματα από τέσσερα διαφορετικά στρώματα. Δεν είναι εναλλάξιμες και συμπληρώνονται από εντελώς διαφορετικούς μηχανισμούς· η κακή χρήση μίας αντί άλλης είναι η πιο συνηθισμένη αιτία σφαλμάτων «γιατί δεν λειτουργεί ο χειρισμός σφαλμάτων μου» σε κώδικα Perl.

Μεταβλητή

Πηγή

Διάρκεια ζωής

$!

errno βιβλιοθήκης C από την τελευταία κλήση συστήματος

Τίθεται σε αποτυχία κλήσης συστήματος· χωρίς νόημα διαφορετικά

$@

Η εξαίρεση που συλλήφθηκε από την τελευταία eval

Τίθεται κατά την έξοδο eval (κενή συμβολοσειρά σε επιτυχία)

$?

Κατάσταση εξόδου της τελευταίας διεργασίας-παιδιού

Τίθεται από κάθε wait/κλείσιμο σωλήνα/system

$^E

Πληροφορίες σφάλματος εκτεταμένες από το OS

Ίδια με τη $! σε Unix· πλουσιότερη σε VMS/Win32

Το καλύτερο πράγμα που μπορείτε να κάνετε για τον χειρισμό σφαλμάτων είναι να εσωτερικεύσετε ποια μεταβλητή απαντάει σε ποια ερώτηση:

  • «Απέτυχε η open/read/connect/κλήση συστήματος μου;» → ελέγξτε πρώτα την τιμή επιστροφής, μετά τη $!.

  • «Έπιασε το μπλοκ eval μου μια εξαίρεση;» → ελέγξτε τη $@.

  • «Ποιος ήταν ο κωδικός εξόδου του προγράμματος που μόλις έτρεξα;» → αποκωδικοποιήστε τη $?.

  • «Βρίσκομαι σε Windows ή VMS και χρειάζομαι κείμενο σφάλματος ειδικό του OS;» → δείτε επίσης τη $^E.

$! - το σφάλμα συστήματος#

Η $! είναι η διεπαφή της Perl με το errno της βιβλιοθήκης C. Έχει διπλή φύση: διαβάστε τη σε αριθμητικό περιβάλλον για να πάρετε τον ακέραιο errno, σε περιβάλλον συμβολοσειράς για να πάρετε την αντίστοιχη συμβολοσειρά σφάλματος:

open(my $fh, '<', '/no/such/file') or do {
    print  "errno = ", $! + 0, "\n";        # numeric: 2
    print  "errstr = $!\n";                 # string: "No such file or directory"
};

Κρίσιμο: η $! έχει νόημα μόνο αμέσως μετά από αποτυχία. Το errno της C δεν καθαρίζεται σε επιτυχία, οπότε μεταγενέστερη ανάγνωση μπορεί να δει παλιά τιμή από κάποια προηγούμενη κλήση συστήματος. Η αξιόπιστη μορφή:

if (open my $fh, '<', $path) {
    # success - $! is meaningless here
    process($fh);
} else {
    # ONLY here is $! meaningful
    die "open $path: $!";
}

Η εγγραφή αποτυχίας στην ίδια γραμμή με τη λειτουργία που την προκάλεσε είναι η ασφαλέστερη γραφή - δεν αφήνει ευκαιρία σε ενδιάμεση λειτουργία Perl να αλλοιώσει το errno.

%! - συμβολικοί έλεγχοι errno#

Η %! είναι ένα hash του οποίου τα κλειδιά είναι ονόματα συμβόλων errno όπως ENOENT, EACCES, EAGAIN. Ένα κλειδί ελέγχεται ως αληθές ακριβώς όταν η $! ισούται τη δεδομένη στιγμή με αυτό το errno:

unless (open my $fh, '<', $path) {
    if ($!{ENOENT}) {
        return [];                # missing file → empty list
    }
    if ($!{EACCES}) {
        die "permission denied: $path";
    }
    die "open $path: $!";          # anything else: re-raise
}

Αυτό είναι φορητό - οι αριθμητικές τιμές των συμβόλων errno διαφέρουν μεταξύ λειτουργικών συστημάτων, αλλά τα κλειδιά %! είναι πάντα τα ονόματα συμβόλων. Στο παρασκήνιο, η %! παρέχεται από το άρθρωμα Errno, το οποίο φορτώνεται κατ” απαίτηση την πρώτη φορά που χρησιμοποιείτε το hash.

Ανάθεση στη $!#

Η εγγραφή στη $! θέτει το errno στη βιβλιοθήκη C - χρήσιμο για κατασκευή μηνύματος σφάλματος που θα δει ο κώδικας παρακάτω:

$! = 13;                         # EACCES
print "$!\n";                    # "Permission denied"

# Slightly more idiomatic - set errno by symbol:
use Errno qw(:POSIX);
$! = EACCES;
die "synthetic permission failure: $!";

$@ - η μεταβλητή εξαίρεσης#

Τίθεται από την eval όταν το μπλοκ (ή η συμβολοσειρά) που εκτέλεσε εκτόξευσε εξαίρεση. Περιέχει είτε την τιμή που μεταβιβάστηκε στη die (που συνήθως είναι συμβολοσειρά, αλλά μπορεί να είναι οποιαδήποτε αναφορά) είτε το μήνυμα σφάλματος ανάλυσης/χρόνου εκτέλεσης από τον διερμηνέα.

eval {
    die "no port configured\n" unless defined $port;
    open(my $sock, '<', "/dev/tcp/$host/$port") or die "connect: $!";
    1;
};
if ($@) {
    warn "could not open $host:$port - $@";
    return;
}

Η επιτυχής αποτίμηση θέτει τη $@ σε κενή συμβολοσειρά ''. Ο έλεγχος if ($@) είναι επομένως αξιόπιστος: η κενή συμβολοσειρά είναι ψευδής, οποιαδήποτε μη κενή τιμή είναι αληθής.

Το κλασικό σφάλμα αλλοίωσης $@#

Υπάρχει κίνδυνος εδώ. Οτιδήποτε εκτελείται μεταξύ της εξόδου eval και του ελέγχου if ($@) μπορεί να αλλοιώσει τη $@. Ο πιο συνηθισμένος ένοχος είναι η μέθοδος DESTROY ενός αντικειμένου - όταν τα αντικείμενα βγαίνουν εκτός εμβέλειας στο κλείσιμο του μπλοκ eval, η DESTROY τους εκτελείται, και αν η DESTROY κάνει η ίδια κάποιο eval, η εξαίρεσή σας χάνεται:

eval {
    my $obj = MyClass->new;
    $obj->might_die;
    1;
};
# If MyClass::DESTROY does an eval, $@ is now empty.
warn "got: $@";                  # may print nothing!

Η ιστορικά συνιστώμενη λύση είναι να αποτυπώσετε τη $@ αμέσως, ή να χρησιμοποιήσετε το άρθρωμα Try::Tiny, που χειρίζεται αυτό και πολλές σχετικές παγίδες:

my $err;
{
    local $@;                    # save and isolate
    eval {
        risky_op();
        1;
    } or $err = $@ || 'unknown error';
}
if ($err) {
    warn "got: $err";
}

Το εγγενές χαρακτηριστικό try/catch της Perl 5.34+ είναι η καθαρότερη σύγχρονη γραφή - δεν χρησιμοποιεί καθόλου τη $@ και δεν επηρεάζεται από το πρόβλημα καταστροφέα:

use feature 'try';
try {
    risky_op();
} catch ($err) {
    warn "got: $err";
}

Η PetaPerl υποστηρίζει εγγενώς τα try/catch. Ο νέος κώδικας πρέπει να τα προτιμά.

eval και $!#

Η eval δεν διατηρεί τη $!. Αν το μπλοκ eval έκανε μια κλήση συστήματος που απέτυχε και μετά die, η $! είναι ό,τι έθεσε η τελευταία λειτουργία - που μέσα σε καταστροφέα ή χειριστή local $SIG{__DIE__} μπορεί να είναι οτιδήποτε. Αν το μήνυμα σφάλματός σας χρειάζεται τη $!, αποτυπώστε τη μέσα στην eval:

eval {
    open(my $fh, '<', $path) or die "open $path: $!\n";
    ...
};

Η \n στο τέλος είναι σύμβαση - καταστέλλει το επίθεμα «at line N» που η Perl προσαρτά στα μηνύματα die, κάτι που είναι σωστό για σφάλματα προσανατολισμένα στον χρήστη.

$? - το σφάλμα παιδιού#

Μετά από system, waitpid, wait, εντολή με backticks, ή κλείσιμο σωλήνα, η $? περιέχει τη 16-bit κατάσταση wait() της διεργασίας-παιδιού. Δεν είναι απλώς ο κωδικός εξόδου:

system($cmd, @args);
my $status = $?;

my $exit_code = $status >> 8;        # exit value passed to exit() in the child
my $signal    = $status & 0x7F;      # signal that killed the child, or 0
my $coredump  = $status & 0x80;      # set if the child dumped core

if ($status == -1) {
    die "system: $!";                # could not even fork/exec
} elsif ($signal) {
    die "$cmd died with signal $signal";
} elsif ($exit_code) {
    die "$cmd failed (exit $exit_code)";
}

Η ολίσθηση >> 8 είναι η πιο αναφερόμενη παγίδα στην Perl. Μόνο το άνω byte είναι ο κωδικός εξόδου· το κάτω byte περιέχει τις σημαίες σήματος-και-core-dump. Αν ξεχάσετε την ολίσθηση παράγονται τιμές πολλαπλασιασμένες με 256.

Η σύγχρονη Perl παρέχει τις μακροεντολές WEXITSTATUS, WIFSIGNALED, WTERMSIG, WIFEXITED του αρθρώματος POSIX, που είναι οι φορητοί ονομαστικοί accessors:

use POSIX ':sys_wait_h';
if (WIFEXITED($?))   { my $code = WEXITSTATUS($?); ... }
if (WIFSIGNALED($?)) { my $sig  = WTERMSIG($?);    ... }

Η $? δεν διατηρείται μεταξύ άλλων λειτουργιών. Αν χρειάζεστε την τιμή, αποτυπώστε τη σε τοπική μεταβλητή στην επόμενη γραμμή - αλλιώς τα επόμενα backticks ή system θα την αντικαταστήσουν.

Η $? σε μπλοκ END#

Μέσα σε μπλοκ END, η $? περιέχει τον σκοπούμενο κωδικό εξόδου. Η ανάθεση σε αυτήν υπερισχύει της κατάστασης εξόδου του σεναρίου:

END {
    $? = 0 if $? == 255 && $shutdown_was_clean;
}

$^E - το εκτεταμένο σφάλμα OS#

Σε Unix, η $^E είναι ακριβώς ίδια με τη $!. Σε Windows, περιέχει την τιμή GetLastError(), η οποία είναι μερικές φορές πιο ενημερωτική από το errno (στο οποίο τα Win32 αντιστοιχίζουν ένα αδρό υποσύνολο). Σε VMS, είναι ο εγγενής κωδικός κατάστασης VMS.

Η PetaPerl είναι μόνο για Linux. Η $^E και η $! είναι εναλλάξιμες εδώ· η μεταβλητή υπάρχει για φορητό κώδικα που μπορεί να τρέχει και σε μη-Unix perl.

Συνδυάζοντάς τα - ένα αναλυτικό παράδειγμα#

sub copy_file {
    my ($src, $dst) = @_;

    open my $in,  '<', $src or die "open $src for reading: $!";
    open my $out, '>', $dst or die "open $dst for writing: $!";

    eval {
        local $/ = \65536;       # block reads, scoped to this eval
        while (defined(my $chunk = <$in>)) {
            print {$out} $chunk or die "write $dst: $!";
        }
        close $out               or die "close $dst: $!";
        1;
    } or do {
        my $err = $@ || 'unknown error';
        unlink $dst;             # clean up the partial file
        close $in;               # may set $! itself
        die "copy_file($src → $dst): $err";
    };

    close $in or warn "close $src: $!";
    return 1;
}

Τρεις μεταβλητές σφάλματος σε πέντε παραγράφους. Η $! αναφέρει κάθε αποτυχία κλήσης συστήματος στη γραμμή που την προκάλεσε. Η $@ συλλέγει κάθε εξαίρεση που πέρασε το όριο eval. Η $? δεν βρίσκεται σε αυτό το παράδειγμα γιατί δεν υπάρχει διεργασία-παιδί - αλλά αν η συνάρτηση εκτελούσε cp, θα αποκωδικοποιούσατε τη $? με την ίδια μορφή όπως στη σελίδα system.

Δείτε επίσης#

  • die - συμπληρώνει τη $@. Ο συνηθισμένος τρόπος για να εγείρετε εξαίρεση από τον δικό σας κώδικα.

  • eval - πιάνει εξαιρέσεις· θέτει τη $@.

  • system, waitpid, wait - συμπληρώνουν τη $?.

  • try/catch - η σύγχρονη εναλλακτική στο eval/$@ που παρακάμπτει τον κίνδυνο αλλοίωσης.

  • Errno - η πηγή των κλειδιών %!· παρέχει ονομαστικές σταθερές errno.

  • Try::Tiny - ανθεκτικό περιτύλιγμα eval που διατηρεί σωστά τη $@.

  • %SIG - οι $SIG{__DIE__} και $SIG{__WARN__} είναι τα hooks που ενεργοποιούνται όταν εκτελούνται η die και η warn· μπορούν να ξαναγράψουν τη $@.

  • Λογικοί τελεστές - το ιδίωμα or die που κινεί σχεδόν κάθε αναφορά $!.