Anonymous Types: Προσοχή στις διαφορές VB.NET – C#


Σε παλιότερο post μου, είχα αναφερθεί στα Anonymous Types, τα οποία μαζί με το Type Inference και τους Object Initializers αποτελούν στοιχεία απαραίτητα για το LINQ. Σε αντίθεση με το Type Inference (που είναι απλό ως concept και παρόμοιο στις δύο γλώσσες) και τους Object Initializers (κι αυτό παρόμοιο στις δύο γλώσσες) τα Anonymous Types κρύβουν μια σημαντική διαφορά (λέγε με breaking change) στην υλοποίησή τους! Πιστεύω ότι αυτή η διαφορά είναι απαραίτητο να τη γνωρίζει κανείς, είτε γράφει τακτικά και στις δύο γλώσσες, είτε πετύχει κάποιο code sample και πρέπει να το μετατρέψει από VB.NET σε C# ή το αντίστοφο.

Στη C#, όταν δύο anonymous types δημιουργούνται με properties που έχουν ίδιο όνομα, ίδια σειρά και ίδιο τύπο, τότε και τα anonymous types έχουν ίδιο τύπο. Αν αντίστοιχα objects έχουν properties με ίδιες τιμές, τότε τα δύο instances έχουν ίδιο τύπο και είναι equal. Δηλαδή στο

var w = new { FirstName = "Manos", LastName = "Kelaiditis" };
var x = new { FirstName = "Manos", LastName = "Kelaiditis" };
var y = new { FirstName = "Kelaiditis", LastName = "Manos" };
var z = new { LastName = "Kelaiditis" , FirstName = "Manos"};
Console.WriteLine(w.Equals(x) ? "Match" : "NoMatch");
Console.WriteLine(w.Equals(y) ? "Match" : "NoMatch");
Console.WriteLine(w.Equals(z) ? "Match" : "NoMatch");

μόνο το w είναι ίδιο με το x. Tο Equals στα anonymous types χρησιμοποιεί το hashcode του instance το οποίο υπολογίζεται προσθέτοντας το hashcode από κάθε member. Με λίγα λόγια, το hashcode εξαρτάται από τη δομή και τις τιμές. Τώρα το θέμα είναι το εξής: Σε συνέπεια του παραπάνω το hashcode χρησιμοποιείται σε πολλές περιπτώσεις, όπως όταν τα anonymous types μπαίνουν σε collections τύπου HashTable, όταν χρησιμοποιούμε grouping και filtering στα Linq queries αλλά ακόμα και στο data binding των collections. Ουσιαστικά τo hashcode παίζει το ρόλο κλειδιού. Αυτό σημαίνει ότι το hashcode δεν επιτρέπεται να αλλάξει ποτέ στη διάρκεια ζωής του object. Γι αυτό και οι σχεδιαστές της C# αποφάσισαν ότι τα Anonymous Types θα είναι Immutable. Όταν διαβάζετε Immutable θα σκεφτόσαστε "χαραγμένα σε πέτρα". Αυτό σημαίνει ότι αν τολμήσω να γράψω το παρακάτω:

z.LastName = "Georgiou";

θα πάρω το λάθος:

Error    1    Property or indexer ‘AnonymousType#1.LastName’ cannot be assigned to — it is read only   

καθώς τα instances από anonymous types είναι read only. Οι σχεδιαστές της C# θεώρησαν ότι τα "read-only" instances από anonymous types δεν φαίνεται να είναι ιδιαίτερο πρόβλημα αφού τα anonymous types έχουν περιορισμένη χρήση καθότι τυπικά δεν μπορούν να χρησιμοποιηθούν εκτός του context που έχουν δημιουργηθεί.

Τώρα, στη ομάδα της VB, είδαν ότι αν υιοθετούσαν το immutable χαρακτηριστικό για τα anonymous types θα ήταν (για λόγους συμβατότητας) μια απόφαση μονόδρομος που θα τους εμπόδιζε να είναι ευέλικτοι αφού αφενός λόγω late binding μπορεί να ανατραπεί το προηγούμενο και αφετέρου μελλοντικά θα προστεθούν χαρακτηριστικά όπως nominal anonymous types και dynamic interfaces.

