Eισαγωγή στα Delegates

Έχουν δημοσιευτεί 09 Φεβρουαρίου 06 01:42 πμ | KelMan 

Ταξίδι στο χρόνο

Σίγουρα όλοι ξέρουμε τι είναι ένα event και όλοι ξέρουμε πώς να καλέσουμε ένα  function. Και πολλές φορές έχουμε συσχετίσει functions με events έτσι ώστε όταν ενεργοποιηθεί (ή όπως συνηθίζεται να λέμε «γίνει raise») ένα event να «τρέχει» αυτομάτως το function (ή Sub για τη περίπτωση της VB). Τι διαφορά έχει το να καλέσουμε εμείς ένα function από το να το καλέσει ένα event;

Ας φανταστούμε δύο objects, το client και το server. To client δημιουργεί το server και καλεί τις μεθόδους του αλλά και το server «ειδοποιεί» το client όταν έχει συμβεί κάτι (οτιδήποτε είναι αυτό που ενεργοποιεί το event). Όταν καλούμε εμείς τις μεθόδους του server object, τότε όλα αυτά γίνονται μέσα στα πλαίσια κάποιας ακολουθίας εντολών που τρέχουν στον client. Τα events που τρέχουν όμως, ενεργοποιούνται άσχετα με την ροή εντολών του προγράμματος είναι δηλαδή κλήσεις σε functions που έχει το client object και στις οποίες κλήσεις θα πρέπει να απαντήσει.

Στη VB6 είχαμε δύο βασικούς μηχανισμούς για την υλοποίηση events. Ο πρώτος είναι ο τυπικός (όταν δηλώναμε ένα object με το WithEvents keyword). O δεύτερος είναι ο μηχανισμός callback, το λεγόμενο «OLE callback» κατά το οποίο ο client περνάει ένα reference του εαυτού του στον server πράγμα που επιτρέπει στον server να καλεί τις μεθόδους του client. Οι δύο τεχνικές αυτές έχουν πλεονεκτήματα και μειονεκτήματα, αλλά ειδικά στη δεύτερη, αν το καλοσκεφτούμε το πράγμα θα δούμε ότι αυτά τα δύο objects θα πρέπει να μείνουν για πάντα στη μνήμη διότι το ένα εξαρτάται από το άλλο και ως εκ τούτου κανένα δεν μπορεί να απελευθερωθεί. Γι αυτόν το λόγο στο COM (γιατί τόση ώρα για COM μιλάμε αλλά δεν ήθελα να το πω από την αρχή μην σταματήσετε να διαβάζετε) υπάρχει ένας μηχανισμός που δεν υποκύπτει στο προηγούμενο πρόβλημα που λέγεται «κυκλική αναφορά» (circular reference) [Σημ. Το πρόβλημα του circular reference δεν αντιμετωπίζεται πάντοτε με επιτυχία και είναι πολύ εύκολο με λίγο «απρόσεχτο» προγραμματισμό να το πετύχουμε].

Ο μηχανισμός που λέγαμε βασίζεται σε κάποιες …χμμμμ «οντότητες» που ονομάζονται connection points στα οποία ένα object μπορεί να εκθέσει πληροφορίες για τα events που διαθέτει, δηλαδή το όνομα του event και τις παραμέτρους με τον τύπο που έχει η κάθε μία. Το server object περιμένει από το client (λέγεμε και event sink) να παρέχει μεθόδους με υπογραφές (υπογραφή = πλήθος και τύπος παραμέτρων και επιστρεφόμενης τιμής) που «ταιριάζουν» με τις υπογραφές των events του.

Αυτά τα connection points υλοποιούνται με μια σειρά COM interfaces που εκθέτουν τις απαραίτητες πληροφορίες και επίσης καθορίζουν πως το server object θα συντηρεί μια λίστα από clients που το χρησιμοποιούν ώστε να μπορεί να γίνει raise ένα event σε όλους τους clients. Επίσης, αυτά τα interfaces καθορίζουν ότι το server object θα πρέπει να κρατάει ένα «weak reference» του κάθε client object. Weak είναι ένα reference που ενώ το server object μπορεί να το χρησιμοποιήσει για να καλέσει τις μεθόδους του client, εντούτοις δεν είναι δεσμευτικό, όπερ σημαίνει ότι το client object μπορεί να καταστραφεί παρότι είναι event sink του server object (για τους nerds – οι υπόλοιποι κάντε skip – να πω ότι αυτό το reference δεν μετριέται κατά το COM reference counting προκειμένου να κατέβει το server object από τη μνήμη)

Όλα αυτά είναι στην πράξη αρκετά σύνθετα και στην περίπτωση της VB γινόντουσαν στο παρασκήνιο και αποτελούν έναν από του λόγους που οι VB developers είναι κακομαθημένοι. Παίρνουν την μπουκιά μασημένη. Πέρα όμως από το λεκτικό spanking για τους τεμπέληδες VB developers, όλα αυτά τα έγραψα για να καταλήξω στην παρακάτω πρόταση:

