Κληρονομικότητα και επίλυση μεθόδων#

Η αναζήτηση μεθόδων στην Perl είναι απλή στην περίπτωση απλής κληρονομικότητας και γνήσια λεπτή μόλις μπουν στο παιχνίδι ρόμβοι ή μεικτά συστήματα κλάσεων. Αυτό το κεφάλαιο καλύπτει την πλήρη ιστορία: τους αλγορίθμους MRO, το SUPER::, τον τελεστή isa, το can, το DOES και την αλληλεπίδραση μεταξύ κλασικών και σύγχρονων κλάσεων σε μία ιεραρχία.

Ο αλγόριθμος αναζήτησης#

Όταν γράφετε $obj->foo(@args), η Perl κάνει τα εξής:

  1. Προσδιορίζει την κλάση. Για ένα αντικείμενο, είναι η κλάση στην οποία έγινε bless ο αναφερόμενος· για κλήση μεθόδου κλάσης, είναι ο ίδιος ο επικαλούμενος.

  2. Διασχίζει τη σειρά επίλυσης μεθόδων (MRO) της κλάσης κατά σειρά. Για κάθε πακέτο στη σειρά, αναζητά μια sub με το όνομα foo.

  3. Η πρώτη αντιστοιχία κερδίζει. Αν δεν βρεθεί αντιστοιχία σε καμία κλάση του MRO, ξαναδιασχίζει αναζητώντας AUTOLOAD.

  4. Αν δεν βρεθεί ούτε AUTOLOAD, εκπέμπει Can't locate object method "foo" via package "Class".

Το MRO είναι αυτό στο οποίο γραμμικοποιείται η αλυσίδα @ISA της κλάσης. Ο κανόνας γραμμικοποίησης εξαρτάται από το ποιο MRO είναι σε ισχύ.

Τα δύο MRO: dfs και c3#

Η Perl παρέχει δύο αλγορίθμους MRO:

  • dfs - αναζήτηση κατά βάθος του @ISA. Η προεπιλογή. Διασχίζει κάθε γονέα και τους γονείς κάθε γονέα, από αριστερά προς τα δεξιά, βυθιζόμενη πλήρως σε κάθε υποδέντρο πριν προχωρήσει.

  • c3 - η γραμμικοποίηση C3 που χρησιμοποιούν η Python, η Raku και πολλές άλλες. Εγγυάται μονοτονικότητα: μια υποκλάση δεν εμφανίζεται ποτέ αργότερα από κάποιον πρόγονό της στη σειρά αναζήτησης.

Στην απλή κληρονομικότητα οι δύο είναι ταυτόσημες: η λίστα ανεβαίνει ευθεία στην αλυσίδα.

Στην πολλαπλή κληρονομικότητα αποκλίνουν. Σκεφτείτε τον ρόμβο:

     A
    / \
   B   C
    \ /
     D
  • dfs: D, B, A, C, A (συμπτυσσόμενη σε D, B, A, C). Μια μέθοδος στο C που υπερκαλύπτει το A επικαλύπτεται από το A επειδή το A επιτυγχάνεται πρώτα μέσω του B.

  • c3: D, B, C, A. Η υπερκάλυψη του C είναι ορατή.

Το C3 είναι σχεδόν πάντα αυτό που θέλετε υπό πολλαπλή κληρονομικότητα. Επιλέξτε το ανά κλάση:

use mro 'c3';
package Diamond::D;
our @ISA = ('Diamond::B', 'Diamond::C');

Για κλάσεις δηλωμένες με class, η πολλαπλή κληρονομικότητα δεν υποστηρίζεται καθόλου, οπότε η επιλογή MRO είναι αμιγώς μια γραμμική διάσχιση και η προεπιλογή dfs είναι μια χαρά.

SUPER::#

Το $self->SUPER::foo(@args) καλεί τη foo σε μια από τις γονικές κλάσεις του πακέτου όπου μεταγλωττίστηκε η τρέχουσα μέθοδος. Αυτή η τελευταία φράση είναι αυτή που όλοι λανθάνουν.

package Animal;
sub speak { 'some sound' }

package Dog;
our @ISA = ('Animal');
sub speak { 'woof' }

package Puppy;
our @ISA = ('Dog');
sub speak {
    my $self = shift;
    return $self->SUPER::speak;
}

Η Puppy::speak καλεί την SUPER::speak. Η Perl αναζητά από το Puppy (το πακέτο μεταγλώττισης), όχι από το ref($self). Η αναζήτηση ξεκινά στο @ISA του Puppy, οπότε βρίσκει την Dog::speak και επιστρέφει 'woof'.

