Καλώς ορίσατε στο dotNETZone.gr - Σύνδεση | Εγγραφή | Βοήθεια

Μάρτιος 2010 - Δημοσιεύσεις

[Μαθαίνοντας Design Patterns] Model – View – ViewModel

Συνεχίζοντας τη σειρά με τα design patterns, σε αυτό το post γίνεται μία αναφορά στο MVVM, μέσα από μία αρκετά απλοϊκή προσέγγιση. Θα περιγραφεί το pattern, θα δωθούν μερικές base classes, ένα utility για το πώς γίνεται type-safely raise ένα event για ανανέωση UI, καθώς και ένα μικρό παράδειγμα, με Bing Maps σε μία Silverlight εφαρμογή.

Τί είναι το MVVM;

Το Model – View – ViewModel είναι ένα πρότυπο σχεδίασης για το σχεδιασμό διεπαφών χρήστη, το οποίο έχει επηρροές τόσο από το Model View Presenter, όσο και από το Model View Controller. Είναι γνωστό  και ως Presentation Model (σχεδόν ίδιο)(PM στο εξής), όπως το έχει καταγράψει ο Martin Fowler. To Model – View – ViewModel (MVVM στο εξής) είναι επίσης ένας καθιερωμένος τρόπος, για τη δημιουργία εφαρμογών που βασίζονται και σε XAML και επιλέχθηκε για την ευελιξία που παρέχει στον προγραμματιστή κατά την ανάπτυξη (decoupling View-Model). Το design pattern που περιγράφεται στη συνέχεια με τη βοήθεια του παραδείγματος σε silverlight, παρουσιάζεται έχοντας κατα νου το ισχυρό πλαίσιο databinding του silverlight (η αντιστοιχία στοιχείου οθόνης με κανονικό Property ενός object).

Ξεκινώντας την περιγραφή του MVVM, αναφέρουμε ότι παρέχει (όπως και το PM) ένα επίπεδο αφαίρεσης του View (της εικόνας δηλαδή που φαίνεται στον χρήστη / XAML) στο οποίο ορίζεται τόσο η κατάστασή του, όσο και η συμπεριφορά του. Έστω ότι έχουμε την οθόνη διαχείρισης καταστημάτων μίας εταιρίας πώλησης παπουτσιών. Υπάρχουν δύο εκδόσεις για την εν λόγω οθόνη, μία που παρουσιάζει όλα τα καταστήματα (datagrid με μία λίστα) και μία που ενεργοποιείται όταν αλλάζουμε ιδιότητες από κάποιο κατάστημα (αρκετά controls, όπως textboxes κτλ με τα οποία μπορούμε να καταχωρούμε πληροφορίες για ένα selected store από την προηγούμενη σελίδα). Οι δύο οθόνες λέγονται αντίστοιχα Master και Detail σελίδες. Έστω ότι έχουμε την Detail σελίδα να αναπτύξουμε. Στη συνέχεια περιγράφεται πώς το MVVM μπορεί να διαχωρίσει τις αρμοδιότητες, για διαφορετικές ενέργειες που λαμβάνουν χώρα πίσω από την οθόνη.

Τα τρία στοιχεία που περιγράφονται είναι τρία διαφορετικά αντικείμενα στη μνήμη και έχουν πεδία και μεθόδους. Έτσι λοιπόν έχουμε το View (τονίζω πάλι ότι αναφερόμαστε σε XAML αρχείο) το οποίο περιέχει διάφορα controls με τα οποία μπορεί να αλληλεπιδράσει ο χρήστης. Κάθε control που περιέχεται, μπορεί να εμφανίσει πληροφορία είτε σε μορφή κειμένου είτε σε κάτι πιο πολυμεσικό.

