ירושה ופתרון מתודות#

חיפוש מתודה ב־Perl הוא פשוט במקרה של ירושה יחידה ועדין באמת ברגע שיהלומים או מערכות מחלקות מעורבות נכנסות לתמונה. הפרק הזה מכסה את הסיפור המלא: אלגוריתמי ה־MRO, SUPER::, האופרטור isa, can, DOES, והאינטראקציה בין מחלקות קלאסיות ומודרניות בהיררכיה אחת.

אלגוריתם החיפוש#

כאשר אתם כותבים $obj->foo(@args), Perl עושה את הדברים הבאים:

  1. קביעת המחלקה. עבור עצם, זו המחלקה שאליה בורכה הישות המוצבעת; עבור קריאת מתודת־מחלקה, זה ה־invocant עצמו.

  2. הליכה על סדר פתרון המתודות (MRO) של המחלקה ברצף. עבור כל חבילה בסדר, חפשו sub בשם foo.

  3. ההתאמה הראשונה מנצחת. אם לא נמצאת התאמה באף מחלקה ב־MRO, יש ללכת שוב בחיפוש אחר AUTOLOAD.

  4. אם גם לא נמצא AUTOLOAD, נזרק Can't locate object method "foo" via package "Class".

ה־MRO הוא מה ששרשרת ה־@ISA של המחלקה מתקווה אליו. כלל הקיווון תלוי באיזה MRO תקף.

שני ה־MROs: 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 בודק חברות במחלקה ללא ה־foot-gun של 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'). האופרטור מקצר על לא־עצמים במקום לקרוס; צורת קריאת הפונקציה גם מובסת על־ידי עצמים עם overloading.

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 של ההורה, ומתודות שנירשות מההורה הקלאסי מופיעות על הילד המודרני. מצב חריצי ההאש של ההורה חי לצד אחסון השדות של הילד.

אזהרה: הילד המודרני לא יכול להגיע אל ההאש של ההורה. $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} חוצה את הגבול. שדות מודרניים אינם חריצי האש, וחריצי האש קלאסיים אינם נראים כשדות. תמיד חצו את הגבול דרך מתודה.

  • ירושה מרובה אל מחלקה מודרנית. התכונה :isa לוקחת רק יעד אחד. אם אתם צריכים לערבב התנהגות ממספר מקומות, ראו תפקידים והאצלה.

  • מניפולציה של @ISA בזמן ריצה על מחלקה מודרנית. התנהגות לא־מוגדרת. התייחסו ל־@ISA כאל לקריאה בלבד עבור כל מחלקה שמוצהרת עם class.

מדוע ירושה מרובה בדרך כלל מזיקה#

כל שפה עם ירושה מרובה מגלה בסופו של דבר את אותן פתולוגיות. חיפוש מתודה נעשה תלוי־סדר; בעיות יהלום מאלצות אתכם לחשוב על קיווון; המקרה ״ירשתי פעמיים מאותו שורש״ מסבך את האתחול. תפקידים - הרכבה של התנהגות ללא יחס ירושה - פותרים את בעיית השיתוף הבסיסית ללא הפתולוגיות.

ב־pperl, התכונה class אוסרת ירושה מרובה במכוון. קוד קלאסי שמשתמש בה צריך לעבור למודל תפקידים; פרק התפקידים הוא התחנה הבאה.

קריאה נוספת#

  • class - תחביר מלא של הצהרות מחלקה מודרניות כולל :isa.

  • bless - הפרימיטיב מתחת לירושה הקלאסית.

  • ref - מדווח את שם המחלקה של הפניה מבורכת.

  • isa - אופרטור החברות־במחלקה המועדף.

  • תפקידים והאצלה - החלופה מבוססת־ההרכבה לירושה מרובה.

  • העברה - הליכה על היררכיה קיימת מקלאסי למודרני.