Αυτό έχει σημασία όταν οι μέθοδοι κληρονομούνται και το παιδί καλεί από μέσα το SUPER::. Επειδή το SUPER:: αγκυρώνεται στο πακέτο μεταγλώττισης, μια μέθοδος κληρονομημένη από το Dog και καλούμενη σε αντικείμενο Puppy θα εξακολουθήσει να καλεί το Animal:: μέσω του δικού της SUPER::, όχι το Dog. Συνήθως αυτή είναι η σωστή συμπεριφορά· όταν δεν είναι, στραφείτε στη ρητή πλοήγηση της αλυσίδας με next::method, που καλύπτεται παρακάτω.

next::method - το SUPER με επίγνωση του MRO#

Η mro::next::method (εκτεθειμένη ως $self->next::method(@args)) διασχίζει το ζωντανό MRO από την πραγματική θέση κλήσης της τρέχουσας μεθόδου, όχι από το πακέτο μεταγλώττισης:

use mro 'c3';

sub speak {
    my $self = shift;
    return $self->next::method;
}

Υπό C3 με ρόμβο, η next::method είναι αυτό που θέλετε· το SUPER:: μπορεί να παρακάμψει έναν αδελφικό κλάδο επειδή συμβουλεύεται μόνο τον αριστερότερο πρόγονο.

Ο τελεστής isa#

Ο τελεστής isa ελέγχει την ιδιότητα μέλους κλάσης χωρίς την παγίδα της κλήσης UNIVERSAL::isa ως συνάρτησης:

if ($obj isa 'File::MP3') { ... }
if ($obj isa File::MP3)   { ... }     # bareword form

Τόσο οι κλάσεις βασισμένες σε bless όσο και εκείνες που δηλώνονται με class απαντούν σωστά στο isa, και οι σχέσεις υποκλάσεων ρέουν μέσω των :isa και @ISA πανομοιότυπα.

Τρεις κανόνες που αξίζει να θυμάστε:

  • Το isa επιστρέφει ψευδές (όχι σφάλμα) όταν του δοθεί μη αντικείμενο. Το 42 isa Foo είναι ψευδές. Το undef isa Foo είναι ψευδές.

  • Το isa σέβεται το ζωντανό MRO τη στιγμή της κλήσης. Αν κώδικας προσθέσει στοιχεία στο @ISA κατά τον χρόνο εκτέλεσης, το isa τα βλέπει. (Αυτός είναι άλλος ένας λόγος που η pperl απορρίπτει τον χειρισμό του @ISA για κλάσεις δηλωμένες με class - οι εγγυήσεις θα γίνονταν θολές.)

  • Προτιμήστε τον τελεστή isa έναντι του UNIVERSAL::isa($obj, 'Class'). Ο τελεστής βραχυκυκλώνει σε μη αντικείμενα αντί να πεθαίνει· η μορφή κλήσης συνάρτησης ηττάται επίσης από overloaded αντικείμενα.

can#

Το $obj->can('method') επιστρέφει μια αναφορά κώδικα αν το αντικείμενο έχει μέθοδο με αυτό το όνομα, διαφορετικά undef:

if (my $code = $obj->can('save')) {
    $code->($obj, @args);
}

Χρήσεις:

  • Ανίχνευση χαρακτηριστικού πριν την κλήση: αποφεύγει το σφάλμα Can't locate object method.

  • Αποστολή σε μέθοδο με βάση το όνομα από συμβολοσειρά αποθηκευμένη σε μεταβλητή - πιο ευανάγνωστο από τη μορφή $obj->$name όταν η μέθοδος ενδέχεται να μην υπάρχει.

Μη-χρήσεις:

  • Μη χρησιμοποιείτε το can για να αποφασίσετε αν ένα αντικείμενο υλοποιεί έναν εννοιολογικό ρόλο. Μια κλάση μπορεί να ορίζει μια μέθοδο save που κάνει κάτι εντελώς άσχετο με την μονιμοποίηση. Για ιδιότητα μέλους ρόλου, δείτε ρόλοι και ανάθεση.

DOES#

Το $obj->DOES('RoleOrClass') είναι το αντίστοιχο με επίγνωση ρόλων του isa. Από προεπιλογή το DOES αναθέτει στο isa, οπότε για κλάσεις χωρίς ρητή μηχανική ρόλων τα δύο συμπεριφέρονται το ίδιο. Τα συστήματα ρόλων (κλασικά Moose, Moo και το μελλοντικό χαρακτηριστικό ρόλων του πυρήνα) υπερκαλύπτουν το DOES ώστε να επιστρέφει αληθές και για ρόλους που καταναλώνει η κλάση.