Έτσι λοιπόν στη VB υπάρχει ο modifier "Key" για τα πεδία από τα anonymous types. Ο Key modifier κάνει το πεδίο read-only και καθοδηγεί τον compiler να κάνει override τις μεθόδους Equals και GetHashCode. Ας δούμε ένα παράδειγμα:

Dim x = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Dim y = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Dim z = New With {.LastName = "Manos", .FirstName = "Kelaiditis"}
Console.WriteLine(If(x.GetType() Is y.GetType(), "TypeMatch", "NoTypeMatch")) ' TypeMatch
Console.WriteLine(If(x.GetType() Is z.GetType(), "TypeMatch", "NoTypeMatch")) ' NoTypeMatch

Το πρώτο αποτέλεσμα δίνει TypeMatch ενώ το δεύτερο NoTypeMatch καθώς τα member του z είνα σε διαφορετική σειρά σε σχέση με το y. Μέχρι εδώ καλά. Ας δούμε το παρακάτω:

Dim x1 = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Dim y1 = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Console.WriteLine(If(x1.Equals(y1), "Match", "NoMatch")) ' NoMatch

Σε αντίθεση με αυτό που ισχύει στη C#, εδώ δεν παίρνουμε "Match" γιατί έχουμε mutable types αφού δεν έχουμε προσδιορίσει το Key. Απόδειξη είναι ότι μπορούμε άνετα να γράψουμε

x1.LastName = "Georgiou" 

Άρα, για να έχουμε αντιστοιχία με τη C# θα πρέπει να γράψουμε

Dim x2 = New With {Key .FirstName = "Manos", Key .LastName = "Kelaiditis"}
Dim y2 = New With {Key .FirstName = "Manos", Key .LastName = "Kelaiditis"}
Console.WriteLine(If(x2.GetType Is y2.GetType, "TypeMatch", "No TypeMatch"))
Console.WriteLine(If(x2.Equals(y2), "Match", "NoMatch"))

Βέβαια, anonymous types χρησιμοποιούμε και στα Linq queries. Τα παρακάτω είναι ισοδύναμα καθώς παράγονται immutable objects και στις δύο περιπτώσεις:

var query1 = from customer in customers
             select new { customer.FirstName, customer.LastName };
Dim query2 = From customer In customers _ 
             Select customer.FirstName, customer.LastName

Εκεί που υπάρχει διαφορά και χρειάζεται προσοχή είναι όταν χρησιμοποιούμε το New στη VB:

Dim query3 = From customer In customers _
             Select New With {.FirstName = customer.FirstName, _
                              .LastName = customer.LastName}

To παραπάνω query δεν είναι ισοδύναμο με το query2, παράγει mutable objects! Για να γράψουμε ισοδύναμο query με το query1 χρησιμοποιώντας το New, θα πρέπει να γράψουμε:

Dim query3 = From customer In customers _ 
             Select New With {Key .FirstName = customer.FirstName, _
 	                     Key .LastName = customer.LastName}

Τωρά, εκεί που ενδέχεται να κολλήσετε αν μεταφράζετε κώδικα από VB σε C# είναι αν σας τύχει το παρακάτω query:

Dim query = From prod In Products _
            Select New With {Key prod.Name, _
                             Key prod.CostPrice, _
                             prod.SalesPrice}

Παρατηρήστε ότι μόνο το Name και το CostPrice έχουν Key modifier ενώ το SalesPrice δεν έχει. Αυτό δεν μπορείτε να το περάσετε σε C# και θα πρέπει να καταφύγετε στη χρήση κανονικών κλάσεων. Βέβαια, το παραπάνω δίνει αρκετή ευελιξία καθώς για παράδειγμα το αποτέλεσμα του query μπορεί να γίνει data bound σε ένα grid και να επιτρέπονται οι αλλαγές στο πεδίο SalesPrice.