Όλα αυτά δεν ισχύουν στο .NET Framework, ούτε έχουμε το περίφημο circular reference πρόβλημα γιατί για όλα αυτά υπάρχουν διαφορετικοί μηχανισμοί σε διάφορα επίπεδα. Και αν θέλουμε να υλοποιήσουμε σενάριο callback, τότε θα χρησιμοποιήσουμε τα delegates.

Στο ζουμί τώρα

Διαβάζοντας τα παραπάνω διαπιστώνουμε ότι ένας μηχανισμός για events αποτελείται από δύο πράγματα. Το πρώτο είναι η ανάγκη του server object και του client object να συμφωνούν στις υπογραφές των events. Το δεύτερο είναι η ανάγκη του server object να κρατάει ένα reference του client object. Λοιπόν στο .ΝΕΤ υπάρχουν κάποια objects που κάνουν ακριβώς αυτό! Είναι μεσολαβητές που κάνουν την βρωμοδουλειά…

Ένα delegate object καθορίζει την υπογραφή μιας μεθόδου και ο ορισμός του γίνεται με το Delegate keyword. Για παράδειγμα:

Delegate Sub SimpeDelegate(ByVal s as String)

Η σύνταξη του παραπάνω είναι κάπως περίεργη… Λέει «Sub» αλλά δεν υπάρχει ούτε περιεχόμενο, ούτε «End Sub». Παρατηρείστε δύο πράγματα. Ένα delegate πάντοτε χειρίζεται ένα και μόνο ένα signature, δηλαδή δεν μπορώ να φτιάξω ένα delegate που να περιέχει όλα τα methods του client object. Το δεύτερο – και σημαντικότερο – είναι ότι με το παραπάνω statement ουσιαστικά δημιουργούμε μια νέα κλάση που κάνει inherit την κλάση Delegate, έχει όνομα το «SimpleDelegate» και προσδιορίζει τις παραμέτρους και τι επιστρέφει αυτό το delegate.

Άρα λοιπόν, αν έχουμε μία κλάση όπως η παρακάτω:

Class CalledClass
    Public Sub DisplayMessage(ByVal sMsg as String)
        Console.WriteLine(sMsg)
    End Dub
End Class

Μπορούμε να συσχετίσουμε το Delegate με το DisplayMessage Sub:

Dim cc as New CalledClass()
Dim d1 as SimpleDelegate = AddressOf cc.DisplayMessage

Προσέξατε αυτό το AddressOf; Το delegate δεν αναφέρεται απλά σε μία μέθοδο του client object αλλά σε κάποια συγκεκριμένη μέθοδο που θα βρίσκεται σε συγκεκριμένη θέση μνήμης, όταν θα έχει τρέξει ο κώδικας.

Πλέον, μπορούμε να καλέσουμε την DisplayMessage με τον παρακάτω κώδικα:

d1.Invoke(“Hello delegate world”)

Ενδεχομένως να το υποθέσατε μέχρι τώρα, το SimpleDelegate μπορεί να χρησιμοποιηθεί παντού, οπουδήποτε υπάρχει signature με μία παράμετρο τύπου string που δεν επιστρέφει τίποτα, σε shared methods, σε event functions, σε υπορουτίνες από modules.

Η χρησιμότητα των delegates είναι μεγάλη. Πέρα από το ότι σε αυτά βασίζεται ο μηχανισμός των events στο .NET Framework, είναι πολλές οι εφαρμογές τους από τη στιγμή που θα κατανοηθεί ο τρόπος λειτουργίας τους. Ένα κλασικό παράδειγμα, είναι μια δυναμική ρουτίνα ταξινόμησης της οποίας το function που συγκρίνει δύο items (η καρδιά κάθε ρουτίνας ταξινόμησης) με της περνιέται ως παράμετρος τύπου delegate!

Events επιτέλους!