if ($obj->DOES('Printable')) { ... }

Στην pperl, το DOES είναι η σωστή μέθοδος ελέγχου όταν θέλετε να ρωτήσετε «υλοποιεί αυτό το αντικείμενο το συμβόλαιο;» αντί για «κληρονομεί αυτό το αντικείμενο από αυτή την κλάση;».

Μεικτές κλασικές / σύγχρονες ιεραρχίες#

Οι κλάσεις δηλωμένες με class και εκείνες βασισμένες σε bless διαλειτουργούν και προς τις δύο κατευθύνσεις, υπό μια χούφτα κανόνες.

Μια σύγχρονη κλάση που κληρονομεί από κλασική κλάση#

package Shape;                           # classical
sub new {
    my ($class, %args) = @_;
    return bless { colour => $args{colour} // 'black' }, $class;
}
sub colour { $_[0]{colour} }

use feature 'class';
class Circle :isa(Shape) {               # modern inherits classical
    field $radius :param :reader;
    method area { 3.14159265 * $radius ** 2 }
}

Αυτό λειτουργεί. Ο δημιουργημένος κατασκευαστής της σύγχρονης κλάσης αναθέτει στη new του γονέα, και οι μέθοδοι που κληρονομούνται από τον κλασικό γονέα εμφανίζονται στο σύγχρονο παιδί. Η κατάσταση των hash-slot του γονέα ζει δίπλα στην αποθήκευση πεδίων του παιδιού.

Επιφύλαξη: το σύγχρονο παιδί δεν μπορεί να φτάσει στο hash του γονέα. Το $self->{colour} δεν είναι θεμιτό μέσα σε μέθοδο της Circle· πρέπει να καλέσετε $self->colour.

Μια κλασική κλάση που κληρονομεί από σύγχρονη κλάση#

use feature 'class';
class Animal {
    field $name :param :reader;
    method speak { 'some sound' }
}

package Dog;                             # classical inherits modern
our @ISA = ('Animal');
sub new {
    my ($class, %args) = @_;
    return $class->SUPER::new(%args);
}
sub speak { 'woof' }

Και αυτό λειτουργεί - ο δημιουργημένος κατασκευαστής του σύγχρονου γονέα κληρονομείται, και το SUPER::new επιλύεται σε αυτόν. Το κλασικό παιδί δεν πρέπει να επιχειρήσει να τροποποιήσει απευθείας τα πεδία του γονέα· τα πεδία είναι ιδιωτικά στην κλάση που τα δήλωσε.

Τι αποτυγχάνει σε μεικτές ιεραρχίες#

  • Πρόσβαση $self->{fieldname} πέρα από το σύνορο. Τα σύγχρονα πεδία δεν είναι hash-slot, και οι κλασικές hash-slot δεν είναι ορατές ως πεδία. Διαπερνάτε πάντα το σύνορο μέσω μεθόδου.

  • Πολλαπλή κληρονομικότητα σε σύγχρονη κλάση. Το attribute :isa παίρνει μόνο έναν στόχο. Αν χρειάζεστε να αναμίξετε συμπεριφορά από πολλαπλά σημεία, δείτε ρόλοι και ανάθεση.

  • Χειρισμός του @ISA κατά τον χρόνο εκτέλεσης σε σύγχρονη κλάση. Απροσδιόριστη συμπεριφορά. Αντιμετωπίστε το @ISA ως μόνο για ανάγνωση για κάθε κλάση δηλωμένη με class.

Γιατί η πολλαπλή κληρονομικότητα συνήθως βλάπτει#

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

Στην pperl το χαρακτηριστικό class απαγορεύει σκόπιμα την πολλαπλή κληρονομικότητα. Κλασικός κώδικας που τη χρησιμοποιεί θα έπρεπε να μεταβαίνει προς ένα μοντέλο ρόλων· το κεφάλαιο για τους ρόλους είναι η επόμενη στάση.

Περαιτέρω ανάγνωση#

  • class - πλήρης σύνταξη των δηλώσεων σύγχρονης κλάσης, συμπεριλαμβανομένου του :isa.

  • bless - η πρωτογενής οντότητα κάτω από την κλασική κληρονομικότητα.

  • ref - αναφέρει το όνομα της κλάσης μιας blessed αναφοράς.

  • isa - ο προτιμώμενος τελεστής ιδιότητας μέλους κλάσης.

  • Ρόλοι και ανάθεση - η εναλλακτική που βασίζεται στη σύνθεση έναντι της πολλαπλής κληρονομικότητας.

  • Μετάβαση - μετακίνηση μιας υπάρχουσας ιεραρχίας από κλασική σε σύγχρονη.