Daily Archives: 2007.12.17. 20:06

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: , , , , ,

Advertisements

Office 2007 SP1 slipstreaming

Közel egy évvel az Office 2007 RTM után végre megjelent hozzá az első javítócsomag. Nem mondom, hogy az Office 2007 annyira használhatatlan lenne RTM formájában, mint a Vista, de azért erre is ráfért végre a service pack.

A Microsoft javítócsomagok már jó ideje lehetővé teszik, hogy beintegráljuk őket az eredeti RTM verzió telepítőjébe, így egyetlen telepítéssel rögtön a javításokkal ellátott verziót varázsolhatjuk a gépünkre. Ezt a folyamatot nevezik slipstreamingnek. Ez eddig egészen trükkösen működött, gyakorlatilag egy speciális kapcsolóval kellett elindítanunk a service pack telepítőjét, ami rátelepítette magát az eredeti install csomagra. Nem véletlen, hogy csak kevés halandó vállalkozott a feladatra.

Szerencsére ezt az Office 2007-tel teljesen újragondolták és leegyszerűsítették: az RTM telepítő tartalmaz egy Updates nevű mappát, amit mi ide beteszünk, azt a telepítő megtalálja és okosan fel is használja. Ide kell tehát tenni az SP1 fájljait is, csak hogy nem a webről letölthető .exe, hanem .msp fájlok formájában. Ehhez előbb ki kell bontanunk az .exe-t, ami az alábbi paranccsal tehető meg, a /extract kapcsolónak egy célkönyvtárat kell megadnunk, például így:

W:install>office2007sp1-kb936982-fullfile-en-us.exe /extract:"W:installOffice2007Updates"

Ehhez a művelethez admin jogra van szükségünk, mert valaki úgy gondolta, hogy a licenc elfogadásához mindenképp atyaúristeni privilégiumok szükségesek. Szerencsére később semmi más feladatunk nincs, mint elindítani a telepítőt, az SP1 automatikusan települni fog. Nekem SP1 telepítés előtt a Word 12.0.6015.5000, SP1 12.0.6212.1000 verziószámot mutatott.

Ugyanez a módszer tökéletesen működik az Office 2007 Language Pack-re is, ahhoz is létezik már SP1.