Έστω ότι έχουμε ένα πεδίο κειμένου (textboxes) και έναν επιλογέα με μία μοναδική επιλογή κάθε φορά (combobox). Τα δύο αυτά controls, δένουν τις τιμές τους με ιδιότητες (properties) του ViewModel μέσω μηχανισμού databinding. Προγραμματιστικά το databinding ανάμεσα στο View και στο ViewModel (που περιέχει όλη την πληροφορία που δείχνει/ανανεώνει/… το View) γίνεται πάρα πολύ εύκολα, μιας και θέτουμε το ViewModel αντικείμενο, ως το πλαίσιο δεδομένων (datacontext) του View (βλ. παράδειγμα). Αν κάποια τιμή, σε κάποιο property αλλάξει, τότε η ίδια ιδιότητα ενεργοποιεί ένα γεγονός (κάνει raise ένα event propertychanged) που υποδηλώνει ότι άλλαξε τιμή και έτσι ενημερώνει το View ότι άλλαξε η τιμή για το συγκριμένο user control (για παράδειγμα ένα textbox – πεδίο εισαγωγής κειμένου). Κάθε πληροφορία δηλαδή, που είναι databound με ένα control όταν αλλάξει, ενημερώνει το UI ότι άλλαξε και ότι θα πρέπει να ανανεωθεί. Για να μπορεί ένα αντικείμενο (το ViewModel στην περίπτωσή μας) να αλληλεπιδρά με αυτόν τον τρόπο θα πρέπει να υλοποιεί το INotifyPropertyChanged interface. Έτσι λοιπόν ανανεώνεται και η αντίστοιχη πληροφορία στον χρήστη. Σε αντίθετη περίπτωση, όταν ένας χρήστης πατήσει ένα κουμπί, μία εντολή (Commanding) θα εκτελεστεί στο ViewModel και θα πραγματοποιήσει όλες τις αλλαγές στα δεδομένα του μοντέλου (τα οποία περιέχονται στο ίδιο το αντικείμενο του ViewModel).

To Model είναι το αντικείμενο το οποίο καλείται από το ViewModel για λειτουργίες πρόσβασης στα δεδομένα. Στην περίπτωσή μας έχουμε δομικές μονάδες που περιγράφουν το πεδίο της εφαρμογής (customers, orders, κτλ). Αξίζει να κρατήσουμε τρία πράγματα: image

 

  • To View ΔΕΝ γνωρίζει το Model (γιατί δεν απαιτείται κάτι τέτοιο εννοιολογικά)
  • To ViewModel ΔΕΝ γνωρίζει τίποτα για View (εξαρτάται, ίσως είναι θέμα προθέσεων του προγραμματιστή)
  • To Model ΔΕΝ γνωρίζει τίποτα για View

Στις XAML εφαρμογές, το View είναι η οθόνη που σχεδιάζουμε στο Expression Blend (την εφαρμογή της Microsoft για σχεδιασμό σελίδων), το ViewModel αναπτύσσεται στο Visual Studio και το Model είναι στην ουσία οι κλάσεις που χρησιμοποιούμε για το πεδίο της εφαρμογής μας οι οποίες περιέχουν όλες τις ιδιότητες οι οποίες υπάρχουν στο αντικείμενο. Στις RIA εφαρμογές το κανονικό μοντέλο υπάρχει στον server και μία πιο μινιμαλιστική έκδοση στον client, για να μπορεί να διαχειρίζεται τα αντικείμενα. Για παράδειγμα στην σελίδα με την επεξεργασία ενός στιγμιότυπου της οντότητας Shoe (π.χ. για rowId = 5) το ViewModel έχει ως ένα από τα πεδία του, ένα αντικείμενο τύπου shoe, το οποίο δηλώνεται στο Model. To Model, έχει στον server την κανονική έκδοση της οντότητας Shoe, ορίζοντας πρόσθετες ιδιότητες αν είναι απαραίτητο (που δεν εμφανίζονται στον client).

Στις ακόλουθες ιεραρχίες μπορούμε να δούμε την κλάση ViewControl που είναι στην ουσία η οθόνη μας, που γνωρίζει το ViewModel και το έχει θέσει ως το Data Context της. Υπάρχει ένα κάπως well defined API μέσα από generic type constraints.

 

Base Classes

