Προγραμματισμός

Ιδού η δύναμη του παραμετρικού πολυμορφισμού

Ας υποθέσουμε ότι θέλετε να εφαρμόσετε μια κατηγορία λίστας στην Java. Ξεκινάτε με μια αφηρημένη τάξη, Λίστακαι δύο υποκατηγορίες, Αδειάζω και Μειονεκτήματα, που αντιπροσωπεύουν κενές και μη λίστες λίστες, αντίστοιχα. Δεδομένου ότι σκοπεύετε να επεκτείνετε τη λειτουργικότητα αυτών των λιστών, σχεδιάζετε ένα Λίστα επισκεπτών διεπαφή και παρέχει αποδέχομαι(...) γάντζοι για Λίστα επισκεπτώνs σε κάθε υποκατηγορία σας. Επιπλέον, το δικό σας Μειονεκτήματα η τάξη έχει δύο πεδία, πρώτα και υπόλοιπο, με αντίστοιχες μεθόδους πρόσβασης.

Ποιοι θα είναι οι τύποι αυτών των πεδίων; Σαφώς, υπόλοιπο πρέπει να είναι τύπου Λίστα. Εάν γνωρίζετε εκ των προτέρων ότι οι λίστες σας θα περιέχουν πάντα στοιχεία μιας συγκεκριμένης κλάσης, το έργο της κωδικοποίησης θα είναι πολύ πιο εύκολο σε αυτό το σημείο. Εάν γνωρίζετε ότι όλα τα στοιχεία της λίστας σας θα είναι ακέραιος αριθμόςΓια παράδειγμα, μπορείτε να εκχωρήσετε πρώτα να είναι τύπου ακέραιος αριθμός.

Ωστόσο, εάν, όπως συμβαίνει συχνά, δεν γνωρίζετε αυτές τις πληροφορίες εκ των προτέρων, πρέπει να διευθετήσετε τη λιγότερο κοινή superclass που περιέχει όλα τα πιθανά στοιχεία που περιλαμβάνονται στις λίστες σας, που συνήθως είναι ο γενικός τύπος αναφοράς Αντικείμενο. Επομένως, ο κώδικάς σας για λίστες στοιχείων διαφορετικού τύπου έχει την ακόλουθη μορφή:

abstract class List {public abstract Object accept (ListVisitor that); } interface ListVisitor {δημόσιο αντικείμενο _case (κενό αυτό); δημόσιο αντικείμενο _case (Μειονεκτήματα ότι); } class Empty επεκτείνει τη λίστα {public Object accept (ListVisitor that) {return that._case (this); }} Το μειονέκτημα κλάσης επεκτείνει τη Λίστα {ιδιωτικό αντικείμενο πρώτα. Υπόλοιπο ιδιωτικής λίστας; Μειονεκτήματα (Object _first, List _rest) {first = _first; ανάπαυση = _rest; } Δημόσιο αντικείμενο πρώτα () {return first;} public List rest () {return rest;} public Object accept (ListVisitor that) {return that._case (this); }} 

Αν και οι προγραμματιστές Java χρησιμοποιούν συχνά το λιγότερο κοινό superclass για ένα πεδίο με αυτόν τον τρόπο, η προσέγγιση έχει τα μειονεκτήματά της. Ας υποθέσουμε ότι δημιουργείτε ένα Λίστα επισκεπτών που προσθέτει όλα τα στοιχεία μιας λίστας Ακέραιος αριθμόςs και επιστρέφει το αποτέλεσμα, όπως φαίνεται παρακάτω:

Η κλάση AddVisitor υλοποιεί ListVisitor {private Integer zero = new Integer (0); public Object _case (Empty that) {return zero;} public Object _case (Μειονεκτήματα ότι) {return new Integer (((Integer) that.first ()). intValue () + ((Integer) that.rest () accept. (αυτό)). intValue ()); }} 

Σημειώστε τα ρητά καστ Ακέραιος αριθμός στο δεύτερο _υπόθεση(...) μέθοδος. Εκτελείτε επανειλημμένα δοκιμές χρόνου εκτέλεσης για να ελέγξετε τις ιδιότητες των δεδομένων. Στην ιδανική περίπτωση, ο μεταγλωττιστής θα πρέπει να εκτελεί αυτές τις δοκιμές για εσάς ως μέρος του ελέγχου τύπου προγράμματος. Αλλά επειδή δεν είστε εγγυημένοι αυτό Προσθήκη επισκέπτη θα εφαρμοστεί μόνο σε Λίστατου Ακέραιος αριθμόςs, ο ελεγκτής τύπου Java δεν μπορεί να επιβεβαιώσει ότι, στην πραγματικότητα, προσθέτετε δύο Ακέραιος αριθμόςs εκτός εάν υπάρχουν τα καστ.

