LINQ IntelliSense dekódolása: Action, Func, TSource és lambda

Úgy látom, hogy a képek nem jól jelennek meg ebben a cikkben. A cikk teljes változata megtalálható az MSDN Kompetencia Központ oldalán.

 

Annyi sok SQL kódot láttunk már, hogy az alábbi LINQ-es kódon már meg sem akad a szemünk:

    string[] elves = new string[] { "Tudor", "Vidor", "Hapci", "Szundi", "Morgó", "Szende", "Kuka" };

    IEnumerable<string> dors = elves.Where( e => e.EndsWith( "dor" ) );

    foreach( string dor in dors )
    {
        Console.WriteLine( dor );
    }

A Where már annyira természetes, hogy fel sem tűnik, mit ír ki az IntelliSense, amikor elkezdjük használni. De nem is kell túl messzire menni ahhoz, hogy az IntelliSense-ből valami igazán nehezen érthetőt varázsoljunk elő. Íme például a "segítség" az OrderBy metódushoz:

OrderBy IntelliSense

A "Sorts the elements…" szöveg persze érthető, de mi az a TSource, meg a TKey és hogy kerül oda az a Func<>?

Ahhoz, hogy ezt megértsük, vissza kell emlékeznünk arra, hogy mit tanultunk a delegate-ekről. A Visual Studio and .NET Framework glossary szerint a delegate:

In the .NET Framework, a reference to a function. A delegate is equivalent to a function pointer.

Ezért aztán már .NET 2.0-ban írhattunk ilyeneket:

    // Egy saját delegate
    public delegate void DoThis( string input );

    // Egy metódus, ami delegate-et kap paraméterül és meghívja azt
    public void ProcessAll( string[] array, DoThis processMethod )
    {
        for( int i = 0; i < array.Length; i++ )
        {
            processMethod( array[ i ] );
        }
    }

    // A delegate erre a metódusra hivatkozik
    public void PrintUpper( string input )
    {
        Console.WriteLine( input.ToUpper() );
    }

    // Itt kezdődik minden
    public void Start()
    {
        string[] days = new string[] { "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat", "Vasárnap" };

        DoThis myMethod;
        myMethod = PrintUpper;
        ProcessAll( days, myMethod );
    }

Kihasználva, hogy vannak generikus típusok, átírhatjuk a delegate-ünket általánosabb formára:

    public delegate void DoThis<T>( T input );

    public void ProcessAll( string[] array, DoThis<string> processMethod )
    {
        for( int i = 0; i < array.Length; i++ )
        {
            processMethod( array[ i ] );
        }
    }

    public void Start()
    {
        //...
        DoThis<string> myMethod;
        //...
    }

Mivel az általunk létrehozott DoThis elég általánosnak tűnik, ezért a .NET Framework készítői már a 2.0 változatban létrehoztak egy pont ugyanilyen delegate-et System.Action<T> néven. Használhatjuk azt is a saját kódunkban, ugyanaz lesz az eredmény:

    public void ProcessAll( string[] array, Action<string> processMethod )
    {
        for( int i = 0; i < array.Length; i++ )
        {
            processMethod( array[ i ] );
        }
    }

    public void Start()
    {
        //...
        Action<string> myMethod;
        //...
    }

Hasonlóan gyakori feladatnak tűnik olyan szűrő metódusok alkalmazása, amelyek egy bemenő paramétert feldolgoznak, majd visszatérnek egy igaz vagy hamis értékkel. Ezek az ún. predicate-ek (a predicate angol szó ugyanis állítást jelent), amik leírására már a .NET 2.0-ban létezett egy System.Predicate<T> delegate ezzel a szignatúrával:

public delegate bool Predicate<T>( T obj )

Ezt felhasználva készíthetünk olyan metódust, ami egy tömb kiválasztott elemeivel csinál valamit – csak éppen nem tudja, hogy melyek a kiválasztott elemek és hogy mit kell velük csinálni. Ezeket a feladatokat más metódusok fogják elvégezni, amikre természetesen delegate-ek segítségével hivatkozunk:

    public void ProcessDays( string[] array, Action<string> processMethod, Predicate<string> filterMethod )
    
        for( int i = 0; i < array.Length; i++ )
        {
            if( filterMethod( array[ i ] ) )
            {
                processMethod( array[ i ] );
            }
        }
    }
    // Szűrő metódus: true-val tér vissza, ha a paraméter munkanap
    public bool IsWorkday( string day )
    {
        if( day == "Szombat" || day == "Vasárnap" )
        {
            return false;
        }
        return true;
    }

    // Feldolgozó metódus: a paramétert kiírja nagybetűkkel
    public void PrintUpper( string input )
    {
        Console.WriteLine( input.ToUpper() );
    }

    public void Start()
    {
        string[] days = new string[] { "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat", "Vasárnap" };
        ProcessDays( days, PrintUpper, IsWorkday );
    }