public class ViewControl : UserControl, IView {     private IViewModel viewModel;     public ViewControl()     {     }     protected ViewControl(IViewModel viewModel) : this()     {         SetViewModelInternal(viewModel);     }     public virtual TViewModel GetViewModel<TViewModel>()         where TViewModel : class, IViewModel     {         return this.viewModel as TViewModel;     }     public virtual void SetViewModel(IViewModel model)     {         this.SetViewModelInternal(model);     }     private void SetViewModelInternal(IViewModel model)     {         this.viewModel = model;         this.DataContext = model;         PropertyChanged.Raise(() => DataContext);     }     #region INotifyPropertyChanged Members     public event PropertyChangedEventHandler PropertyChanged;     #endregion } public interface IView : INotifyPropertyChanged { } public interface IView<TViewModel> : IView     where TViewModel : IViewModel {     TViewModel ViewModel { get; set; } } public interface IViewModel : INotifyPropertyChanged { } public interface IViewModel<TView> : IViewModel     where TView : IView {     TView View { get; set; } } public abstract class ViewModel<TView>     where TView : IView {     public bool IsDesignTime     {         get         {             return (Application.Current == null) || (Application.Current.GetType() == typeof(Application));         }     }     #region INotifyPropertyChanged Members     public event PropertyChangedEventHandler PropertyChanged;     #endregion }

Έστω η περίπτωση που έχω controls που στέλνουν εντολές, buttons, κλπ κλπ. Θα πρέπει να υπάρχει μία δομή στο XAML, που θα δηλώνει το πια εντολή είναι αυτή που θα πρέπει να εκτελεστεί όταν πατηθεί το κουμπί. Στην περίπτωση του code behind μπορούμε να παίξουμε με handlers αλλά με MVVM μπορώ να έχω μία διαφορετική προσέγγιση. Γιατί να μη εκμεταλλευτώ το μοντέλο του MVVM και να “βρωμίσω” το code behind μου με κώδικα hard-wired με το View? Έτσι λοιπόν ένα σημείο κλειδί είναι να χρησιμοποιήσουμε Commands. Στο WPF υπάρχουν πραγματικά commands που σε συνδυασμό με τα routed events δημιουργούν την έννοια του routed command, αλλά δεν θα μιλήσουμε εδώ γι’ αυτό. Στο Silverlight (3) υπάρχει το ICommand interface το οποίο είναι στην ουσία το εξής, και θα αναλύσουμε σε κάποια άλλη συζήτηση για το commanding σε MVVM:

interface ICommand {     void Execute(object parameter);     bool CanExecute(object parameter);     event EventHandler CanExecuteChanged; }

 

Bing Maps MVVM

Στο συγκεκριμένο παράδειγμα παραθέτω το ViewModel μίας οθόνης που έχει ένα Bing Maps control. Θέλω σε αυτόν το χάρτη, να παρουσιάσω καταστήματα που έχουν χωρική πληροφορία και θα εμφανιστούν στον χάρτη μας. Τι χρειάζομαι ως business πληροφορία; Ένα collection από καταστήματα (τύπου ObservableCollection που μπορεί να ενημερώνει όταν επιδέχεται αλλαγές) και ίσως ένα SelectedStore για να γνωρίζω πιο store έχει επιλεγχθεί (για απεικόνιση περισσότερων πληροφοριών).

public class StoreMapViewModel : ViewModel<IStoreMapView>, IStoreMapViewModel    {        private BusinessLogicDomainContext domainContext = new BusinessLogicDomainContext();        public StoreMapViewModel()        {            Load();        }        private ObservableCollection<Store> stores;        public ObservableCollection<Store> Stores        {            get            {                return stores;            }            set            {                stores = value;                PropertyChanged.Raise(() => Stores);            }        }        private Store selectedStore;        public Store SelectedStore        {            set            {                selectedStore = value;                PropertyChanged.Raise(() => SelectedStore);            }            get            {                return selectedStore;            }        }        private void Load()        {           //Εδώ υπάρχει κώδικας για ανάκτηση όλων των stores. Χρησιμοποιήθηκε το WCF Ria Services κομμάτι του WCF           //το οποίο σχεδιάστηκε ειδικά για τις ανάγκες Line of Business silverlight applications            LoadOperation<Store> loadedStores = this.domainContext.Load(this.domainContext.GetStoresQuery());            loadedStores.Completed += (sender, e) => { Stores = new ObservableCollection<Store>(domainContext.Stores); };        }        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;    }

Αναφορικά με το όμορφο API για το raising των property changed events, προκειμένου να γράφουμε κάτι του στυλ Raise(“PropertyName”), θεωρώ πιο όμορφo να χρησιμοποιήσω έναν type safe τρόπο με lambda που na παρέχει το property name, αντί να γράφω κάτι error prone όπως το όνομα του property με το χέρι. (Credits to original author)

public static void Raise(this PropertyChangedEventHandler handler, Expression<Func<object>> propertyExpression) if (handler != null) {     // Retreive lambda body     var body = propertyExpression.Body as MemberExpression;     if (body == null)         throw new ArgumentException("'propertyExpression' should be a member expression");      // Extract the right part (after "=>")     var vmExpression = body.Expression as ConstantExpression;     if (vmExpression == null)         throw new ArgumentException("'propertyExpression' body should be a constant expression");      // Create a reference to the calling object to pass it as the sender     LambdaExpression vmlambda = System.Linq.Expressions.Expression.Lambda(vmExpression);     Delegate vmFunc = vmlambda.Compile();     object vm = vmFunc.DynamicInvoke();      // Extract the name of the property to raise a change on     string propertyName = body.Member.Name;     var e = new PropertyChangedEventArgs(propertyName);     handler(vm, e); }

Bing Maps View

To View μας που περιέχει το bing maps control, είναι ένα μάθημα για databinding με XAML, templating κτλ από μόνο του, αλλά ήθελα να το δείξω εδώ για πληρότητα. Παρατηρήστε πόσο εύκολα και κατανοητά δηλώνω το που αντλώ πληροφορία για τα pushpins (Stores collection) και πιό member διαλέγω για να ανακτήσω την πληροφορία (LocationInformation).

<controls:ViewControl     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"     xmlns:m="clr-namespace:Microsoft.Maps.MapControl;assembly=Microsoft.Maps.MapControl"     mc:Ignorable="d"     xmlns:controls="clr-namespace:Infrastructure.Controls;assembly=Infrastructure"     d:DesignWidth="640" d:DesignHeight="480"     x:Class="Views.StoreMapView">     <UserControl.Resources>         <DataTemplate x:Key="LogoTemplate">             <m:Pushpin m:MapLayer.Position="{Binding LocationInformation}" />         </DataTemplate>     </UserControl.Resources>     <Grid x:Name="LayoutRoot">         <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}" Margin="0,0,-38,0" >             <StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">                 <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"                                    Text="{Binding Path=ApplicationStrings.StoreMapPageTitle, Source={StaticResource ResourceWrapper}}"/>                 <StackPanel x:Name="MapControl" Orientation="Horizontal">                     <m:Map x:Name="MyMap" CredentialsProvider=<!--Εδώ τοποθετούνται τα δικά μας credentials--> Width="519" HorizontalAlignment="Left">                                        <m:MapItemsControl x:Name="ListOfItems"                                     ItemTemplate="{StaticResource LogoTemplate}"                                     ItemsSource="{Binding Stores}">                         </m:MapItemsControl>                     </m:Map>                     <ComboBox Height="23" Name="comboBox1" Width="150" VerticalContentAlignment="Top"                                                           VerticalAlignment="Top" ItemsSource="{Binding Stores}" DisplayMemberPath="storeName"                                                           SelectionChanged="comboBox1_SelectionChanged" />                 </StackPanel>             </StackPanel>         </ScrollViewer>     </Grid> </controls:ViewControl>

 

Ελπίζω να έδωσα αρκετό food for thought. Enjoy Silverlight & MVVM!!!

Posted: Πέμπτη, 11 Μαρτίου 2010 2:30 πμ από George J. Capnias | 0 σχόλια
Δημοσίευση στην κατηγορία: , , ,

Closures στη C#

Τελικά τα closures είναι ένα απλό concept, που όταν το κατανοήσει κάποιος, το βλέπει τετριμμένο μετά. Αν μπορούσα να δανειστώ από κάπου, δυό λόγια για ορισμό, νομίζω η wikipedia το περιγράφει αρκετά καλά. Τα closures είναι μία ιδέα των γλωσσών προγραμματισμού, που επιτρέπει, σε συναρτήσεις που αντιμετωπίζονται ως first-class objects, τον συσχετισμό ελεύθερων μεταβλητών τους, με τον λεκτικό περιβάλλοντα χώρο της συνάρτησης. Στη συνέχεια θα ήθελα να διατυπώσω το εν λόγω concept όσο πιο σύντομα γίνεται, δανειζόμενος (μερικώς ή πλήρως) κώδικα από το βιβλίο C# in Depth του Jon Skeet και από το Reference της C#.

First – class functions στην C#

First class functions είναι συναρτήσεις που έχουν όλες τις ιδιότητες των αντικειμένων, μπορούν να δημιουργηθούν δυναμικά, μπορούν να συμμετέχουν σε δομές δεδομένων, να ανατεθούν ως παράμετροι, καθώς και να επιστραφούν από κλήσεις άλλων συναρτήσεων. Στην οικογένεια των γλωσσών προγραμματισμού, η C# ανήκει σε εκείνες που υποστηρίζουν first class functions, μέσω του τύπου δεδομένων Delegate και όλης της πραγματικότητας γύρω από αυτό (anonymous methods, lambdas, expression trees). Θυμίζω ότι σε γενικές γραμμές Delegate είναι ένας τύπος δεδομένων (reference type) όπου παρέχει έναν ασφαλή τρόπο για να ενθυλακώσουμε συναρτήσεις (είτε static είτε instance). Ένα Delegate μπορεί να αναφέρεται μόνο σε συναρτήσεις που είναι συμβατές με το signature που περιγράφει.

Η έννοια των πραγματικών closures, ήρθε στην C# με την έκδοση 2.0, υπό τη μορφή των anonymous delegates (και κατεπέκταση στα lambdas), που σε γενικές γραμμές επιτρέπουν την προσπέλαση (read & write) μεταβλητών που δηλώνονται έξω από το scope αυτών. Χωρίς true closure support, είναι π.χ., η Java και ότι πιο κοντινό έχει σε αυτό το concept, είναι τα anonymous inners classes. Ένα simplistic closure support υπολογίζεται ότι θα υποστηριχθεί με την έλευση της νέας έκδοσης 7, το Σεπτέμβρη 2010.

Περιβάλλον ενός lambda

Στο παρακάτω μπλοκ κώδικα θα δούμε διαφορετικούς τύπους μεταβλητών, αναφορικά, με την θέση τους σε σχέση με το lambda.

Outer/free Variables: Οι μεταβλητές που βρίσκονται στο λεκτικό περιβάλλον του delegate (δηλαδή το scope τους περιλαμβάνει το delegate) θεωρούνται ώς πιθανές μεταβλητές που μπορούν να χρησιμοποιηθούν από αυτό. Ελεύθερες μεταβλητές λοιπόν για ένα lambda, είναι όσες δεν δηλώνονται μέσα στο scope του και όσες δεν δηλώνονται στις παραμέτρους τους (τότε θα λέγονταν bound variables). Μαθηματικώς, για παράδειγμα στον Fourier, για κάποιο χ τα a0, a1, a2, .., bn είναι ελεύθερες μεταβλητές (μπορούν να πάρουν τιμές από τον περιβάλλοντα χώρο) :

image

Όταν ένα free variable δεσμευτεί στο περιβάλλον του lambda, λέγεται ότι είναι ένα από τα Captured Variables, τα οποία είναι εξωτερικές μεταβλητές που χρησιμοποιούνται μέσα στο lambda. Ότι δεν είναι free variable, λέγεται bound variable, όπως για παράδειγμα η μεταβλητή Anonymous Local που ανήκει στις μεταβλητές που δηλώνονται και χρησιμοποιούνται μέσα στο lambda.

int outerVariable = 5; string capturedVariable = "captured"; if (DateTime.Now.Hour==23) {     int normalLocalVariable = DateTime.Now.Minute;           Console.WriteLine(normalLocalVariable); } ThreadStart x = delegate()     {         string anonLocal="local to anonymous method";            Console.WriteLine(capturedVariable + anonLocal);     }; x();

Όταν μέσα σε ένα lambda γίνεται αναφορά σε μία εξωτερική μεταβλητή, λέμε ότι η μεταβλητή αυτή, έχει δεσμευτεί (captured) και η ίδια ζει, όσο ζει και το lambda (γίνεται όπως λέμε, επέκταση του χρόνου ζωής της). Τι συμβαίνει στην πραγματικότητα όταν γίνεται capture; Ας το δούμε μέσα από ένα παράδειγμα.

Έστω ότι έχουμε μία συνάρτηση χωρίς capturing μεταβλητών:

static void F() {     D d = () => { Console.Writeline("No captured variables")}; }

όπου:

public delegate void D();

το delegate.

O compiler αντιμετωπίζει το delegate type instantiation (με το lambda που βλέπετε εκεί), κάνοντας allocation μία πραγματική ενδιάμεση μέθοδο που έχει κάνει generate.

static void F() {     D d = new D(generated_Method); } static void generated_Method() {     Console.Writeline("No captured variables") }

Έστω ότι το lambda κάνει capture μία τοπική μεταβλητή:

static void F() {     int y = 1;     D d = () => { Console.Writeline(y)}; }

H C# σε αυτό το σημείο, προσδίδει σημασιολογία closure και λέει: Επειδή το αντικείμενο που γίνεται reference από το d (η μέθοδος που κάνει Writeline το y, δηλαδή),  (μπορεί) να ζήσει περισσότερο από το scope της F (π.χ., αν επιστραφεί από τη συνάρτηση), κρατά τη μεταβλητή y (όχι απλά την τιμή της), για να την χρησιμοποιήσει, όταν το d γίνει invoke και εκτελεστεί. Πριν δείξουμε όμως την περίπτωση όπου το d ζει περισσότερο από το y (προς το παρόν δεν ζει) ας δείξουμε τι γίνεται σε αυτό το παράδειγμα. O compiler under the hood κάνει το εξής:

void F() {     Generated_Class_Keeping_Environment gen = new Generated_Class_Keeping_Environment();     gen.y = 1;     D d = new D(gen.generated_Method); } class Generated_Class_Keeping_Environment {     public int y;     public void generated_Method()     {         Console.Writeline(y);     } }

Ταξιδεύοντας λίγο περισσότερο στο flow, τι γίνεται αν έχω μεταβλητές σε διαφορετικά statement blocks, όπως στο παρακάτω παράδειγμα; Τότε ο compiler λειτουργεί αναλόγως. Στην παρακάτω περίπτωση, έχουμε αναφορά σε μία εξωτερική μεταβλητή (this.x)

class Top {     int x;     static void F()     {         int y = 1;         for (int i =0; i<10;i++)         {             int z = i*2;             D d = () => {Console.Writeline(x+y+z);};         }     } }

Στην παραπάνω μέθοδο F, το lambda κάνει capture μία μεταβλητή που δηλώνεται μία φορά στο this και δεν αλλάζει, μία τοπική μεταβλητή που δεν αλλάζει και μία τοπική μεταβλητή που αλλάζει συνεχώς, με κάθε iteration. O compiler θα κάνει generate δύο κλάσεις. Μία (Locals2) που θα περιέχει τη μέθοδο, την μεταβλητή Z, καθώς και αναφορά στην άλλη generated κλάση. Η δεύτερη generated κλάση (Locals1) θα έχει τη μεταβλητή y και ένα reference σε object τύπου Top οπότε η συνάρτηση F θα μετασχηματιστεί ως εξής:

void F () {     Locals1 locals1 = new Locals1();     locals1._this = this; //H Top     locals1.y = 1;     for (int i =0; i<10;i++)     {         Locals2 locals2 = new Locals2();         locals2.locals1 = locals1;         locals2.z = i*2;         D d = new D(locals2.method);     }    }

Όπως παρατηρούμε, οι μεταβλητές που έχουν γίνει capture, ζουν τώρα πια στο heap καθώς έχουν γίνει dynamic allocated μέσα σε κλάση.

Ας δούμε όμως, μία χρήση των anonymous delegates σαν κανονικά first order functions που είναι. Διαβάστε προσεκτικά τον παρακάτω κώδικα. Έστω ότι θέλω να ορίσω ThreadStart delegates. Φτιάχνω ένα array από δύο anonymous delegates μέσα στο for και αναθέτω σε κάθε ένα από τα δύο κελιά του array, από ένα anonymous delegate. Τα anonymous delegates έχουν κάνει capture δύο free variables, τις outside και inside (και allocate στο heap με τις compiler generated κλάσεις που είδαμε).

static void Main() {     ThreadStart[] delegates = new ThreadStart[2];     int outside = 0;                                               for (int i=0; i < 2; i++)         {             int inside = 0;                                                delegates[ i] = delegate                                        {                 Console.WriteLine ("({0},{1})",                                    outside, inside);                 outside++;                 inside++;             };         }         ThreadStart first = delegates[0];         ThreadStart second = delegates[1];         first();         first();         first();         second();         second();     } }

Σκεφτείτε ότι την ώρα που γίνεται η ανάθεση τιμής στο delegates[ i] γίνονται οι αναθέσεις, έχουν γίνει capture οι μεταβλητές και ο κώδικας εκτελείται παρακάτω. Γίνεται invoke το delegate της θέσης 0 και τυπώνει (0,0). Τη δεύτερη φορά και τα δύο αυξάνονται, όμοια και την τρίτη φορά και έχουμε (1,1) και (2,2). Τώρα εκτελείται το δεύτερο delegate. To outside είναι σε κατάσταση 3, οπότε με την αύξηση γίνεται 4, το inside όμως έχει ΞΑΝΑ-δηλωθεί (int inside = 0), οπότε το delegate έχει διαφορετικό instance της inside, αρχικοποιημένο στο 0. Οπότε η συνέχεια έπεται ως (3,0) και (4,1). Αν γράφατε τον παραπάνω κώδικα δείτε τι θα σας έδειχνε ο Resharper … ;) Πώς μπορώ να το διορθώσω αυτό, αν δεν το επιθυμώ (χωρίς να πατήσω την λάμπα αριστερά :P);

image

Γιατί είναι τόσο σημαντικά τα closure semantics;

Έστω ότι θέλουμε να φιλτράρουμε μία λίστα με βάση κάποια κριτήρια. Ας το υλοποιήσουμε απλά! Τι χρειαζόμαστε; Μία λίστα για να διαβάσουμε, έναν κανόνα που να δείχνει true αν κάνει pass το αντικείμενο και να θα το εισάγει σε μία νέα λίστα. Αφού διατρέξουμε όλη τη λίστα, θα έχουμε στα χέρια μας μία άλλη λίστα μόνο με όσα “πέρασαν”.

public static IList<T> FindAllItemsBasedOnPredicate<T>(IList<T> src, Predicate<T> predicate)     {         List<T> list= new List<T>();         foreach (T item in src)         {             if (predicate(item))              list.Add(item);                }         return list;     }


Όμορφα. Προχωράμε στο filtering. Έστω ότι θέλουμε να βρούμε όλους τους πελάτες ως 18 ετών.

Στη C# 3.0 (λόγω lambdas) μπορούμε να γράψουμε το εξής:

Predicate<Customer> ageRule = customer => customer.age <= 18; IList<Customer> youngCustomers= FindAllItemsBasedOnPredicate(CustomerList, ageRule );

Αν όμως δεν θέλουμε hard coded το 18 και χρειαζόμαστε να το ρυθμίζουμε μέσα από μία μεταβλητή; Έστω ότι δεν έχουμε closures. Τι κάνουμε; Το αλλάζουμε απλά με μία μεταβλητή; Και αν η μεταβλητή είναι static και αλλάξει; Και αν θέλουμε thread safety; Αν αναφερόμαστε σε Threading και θέλουμε να περάσουμε παραμέτρους στο thread μας; H C# έχει την δυνατότητα capturing μεταβλητής και όχι τιμής και αυτό είναι ένα από τα δυνατά της σημεία.

Some food for thought

Τι θα τυπώσει ο παρακάτω κώδικας χωρίς Copy-Paste-F5; ;););)

delegate void Func(); class Program {     static Func[] functionArray = new Func[10];     static void FillFunctionArray(int count)     {         for (int i = 0; i < count; i++)         {             functionArray[ i] = () =>             {                 Console.Write("{0} ", i);             };         }     }     static void Main(string[] args)     {         FillFunctionArray(functionArray.Length);         for (int i = 0; i < functionArray.Length; i++)         {             functionArray[ i]();         }         Console.ReadKey();                } }

Enjoy closures!!!

Για όσους ενδιαφέρονται να διαβάσουν μία ιδέα για το πώς μπορεί να επεκταθεί η IL για να υποστηρίζονται closures και άλλα όμορφα πράγματα σε επίπεδο IL, μπορούν να διαβάσουν μία σχετική δημοσίευση από MSR του Don Syme, δημιουργού της F#.

 

  1. D. Syme, “ILX: Extending the. NET Common IL for Functional Language Interoperability,” Electronic Notes in Theoretical Computer Science 59, no. 1 (2001): 53–72.
Posted: Τρίτη, 9 Μαρτίου 2010 12:13 πμ από George J. Capnias | 0 σχόλια
Δημοσίευση στην κατηγορία: , , ,