Θα μπορούσατε ενδεχομένως να αποκτήσετε πιο ακριβή έλεγχο τύπου, αλλά μόνο θυσιάζοντας τον πολυμορφισμό και τον διπλό κώδικα. Θα μπορούσατε, για παράδειγμα, να δημιουργήσετε ένα ειδικό Λίστα τάξη (με αντίστοιχο Μειονεκτήματα και Αδειάζω υποκατηγορίες, καθώς και ένα ειδικό Επισκέπτης διεπαφή) για κάθε κατηγορία στοιχείων που αποθηκεύετε σε Λίστα. Στο παραπάνω παράδειγμα, θα δημιουργήσετε ένα IntegerList τάξη των οποίων τα στοιχεία είναι όλα Ακέραιος αριθμόςμικρό. Αλλά αν θέλετε να αποθηκεύσετε, ας πούμε, Booleanσε κάποιο άλλο μέρος του προγράμματος, θα πρέπει να δημιουργήσετε ένα BooleanList τάξη.

Είναι σαφές ότι το μέγεθος ενός προγράμματος που γράφτηκε χρησιμοποιώντας αυτήν την τεχνική θα αυξανόταν γρήγορα. Υπάρχουν και άλλα στυλιστικά ζητήματα. Μία από τις βασικές αρχές της καλής μηχανικής λογισμικού είναι να υπάρχει ένα μόνο σημείο ελέγχου για κάθε λειτουργικό στοιχείο του προγράμματος, και ο διπλότυπος κώδικας με αυτόν τον τρόπο αντιγραφής και επικόλλησης παραβιάζει αυτήν την αρχή. Αυτό συνήθως οδηγεί σε υψηλό κόστος ανάπτυξης και συντήρησης λογισμικού. Για να δείτε γιατί, σκεφτείτε τι συμβαίνει όταν εντοπιστεί ένα σφάλμα: ο προγραμματιστής θα πρέπει να επιστρέψει και να διορθώσει αυτό το σφάλμα ξεχωριστά σε κάθε αντίγραφο που δημιουργήθηκε. Εάν ο προγραμματιστής ξεχάσει να προσδιορίσει όλους τους διπλότυπους ιστότοπους, θα εισαχθεί ένα νέο σφάλμα!

Ωστόσο, όπως δείχνει το παραπάνω παράδειγμα, θα δυσκολευτείτε να διατηρήσετε ταυτόχρονα ένα μόνο σημείο ελέγχου και να χρησιμοποιήσετε πούλια στατικού τύπου για να διασφαλίσετε ότι ορισμένα σφάλματα δεν θα εμφανιστούν ποτέ όταν εκτελείται το πρόγραμμα. Στην Java, όπως υπάρχει σήμερα, συχνά δεν έχετε άλλη επιλογή από το να αντιγράψετε κώδικα εάν θέλετε ακριβή έλεγχο στατικού τύπου. Για να είμαστε σίγουροι, δεν θα μπορούσατε ποτέ να εξαλείψετε εντελώς αυτήν την πτυχή της Java. Ορισμένα αξιώματα της θεωρίας των αυτομάτων, που έχουν ληφθεί στο λογικό τους συμπέρασμα, υπονοούν ότι κανένα σύστημα ήχου δεν μπορεί να καθορίσει με ακρίβεια το σύνολο των έγκυρων εισόδων (ή εξόδων) για όλες τις μεθόδους ενός προγράμματος. Κατά συνέπεια, κάθε σύστημα τύπου πρέπει να εξισορροπεί τη δική του απλότητα και την εκφραστικότητα της γλώσσας που προκύπτει. το σύστημα τύπου Java κλίνει λίγο πολύ προς την κατεύθυνση της απλότητας. Στο πρώτο παράδειγμα, ένα ελαφρώς πιο εκφραστικό σύστημα τύπου θα σας επέτρεπε να διατηρήσετε τον ακριβή έλεγχο τύπου χωρίς να χρειάζεται να αντιγράψετε κώδικα.