Az eredmény pedig a munkanapok nevei a konzolon, csupa nagybetűvel.

Mi újat hoz mindebbe a .NET 3.5 és a C# 3.0? Először is, hogy a lambda kifejezések segítségével még tömörebben leírhatjuk mindezt. Például az egész delegate átadást és metódus hívást egyetlen kifejezésbe sűríthetjük:

    ProcessAll( days, day => Console.WriteLine( day.ToUpper() ) );

A szintaxis működik több delegate-re is:

    ProcessDays( days, 
        day => Console.WriteLine( day.ToUpper() ), 
        day => !(day == "Szombat" || day == "Vasárnap" ) );

A lambda kifejezés tehát egy delegate-es metódus hívásra fordul le LINQ to Objects esetén.

A másik .NET 3.5 újdonság, hogy a System.Core.dll-ben ott figyel jó néhány előre definiált, általános célú delegate:

    namespace System
    {
        public delegate void Action();

        public delegate void Action<T1, T2>( T1 arg1, T2 arg2 );

        public delegate void Action<T1, T2, T3>( T1 arg1, T2 arg2, T3 arg3 );

        public delegate void Action<T1, T2, T3, T4>( T1 arg1, T2 arg2, T3 arg3, T4 arg4 );

        public delegate TResult Func<TResult>();

        public delegate TResult Func<T, TResult>( T arg );

        public delegate TResult Func<T1, T2, TResult>( T1 arg1, T2 arg2 );

        public delegate TResult Func<T1, T2, T3, TResult>( T1 arg1, T2 arg2, T3 arg3 );

        public delegate TResult Func<T1, T2, T3, T4, TResult>( T1 arg1, T2 arg2, T3 arg3, T4 arg4 );
    }

Ezek közül az Action variációkat már ismerjük, olyan metódusok, amelyek csak bemeneti paramétereket várnak és nem térnek vissza semmivel (void). Ezzel szemben a Func változatoknak van visszatérési értékük, melyek típusát is meg kell adnunk, ezt jelzi a TResult. Ebből is van több változat, hogy a többféle bemeneti paramétert le tudjuk írni T1..T4 típusokkal.

Ezek után már könnyen megérthetjük az egyes LINQ metódusokhoz kapcsolódó IntelliSense tippet. A Where esetén például ezt láthatjuk:

Where tooltip

Az előző lista alapján érthetjük, hogy a Func<string, bool> éppen a Func<T, TResult> delegate-nek felel meg, azaz string bemeneti értékhez bool visszatérési értéket rendelő függvényről van szó – és az ilyen függvényt hívjuk predicate-nek. Használhatnánk éppen a Predicate<T> típust is, de valamiért a LINQ bővítő metódusok megalkotói inkább a Funcokat választották. 

Felhasználva a korábbi IsWorkday metódusunkat, írhatjuk akár ezt is, hiszen annak épp string be, bool ki szignatúrája van:

    IEnumerable<string> workdays = days.Where( IsWorkday );

Amit persze lambdával "illik" leírni, akár metódushívással:

    IEnumerable<string> workdays = days.Where( day => IsWorkday( day ) );

Akár pedig tömören, metódushívás nélkül:

    IEnumerable<string> workdays = days.Where( day => !( day == "Szombat" || day == "Vasárnap" ) );

A Where persze egy egyszerű eset, de mi a helyzet az OrderBy paramétereivel? Írhatjuk például ezt és megkapjuk a napokat hosszuk szerint rendezve:

    IEnumerable<string> workdays = days.OrderBy( day => day.Length );

Az OrderBy paramétere egy Func<TSource, TKey> (lásd a tooltippet a cikk elején), amiről már tudjuk, hogy egy olyan metódus, aminek TSource a bemeneti paramétere és TKey típussal tér vissza. Mivel a TKey is csak egy paraméter, az OrderBy tetszés szerinti érték szerint tud rendezni.

Aki úgy gondolja, hogy ez gyerekjáték, annak javaslom az alábbi kód megfejtését nem "mit ír ki", hanem inkább "miért ezt kell ideírnunk" játék formájában (segítségként persze használható az Aggregate függvény leírása az MSDN-ben):

    string totalLength = days.Aggregate( 
        0, 
        ( total, day ) => total += day.Length,
        result => String.Format( "A hét {0} karakter hosszú.", result )
        );

Ez ugyan valóban csak egy sor kód, de edzettebb szem kell a megértéséhez, mintha ugyanezt egy foreach-ben írtuk volna le.

 

Technorati tags: , , , , ,

Reklámok

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s