Daug metų buvau ištikimas C++ programavimo kalbos fanas, neslėpsiu, daugelis šios kalbos idėjų iki šiol mane žavi. Kadangi programavau daugiausiai tik savo malonumui, man ši kalba idealiai tiko. Ir tada pasirodė pirmoji C# versija — kalba, kuri, kaip buvo skelbta, ištaisė daugelį C++ problemų, sukurta tam, kad būtų paprasta, efektyvi, lengvai išmokstama ir t.t., kitais žodžiais tariant, labai nuobodi ir neįdomi. Tada atrodė, kad kalba sukurta tik tam, kad būtų galima greičiau atlikti darbą, būtent, darbas — tokia asociacija man iškildavo pamačius C#. Žavėjausi kitų naujų kalbų, tokių kaip ruby, šūkiais, kad svarbiausia programuojant jausti malonumą. Nors ir norėjosi neatsilikti nuo naujovių, vis dėlto norėjau likti ištikimas C šeimos kalboms.

C# 2.0 šiek tiek pataisė padėtį, mat toje versijoje atsirado mano mėgstamiausios C++ konstrukcijos — šablonų (angl. templates), atitikmuo — bendrybės (angl. generics). Ir nors tai nebuvo toks galingas sprendimas kaip C++ variantas, reikalai krypo teisinga linkme.
Ir štai, Microsoft anonsavo 2008 m. pradžioje pasirodysiančios C# trečiosios versijos specifikaciją. Dabar galiu drąsiai sakyti, kad programavimo malonumas grįžta į Microsoft technologijų pasaulį. Šiame straipsnyje apžvelgsiu turbūt daugiausiai triukšmo sukėlusią naujovę — — bei pateiksiu konkretų pavyzdį, parodantį, koks smagus tai dalykas.

C# 3.0 naujovės

, be abejo, yra C# 3.0 pažiba, tačiau kalboje atsirado ir kitų paminėjimo vertų naujovių, daugelis — būtent dėl . Nesigilinsiu į anoniminius duomenų tipus, praplėtimo metodus ir pan., nes tai daugiau techniniai dalykai, apie kuriuos galima pasiskaityti bet kokiame įvade į .

Lambda išraiškos

Trumpai tariant, lambda išraiška (angl. lambda expression), tai galimybė kodo gabaliuką funkcijai perduoti kaip parametrą, o ilgesnį paaiškinimą galima rasti kokioje nors programavimo knygoje, mat tai yra vienas iš tų dalykų, dėl kurių kompiuterija vadinama mokslu (todėl ir pavadinimas, lambda išraiškos, toks keistas). Štai paprastas pavyzdys, spausdinantis skaičius iš intervalo 1 — 10, kurie dalinasi iš 3:

var numbers = Enumerable.Range(1, 10);
foreach (int num in numbers.Where( i => (i % 3 == 0) )) {
      Console.WriteLine(num);
}

Pirmoje eilutėje sukuriame skaičių nuo 1 iki 10 rinkinį, tada jį filtruojame, kviesdami metodą Where, kuriam kaip filtravimo parametrą perduodame lambda išraišką, i => (i % 3 == 0), o praėjusius filtrą skaičius (3, 6, 9) atspausdiname.
Jei šis pavyzdys pasirodė nesuprantamas, pabandysiu jį paaiškinti iš istorinės perspektyvos.
Norėdami išlaikyti pavyzdžio esmę ir jį parašyti C# 1.0 ar kita kalba, kuri neturi nieko panašaus į lambda išraiškas, turėtume pasirašyti metodą Where, kurio parametras būtų rodyklė į filtravimo funkciją (arba delegatas C# kalboje). Šis metodas atliktų filtravimą — eitų per visą kolekciją ir tikrintų, ar objektai tenkina filtravimo kriterijų (šiuo atveju filtravimo funkcija tikrina ar skaičius dalijasi iš 3). Formaliau tokie filtrai vadinami predikatais (angl. predicates).
Viskas atrodytų maždaug taip:

// Filtro aprašymas.
public delegate bool Filter(int num);
 