Ένα τέτοιο εκφραστικό σύστημα θα προσθέσει γενικοί τύποι στη γλώσσα. Οι γενικοί τύποι είναι μεταβλητές τύπων που μπορούν να τεκμηριωθούν με έναν κατάλληλα συγκεκριμένο τύπο για κάθε παρουσία μιας κλάσης. Για τους σκοπούς αυτού του άρθρου, θα δηλώσω τις μεταβλητές τύπου σε αγκύλες πάνω από τους ορισμούς κλάσης ή διεπαφής. Το εύρος μιας μεταβλητής τύπου θα αποτελείται τότε από το σώμα του ορισμού στον οποίο δηλώθηκε (εκτός από το εκτείνεται ρήτρα). Μέσα σε αυτό το πεδίο, μπορείτε να χρησιμοποιήσετε τη μεταβλητή τύπου οπουδήποτε μπορείτε να χρησιμοποιήσετε έναν συνηθισμένο τύπο.

Για παράδειγμα, με γενικούς τύπους, μπορείτε να ξαναγράψετε το δικό σας Λίστα τάξη ως εξής:

abstract class List {public abstract T accept (ListVisitor ότι); } interface ListVisitor {public T _case (Αδειάστε το); δημόσιο T _case (Μειονεκτήματα ότι); } class Empty επεκτείνει τη λίστα {public T accept (ListVisitor that) {return that._case (this); }} Τα μειονεκτήματα της κλάσης επεκτείνουν τη Λίστα {private T πρώτα. Υπόλοιπο ιδιωτικής λίστας; Μειονεκτήματα (T _first, List _rest) {first = _first; ανάπαυση = _rest; } δημόσια T πρώτα () {return first;} public List rest () {return rest;} public T accept (ListVisitor that) {return that._case (this); }} 

Τώρα μπορείτε να ξαναγράψετε Προσθήκη επισκέπτη για να επωφεληθείτε από τους γενικούς τύπους:

Η κλάση AddVisitor υλοποιεί ListVisitor {private Integer zero = new Integer (0); public Integer _case (Κενό ότι) {return zero;} public Integer _case (Μειονεκτήματα που) {return new Integer ((that.first ()). intValue () + (that.rest (). accept (this)). intValue ()) · }} 

Παρατηρήστε ότι η ρητή μετάδοση Ακέραιος αριθμός δεν χρειάζονται πλέον. Το επιχείρημα ότι στο δεύτερο _υπόθεση(...) η μέθοδος δηλώνεται ότι είναι Μειονεκτήματα, υποδεικνύοντας τη μεταβλητή τύπου για το Μειονεκτήματα τάξη με Ακέραιος αριθμός. Επομένως, ο ελεγκτής στατικού τύπου μπορεί να το αποδείξει αυτό αυτό. πρώτα () θα είναι τύπου Ακέραιος αριθμός και αυτό that.rest () θα είναι τύπου Λίστα. Παρόμοιες περιπτώσεις θα γινόταν κάθε φορά μια νέα παρουσία του Αδειάζω ή Μειονεκτήματα δηλώνεται.

Στο παραπάνω παράδειγμα, οι μεταβλητές τύπου θα μπορούσαν να τεκμηριωθούν με οποιαδήποτε Αντικείμενο. Θα μπορούσατε επίσης να παρέχετε ένα πιο συγκεκριμένο άνω όριο σε μια μεταβλητή τύπου. Σε τέτοιες περιπτώσεις, θα μπορούσατε να καθορίσετε αυτό το δεσμευμένο στο σημείο δήλωσης της μεταβλητής τύπου με την ακόλουθη σύνταξη:

  εκτείνεται 

Για παράδειγμα, αν θέλετε το δικό σας Λίστανα περιέχει μόνο Συγκρίσιμος αντικείμενα, θα μπορούσατε να ορίσετε τις τρεις κατηγορίες σας ως εξής:

Λίστα τάξεων {...} Μειονεκτήματα τάξης {...} Κενή κλάσης {...} 