Στην VB.NET (sorry C# developers, ενδεχομένως κάποιος να προσαρμόσει το άρθρο με C# παραδείγματα) όταν κάνουμε κατά το design time διπλό-κλικ πάνω σε ένα button, αυτόματα έχουμε τον κώδικα:

Private Sub Button1_Click(ByVal sender as System.Object, ByVal e as System.EventArgs) Handles Button1.Click

End Sub

Αυτό το «Handles» ουσιαστικά δημιουργεί ένα Delegate που συσχετίζει το Click event του Button1 με τη Sub «Button1_Click». Εσωτερικά, η κλάση Button θα μπορούσε να έχει κώδικα σαν τον παρακάτω:

Public Class Button
    Public Event Click(ByVal sender as System.Object, ByVal e as System.EventArgs)

    Public Sub Something()
        RaiseEvent Click(Me, _e)
    End Sub
End Class

Όταν εκτελείται το RaiseEvent, ουσιαστικά αυτό που γίνεται είναι να καλείται η Invoke μέθοδος του delegate!

Αντί λοιπόν να κάνουμε διπλό κλικ και να γράψουμε κώδικα, μπορούμε να γράψουμε:

Private Sub ClickHandler(ByVal sender as System.Object, ByVal e as System.EventArgs)

End Sub

Και κατόπιν, σε οποιοδήποτε σημείο του προγράμματος

AddHandler Button1.Click, AddressOf ClickHandler

Εδώ δεν θα χρειαστεί να δηλώσουμε explicitly το delegate, θα το φτιάξει στο background η AddHandler. Αυτό μας δίνει τρομερά μεγάλη ευελιξία καθώς δεν είμαστε υποχρεωμένοι να ξέρουμε κατά το design-time ποια είναι τα objects για τα οποία θέλουμε να φτιάξουμε event handlers. Επίσης, μας επιτρέπει αν πάσα στιγμή να κάνουμε

RemoveHandler Button1.Click, AddressOf ClickHandler

και να σταματήσουμε να λαμβάνουμε events από το Button1 (απαραιτήτως κάνουμε RemoveHandler κατά το τέλος της ζωής του object στο οποίο έχουν γίνει τα AddHandler, αν δεν το έχουμε κάνει νωρίτερα). Για παράδειγμα, όταν δημιουργούμε ένα array από objects μπορεί κάποια από αυτά βάσει κριτηρίου να θέλουμε να μας ειδοποιήσουν για κάποιο event. Χωρίς delegates θα έπρεπε στον event handler να βάλουμε κώδικα του τύπου

αν {object είναι κατάλληλο} τότε

τέλος

Ενώ με τα delegates μπορούμε να βγάλουμε αυτόν τον κώδικα από τον event handler και να τον τοποθετήσουμε στο τμήμα που φτιάχνει τα objects και τα βάζει στο array

For Each cl as Client in aClients
    If Client.Active Then
        AddHandler Client.AddOrder, AddressOf ManageOrders
    End If
End For

Έτσι ο κώδικας του event handler είναι πιο «καθαρός». Πέρα όμως από αυτό, ο παραπάνω κώδικας έχει κάτι το ιδιαίτερο. Κοιτάξτε τον καλά, το εξηγώ στη συνέχεια.

Το κερασάκι

Ήδη από τις beta της VB.NET η κοινότητα θορυβήθηκε από την απουσία των control arrays. Φυσικά, αυτά έλλειπαν διότι πλέον, λόγω delegates, δεν είναι υποχρεωτικό να υπάρχουν control arrays για να έχουμε μια ομάδα από controls στα οποία αντιστοιχεί κοινός κώδικας για κάποιο event. Πλέον, μπορούμε να πούμε:

Private Sub ClickHandler(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.Click, RadioButton1.Click, CheckBox1.Click

End Sub

Δηλαδή, διαφορετικά objects έχουν έναν event handler! Είναι το ίδιο σαν να λέω:

Private Sub ClickHandler(ByVal sender As Object, ByVal e As System.EventArgs)

End Sub

 
και

AddHandler TextBox1.Click, AddressOf ClickHandler
AddHandler RadioButton1.Click, AddressOf ClickHandler
AddHandler CheckBox1.Click, AddressOf ClickHandler

Δηλαδή, πολλαπλά events σε έναν event handler. Για την ακρίβεια, θα μπορούσα να συσχετίσω οποιοδήποτε event στο sub ClickHandler αρκεί να έχει ίδιο signature με αυτό του sub. Απλά ενδέχεται να δυσκολέψει κάπως ο κώδικας του sub αν θα πρέπει να καταλαβαίνει ποιος τον κάλεσε και σε απάντηση ποιού event.

Επίσης, θα μπορούσαμε να πούμε και το εξής:

AddHandler Button1.Click, AddressOf OneClass.ClickHandler
AddHandler Button1.Click, AddressOf TwoClass.ClickHandler
AddHandler Button1.Click, AddressOf SomeClass.ClickHandler

Δηλαδή, ένα event σε πολλαπλούς event handlers! Το delegate που δημιουργείται εμμέσως για να υποστηρίξει αυτά τα AddHandler, ονομάζεται «Multicast Delegate» (κάνει inherit το MulticastDelegate class).

Αυτή ήταν μια εισαγωγή στα delegates. Υπάρχουν πάρα πολλά που θα μπορούσε να πει κανείς γι αυτά καθώς χρησιμοποιούνται σε ασύγχρονα events, στο late binding, στo προγραμματισμό με threads, κλπ. Τέλος, υπάρχουν πολλά που θα μπορούσαμε να πούμε για το πώς δουλεύει το inheritance με τα delegates.

 

Σχόλια:

Χωρίς Σχόλια
Έχει απενεργοποιηθεί η προσθήκη σχολίων από ανώνυμα μέλη

Search

Go

Συνδρομές