// Bendras filtravimo metodas.
public IEnumerable Where(IEnumerable sequence, Filter filter)
{
    ArrayList new_seq = new ArrayList();
 
    foreach (int num in sequence)
    {
        if (filter(num))
            new_seq.Add(num);
    }
 
    return new_seq;
}
 
// Konkretaus filtro implementacija.
public bool DividesByThree(int num)
{
    return num % 3 == 0;
}
 
// Ir pats pavyzdys.
 
int[] numbers = new int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
 
foreach (int num in Where(numbers, DividesByThree)) {
    Console.WriteLine(num);
}

Iš šio pavyzdžio turėtų būti aišku, kodėl sakiau, kad C# 1.0 man asocijavosi su darbu, teko prirašyti daug neesminio ir nuobodaus kodo. Šis kodas turi ir kitokių problemų — metodas veikia tik su sveikais skaičiais, o norint metodą apibendrinti, reikėtų atlikinėti daug duomenų tipų tikrinimų, patys filtrai aprašyti kažkur toli nuo kodo, kuris juos naudoja ir t.t.
Tokios naujovės C# 2.0 kalboje, kaip anoniminiai metodai ir bendrybės, šias problemas iš esmės išsprendė — dėka bendrybių filtravimą galime apibendrinti bet kokiems duomenų tipams, o su anoniminiais metodais, filtro kodą galime perduoti tiesiog kaip parametrą:

int[] numbers = new int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
foreach (int num in Where(numbers, 
                          delegate(int i) { return i % 3 == 0; }
                          ))
    Console.WriteLine(num);

Šis kodas jau nelabai kuo skiriasi nuo pačio pirmo varianto, išskyrus tai, kad C# 3.0 jau turi Where ir Range metodus bei ten vietoj anoniminio metodo panaudojome lambda išraišką. Be Where, visos kolekcijos C# 3.0 turi daug kitų praplėtimo metodų, tokių kaip Any, Contains, Average, Except ir t.t. Visiems jiems galime perduoti lambda išraiškas arba anoniminius metodus, taip atrinkdami tik mus dominančius kolekcijos narius, pvz.:

if (numbers.Any(i => i > 10)) {
    Console.WriteLine("Kolekcijoje yra skaičių didesnių nei 10 !");
}

Išraiškų medžiai
Gali pasirodyti, kad apart kompaktiškesnės sintaksės anoniminiai metodai nelabai kuo skiriasi nuo lambda išraiškų, tačiau yra dar viena C# 3.0 naujovė, kur pasireiškia pastarosios — tai išraiškų medžiai (angl. expression trees). Ši naujovė dažnai pamirštama pristatinėjant , nors man atrodo, kad ji yra viena esminių.
Kadangi lambda išraišką galime įsivaizduoti kaip metodą sukurtą „ant smūgio“, tai ją galime priskirti delegatui — toks kodas neturėtų stebinti:

Func<int, bool> DividesByThree = (  i => (i % 3 == 0)  );

Func yra iš anksto aprašytas bendrybinis delegatas, šiuo atveju analogiškas mūsų anksčiau aprašytam Filter delegatui, o visa ši eilutė iš esmės analogiška anksčiau aprašytam metodui DividesByThree — šį delegatą galėtume panaudoti kaip filtrą mūsų parašytai funkcijai Where.
Tačiau šis kodas jau gali priversti pasikrapštyti pakaušį:

Expression<Func<int, bool>> DBT_Expression = (  i => (i % 3 == 0)  );

Čia sukūrėme ne pačią išraišką, o tik jos aprašymą. Iš aprašymo galime susikurti ir išraišką bei ją įvykdyti:

Func<int, bool> DividesByThree = DBT_Expression.Compile();
 
if (DividesByThree.Invoke(x)) {
    Console.WriteLine("x dalyjasi iš 3.");
}

Iš tikrųjų, pirmu atveju kompiliatorius tiesiog sukompiliuos lambda išraišką į atitinkamas IL instrukcijas. Tuo tarpu antru atveju, kompiliatorius pamatęs Expression tipą pasielgs kitaip — kodą sukompiliuos į IL instrukcijas, kurios sukonstruos išraišką aprašančių objektų medį (iš čia ir kilo šios naujovės pavadinimas). Kitais žodžiais tariant, antrasis variantas yra ekvivalentiškas tokiam kodui:

ParameterExpression i_arg = Expression.Parameter(typeof(int), "i");
Expression<Func<int, bool>> DBT_Expression 
  = Expression.Lambda<Func<int, bool>>
    (
       Expression.Equal
       (
           Expression.Modulo(i_arg, Expression.Constant(3)),
           Expression.Constant(0)
       ),
       i_arg
    );

Dabar akivaizdžiai matyti, kad DBT_Expression yra tiesiog lambda išraiškos aprašymas — tai yra tiesiog duomenys apibūdinantys išraišką, o ne pati išraiška, kaip pirmu atveju.
Prisipažinsiu, pirmą kartą pamatęs tokį kodą sėdėjau netekęs amo (panašiai buvo pirmą kartą C++ kalboje pamačius lambda išraiškas ar meta programavimą naudojant šablonus). Matyti kažką panašaus interpretuojamoje kalboje lyg ir nieko įspūdingo, bet C# yra kompiliuojama kalba! Kur ši metodika panaudojama, papasakosiu vėliau.


Pagaliau priėjome prie esminės C# 3.0 naujovės. Jei trumpai — (Language INtegrated Query) yra būdas ištraukti duomenis iš įvairių duomenų kolekcijų — panašiai kaip su SQL imame duomenis iš duomenų bazės, taip ir su imame duomenis iš C# duomenų struktūrų, pvz., masyvų, kolekcijų, xml šakų ar netgi tos pačios duomenų bazės. Štai paprasčiausias pavyzdys iš kolekcijos išrenkantis tik skaičius, kurie didesni už penkis:

var numbers = new List<int>() { 2, 6, 8, 4, 1, 9, 3, 1 };
var new_nums = from int num in numbers where num > 5 select num;
 
foreach (int num in new_nums) {
Console.WriteLine(num + " yra daugiau nei 5");
}

Žinoma, nebūtina apsiriboti skaičiais, galime pritaikyti dirbdami su bet kokiais duomenimis:

var families = new FamilyTreeList()
{
    { "Antanas", new Children() { "Juozukas", "Onytė"               } },
    { "Ignas",   new Children() { "Marytė"                          } },
    { "Jonas",   new Children() { "Petriukas", "Mantukas", "Agnytė" } }
};
 
var f = (from family in families
         orderby family.Kids.Count descending
         select new { Name = family.Name, Count = family.Kids.Count }
        ).First();
 
Console.WriteLine(f.Name + " turi daugiausia, net " + f.Count + " vaikus!");

Iš šio kodo sužinotume, kad Jonas turi daugiausiai vaikų — net tris!
savo arsenale turi praktiškai visų pagrindinių SQL raktožodžių atitikmenis — galime atlikti grupavimą, rūšiavimą, apjungimą ir t.t.
Iš techninės pusės, visi šie raktažodžiai (select, order, join, from ir t.t.), yra tik sintaksės supaprastinimas, sakiniai iš tiesų yra jau mūsų aptartų praplėtimo metodų iškvietimų su lambda išraiškomis grandinė. Pvz., pirmasis pavyzdys yra ekvivalentiškas tokiam kodui:

var new_nums = numbers.Where(i => i > 5).Select(i => i);
 
foreach (int num in new_nums)
Console.WriteLine(num + " yra daugiau nei 5");
}

Turbūt kyla klausimas, ar toks duomenų paėmimas nėra lėtas — atrodytų, kad kiekvienas toks papildomas iškvietimas, kaip Where ir Select, turėtų pereiti per visą kolekciją ir atlikti su ja kažkokius veiksmus. Tačiau čia visu grožiu pasireiškia mūsų jau aptarti išraiškų medžiai.
išraiškos nėra vykdomos jas aprašant — duomenų paėmimas vykdomas tik bandant pasinaudoti išraiškos rezultatu. Aprašyta išraiška yra sukompiliuojama į išraiškų medį, kuris yra išanalizuojamas ir optimizuojamas taip, kad duomenų paėmimas būtų optimalus greičio atžvilgiu, t.y. galime būti tikri, kad duomenys nebus skaitomi kelis kartus be reikalo. Ši optimizacija ypač akivaizdi naudojant su duomenų bazėmis (DLINQ), ten išraiškos praktiškai išverčiamos į analogišką SQL sakinį ir į duomenų bazę kreipiamasi tik vieną kartą.
Be to šiuo metu Microsoft kuria PLINQ technologiją — praplėtimą, kuris pilnai išnaudos paralelizmą, t.y. užklausų vykdymas bus paskirstytas visiems procesoriams ir procesorių branduoliams.