Παρόλο που η προσθήκη παραμετροποιημένων τύπων στην Java θα σας δώσει τα οφέλη που εμφανίζονται παραπάνω, κάτι τέτοιο δεν θα ήταν χρήσιμο αν σήμαινε τη θυσία της συμβατότητας με τον παλαιό κώδικα στη διαδικασία. Ευτυχώς, μια τέτοια θυσία δεν είναι απαραίτητη. Είναι δυνατή η αυτόματη μετάφραση κώδικα, γραμμένο σε μια επέκταση Java που έχει γενικούς τύπους, σε bytecode για το υπάρχον JVM. Πολλοί μεταγλωττιστές το κάνουν ήδη - οι μεταγλωττιστές Pizza και GJ, που γράφτηκαν από τον Martin Odersky, είναι ιδιαίτερα καλά παραδείγματα. Η πίτσα ήταν μια πειραματική γλώσσα που πρόσθεσε αρκετά νέα χαρακτηριστικά στην Java, μερικά από τα οποία ενσωματώθηκαν στην Java 1.2. Το GJ είναι διάδοχος της πίτσας που προσθέτει μόνο γενικούς τύπους. Δεδομένου ότι αυτή είναι η μόνη προστιθέμενη δυνατότητα, ο μεταγλωττιστής GJ μπορεί να παράγει bytecode που λειτουργεί ομαλά με τον παλαιό κώδικα. Συντάσσει την πηγή στο bytecode μέσω του διαγραφή τύπου, που αντικαθιστά κάθε παρουσία κάθε μεταβλητής τύπου με το άνω όριο αυτής της μεταβλητής. Επιτρέπει επίσης τη δήλωση μεταβλητών τύπου για συγκεκριμένες μεθόδους και όχι για ολόκληρες κατηγορίες. Το GJ χρησιμοποιεί την ίδια σύνταξη για γενικούς τύπους που χρησιμοποιώ σε αυτό το άρθρο.

Εργασία σε εξέλιξη

Στο Rice University, η ομάδα τεχνολογίας γλωσσών προγραμματισμού στην οποία εργάζομαι εφαρμόζει έναν μεταγλωττιστή για μια συμβατή προς τα πάνω έκδοση του GJ, που ονομάζεται NextGen. Η γλώσσα NextGen αναπτύχθηκε από κοινού από τον καθηγητή Robert Cartwright του τμήματος επιστήμης υπολογιστών της Rice και τον Guy Steele της Sun Microsystems. Προσθέτει τη δυνατότητα εκτέλεσης ελέγχων χρόνου εκτέλεσης μεταβλητών τύπου στο GJ.

Μια άλλη πιθανή λύση σε αυτό το πρόβλημα, που ονομάζεται PolyJ, αναπτύχθηκε στο MIT. Επεκτείνεται στο Cornell. Το PolyJ χρησιμοποιεί μια ελαφρώς διαφορετική σύνταξη από το GJ / NextGen. Διαφέρει επίσης ελαφρώς στη χρήση γενικών τύπων. Για παράδειγμα, δεν υποστηρίζει παραμετροποίηση τύπου μεμονωμένων μεθόδων και προς το παρόν δεν υποστηρίζει εσωτερικές τάξεις. Αλλά σε αντίθεση με το GJ ή το NextGen, επιτρέπει τις μεταβλητές τύπων να δημιουργούνται με πρωτόγονους τύπους. Επίσης, όπως το NextGen, το PolyJ υποστηρίζει λειτουργίες χρόνου εκτέλεσης σε γενικούς τύπους.

Η Sun κυκλοφόρησε ένα Java Specification Request (JSR) για την προσθήκη γενικών τύπων στη γλώσσα. Δεν αποτελεί έκπληξη, ένας από τους βασικούς στόχους που αναφέρονται για οποιαδήποτε υποβολή είναι η διατήρηση της συμβατότητας με τις υπάρχουσες βιβλιοθήκες τάξεων. Όταν προστίθενται γενικοί τύποι στην Java, είναι πιθανό ότι μία από τις προτάσεις που συζητήθηκαν παραπάνω θα χρησιμεύσει ως πρωτότυπο.

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

Είναι τα πρότυπα κακά;

C ++ χρήσεις πρότυπα να παρέχει μια μορφή γενικών τύπων. Τα πρότυπα έχουν κερδίσει μια κακή φήμη μεταξύ ορισμένων προγραμματιστών C ++ επειδή οι ορισμοί τους δεν έχουν επιλεγεί με παραμετροποίηση. Αντ 'αυτού, ο κώδικας αναπαράγεται σε κάθε περίπτωση και κάθε αναπαραγωγή ελέγχεται ξεχωριστά. Το πρόβλημα με αυτήν την προσέγγιση είναι ότι ενδέχεται να υπάρχουν σφάλματα τύπου στον αρχικό κώδικα που δεν εμφανίζονται σε καμία από τις αρχικές περιπτώσεις. Αυτά τα σφάλματα μπορούν να εκδηλωθούν αργότερα, εάν οι αναθεωρήσεις ή οι επεκτάσεις προγράμματος εισάγουν νέες καταστάσεις. Φανταστείτε την απογοήτευση ενός προγραμματιστή που χρησιμοποιεί υπάρχουσες τάξεις που πληκτρολογούν έλεγχο όταν συντάσσονται από μόνες τους, αλλά όχι αφού προσθέσει μια νέα, απολύτως νόμιμη υποκατηγορία! Ακόμα χειρότερα, εάν το πρότυπο δεν μεταγλωττιστεί μαζί με τις νέες τάξεις, τέτοια σφάλματα δεν θα εντοπιστούν, αλλά θα καταστρέψουν το πρόγραμμα εκτέλεσης.