Taigi rašydami išraiškas ne tik, kad galime susikoncentruoti į algoritmo rašymą nekreipdami dėmesio į implementacijos smulkmenas, bet ir galime būti tikri, kad kodas bus optimizuotas turbūt geriau nei pavyktų mums patiems.

Pasinaudokime Amazon Web Services su XLINQ
Prieš daugiau nei metus rašiau programą Krabas, kuri pasinaudodama Amazon Web Services paslauga gali atpažinti muzikinį albumą iš katalogo turinio ir jį sutvarkyti, t.y. teisingai pervadinti failus, atsiųsti albumo viršelį, surašyti ID3 žymes į mp3 failus ir t.t.
Atlikti paiešką pasinaudojant Amazon Web Services labai lengva. Užsiregistravus šią paslaugą, tereikia surašyti savo užklausą ir jos parametrus į URL adresą ir ten nuėję pamatysime xml failą su paieškos rezultatais (pavyzdys čia).
Programą rašiau su C++, o XML failus apdorojau su TinyXML biblioteka. Neslėpsiu, net paprasto failo apdorojimas buvo gana sunki ir daug kruopštumo bei dėmesio reikalaujanti procedūra. Programos kodo dalį, kuri apdorojo amazon gražintus XML failus galite pamatyti čia (visi amazon failai).
Dabar pabandžiau tokią paiešką atlikti pasinaudodamas XLINQ ( skirtas xml). Pats nustebau, kaip tai buvo lengva. Štai kaip atrodė paieškos kodas:

public static List<Album> SearchForAlbums(string query)
{
    string url = GetRequestUrl(query);
    string xml = DownloadFile(url);
 
    XDocument results = XDocument.Parse(xml);
 
    var albums = from album in results.Element("ItemSearchResponse")
                                      .Element("Items")
                                      .Elements("Item")
      where album.Element("MediumImage") != null
        select new Album
        {
          Artist = (string)album.Element("ItemAttributes").Element("Artist"),
          Title = (string)album.Element("ItemAttributes").Element("Title"),
          DetailUrl = (string)album.Element("DetailPageURL"),
          ImageUrl = (string)album.Element("MediumImage").Element("URL")
        };
 
    return albums.ToList();
}

Tiesa, šis kodas šiek tiek apkarpytas, iš tikrųjų gauta XML struktūra naudoja vardų sritis, tai kodas yra šiek tiek griozdiškesnis, bet tai esmės nekeičia. Visa amazon paieškos implementacija užėmė tik kiek daugiau nei 100 eilučių, o parašyti užtruko tik apie valandą! Kas be ko, ši implementacija nepalyginamai skaitomesnė ir aiškesnė nei mano C++ variantas. Žymiai ilgiau užtrukau bandydamas padaryti gražų puslapį rodantį paieškos rezultatus (ir tai nelabai pavyko):
C# screenshot

Pabaigai

Šiame straipsnyje apžvelgiau mano galva esmines C# 3.0 naujoves. Kai kurios jų gali pasirodyti per sudėtingos, kad būtų galima taikyti kasdieniuose uždaviniuose (pvz., išraiškų medžiai), tačiau man visada imponuoja kalbos, kuriose yra kažkas tokio, kas priverčia palaužyti galvą (už tai iki šiol mėgstu C++) — tai padaro kalbą įdomią. O štai yra tas dalykas, kurį reikėtų taikyti kuo dažniau — tai ne tik elegantiškas, deklaratyvus ir optimizuotas būdas dirbti su duomenimis, svarbiausia, kad tai yra smagus programavimo būdas, atitolinantis mus nuo neesminių smulkmenų ir leidžiantis tiesiog programuoti.
Ne vieno kolegos veide teko matyti šypseną nuo ausies iki ausies padirbėjus su naujuoju C# — juk galų gale, dėl to ir pasirinkome šią profesiją.

Nuorodos
101 LINQ pavyzdys
C# 3.0 specifikacija
Pilnas amazon paieškos puslapio kodas
AmazonSearch.cs kodas

Atnaujinimas 2007-10-29 23:00: per klaidą nebuvo prisegtas paveiksliukas.

Panašūs straipsniai


“Įdomesnis ir linksmesnis programavimas su C# 3.0” komentarų: 12

  1. Saulius

    Trumpas disclamer’is visiems MS hateriams:

    Straipsnis nesako, kad C# 3.0 yra įdomesnis/linksmesnis už , o sako, kad C# 3.0 > C# 1.0 ir 2.0. Priešingai, aš netgi pagyriau ruby ir užvažiavau ant C# 1.0 (!).

    Specialiai ZaZa: visi linkai po straipsniu veikia - ne visi .NET developeriai yra melagiai :)

    Beje, tiems, kurie įkėlė straipsnį - trūksta screenshoto. O jei jo nutarėt dėl kažkokių priežasčių nedėt, būkit malonūs, pakeiskit dvitaškį pastraipos pabaigoj į tašką. Ir C reiktų rašyt iš didžiosios. Dėkui.

  2. Saulius

    Ten po “už” buvo <your favourite programming language/framework> - wordpressas nepraleido. Ir kodėl aš negaliu savo komentarų editinti ? :(

  3. kik

    Pritariu Sauliui dėl to, kad neina redaguoti komentarų, tas užknisa, kai matai, kad galėjai kitaip parašyti ir negali pataisyti tai taip žlugdo… P.S. Labai kritikuojančiai parašiau, bet tikiuosi, bus galima redaguoti, bent jau redaguoti tuos pranešimus kuriuos parašei šiandien, jeigu galvojama, kad pakoregavimas būtų bandymas įkišti šiukšles.

  4. Aras Pranckevičius

    +1

    Smagu, kad pradedama nuo lambda ir išraiškų medžių, ir taip prieinama iki LINQ. O ne kaip kad paprastai būna “LINQ tai kaip SQL!!11eleven”.

  5. Giedrius

    siaip man baisu, kad mano kolektyve neatsirastu zmoniu, kurie turi iproti su logaritminem liniuotem vinis kalti - kad nepradetu tokiu dalyku naudoti kur nereikia, nes kodo skaitomumui tai nepadeda. Kad ir kaip gerbciau C++, nenoreciau, kad C# taptu C++ klonu, tikesimes kad C# 4.0 makrosai neatsiras :)

  6. Saulius

    Teisingai panaudotas LINQ tik padidina skaitomumą - vis dėlto yra ir bus tokių žmonių, kurie ir iš jo sugebės padaryt spagetį :(
    O kas dėl makrosų, tai tokie gi yra, tik supaprastinti biški :) O kad priartėt prie C++’o C#’ui iš esmės, mano nuomone, beliko pridėt generics’us su value typed parametrais. Bet kaip suprantu, microsoftas to daryt artimiausiu metu nežada.

  7. Saulius

    Ir šiaip, gal reiktų tag’o .NET straipsniui - LINQ ir lambdos yra visose naujos framework’o versijos kalbose.

  8. jungle

    o nuo kada cia genericsai negali tureti value typed parametru ? :)

  9. Saulius

    Gal nuo visada ? :)

  10. ElbRus

    graziai parasyta, patiko :thumb up:

  11. Pixel.lt ir “Microsoft Lietuva” konkurso nugalėtojai » Pixel.lt

    […] nugalėtojai yra… Nugalėtojai: Absoliutus nugalėtojas - Saulius ir jo straipnis “Įdomesnis ir linksmesnis programavimas su C# 3.0“. Šis straipsnis užėmė pirmąją vietą ir .NET, ir programavimo kategorijose. Taip pat […]

  12. Profas

    O tu išmanai savo reikalą.

Rašyti komentarą

Jūs privalote prisijungti jeigu norite rašyti komentarą.