Λόγω αυτών των προβλημάτων, μερικοί άνθρωποι φοβούνται όταν επαναφέρουν τα πρότυπα, αναμένοντας ότι τα μειονεκτήματα των προτύπων στο C ++ θα εφαρμοστούν σε ένα σύστημα γενικού τύπου στην Java. Αυτή η αναλογία είναι παραπλανητική, επειδή οι σημασιολογικές βάσεις της Java και του C ++ είναι ριζικά διαφορετικές. Το C ++ είναι μια μη ασφαλής γλώσσα, στην οποία ο έλεγχος στατικού τύπου είναι μια ευρετική διαδικασία χωρίς μαθηματική βάση. Αντίθετα, η Java είναι μια ασφαλής γλώσσα, στην οποία ο ελεγκτής στατικού τύπου αποδεικνύει κυριολεκτικά ότι ορισμένα σφάλματα δεν μπορούν να προκύψουν κατά την εκτέλεση του κώδικα. Ως αποτέλεσμα, τα προγράμματα C ++ που περιλαμβάνουν πρότυπα υποφέρουν από μυριάδες προβλήματα ασφάλειας που δεν μπορούν να προκύψουν στην Java.

Επιπλέον, όλες οι εξέχουσες προτάσεις για ένα γενικό Java εκτελούν σαφή έλεγχο στατικού τύπου των παραμετροποιημένων τάξεων, αντί να το κάνουν σε κάθε περίπτωση της τάξης. Εάν ανησυχείτε ότι αυτός ο ρητός έλεγχος θα επιβραδύνει τον έλεγχο τύπου, βεβαιωθείτε ότι, στην πραγματικότητα, ισχύει το αντίθετο: δεδομένου ότι ο ελεγκτής τύπου κάνει μόνο ένα πέρασμα πάνω από τον παραμετροποιημένο κώδικα, σε αντίθεση με ένα πάσο για κάθε παρουσίαση του παραμετροποιημένοι τύποι, επιταχύνεται η διαδικασία ελέγχου τύπου. Για αυτούς τους λόγους, οι πολυάριθμες αντιρρήσεις στα πρότυπα C ++ δεν ισχύουν για προτάσεις γενικού τύπου για Java. Στην πραγματικότητα, αν κοιτάξετε πέρα ​​από αυτό που έχει χρησιμοποιηθεί ευρέως στη βιομηχανία, υπάρχουν πολλές λιγότερο δημοφιλείς αλλά πολύ καλά σχεδιασμένες γλώσσες, όπως το Objective Caml και το Eiffel, που υποστηρίζουν παραμετροποιημένους τύπους με μεγάλο πλεονέκτημα.

Τα συστήματα γενικού τύπου είναι αντικειμενοστραφή;

Τέλος, ορισμένοι προγραμματιστές αντιτίθενται σε οποιοδήποτε σύστημα γενικού τύπου με το επιχείρημα ότι, επειδή τέτοια συστήματα αναπτύχθηκαν αρχικά για λειτουργικές γλώσσες, δεν είναι αντικειμενοστραφή. Αυτή η αντίρρηση είναι πλαστή. Οι γενικοί τύποι ταιριάζουν πολύ φυσικά σε ένα αντικειμενοστρεφό πλαίσιο, όπως καταδεικνύουν τα παραπάνω παραδείγματα και συζήτηση. Αλλά υποψιάζομαι ότι αυτή η αντίρρηση βασίζεται στην έλλειψη κατανόησης του τρόπου ενσωμάτωσης γενικών τύπων με τον πολυμορφισμό κληρονομιάς της Java. Στην πραγματικότητα, μια τέτοια ολοκλήρωση είναι δυνατή και αποτελεί τη βάση για την εφαρμογή του NextGen.