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ę — LINQ — bei pateiksiu konkretų pavyzdį, parodantį, koks smagus tai dalykas.
C# 3.0 naujovės
LINQ, be abejo, yra C# 3.0 pažiba, tačiau kalboje atsirado ir kitų paminėjimo vertų naujovių, daugelis — būtent dėl LINQ. Nesigilinsiu į anoniminius duomenų tipus, praplėtimo metodus ir pan., nes tai daugiau techniniai dalykai, apie kuriuos galima pasiskaityti bet kokiame įvade į LINQ.
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 LINQ, 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.
LINQ
Pagaliau priėjome prie esminės C# 3.0 naujovės. Jei trumpai — LINQ (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 LINQ 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, LINQ 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!
LINQ 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, LINQ 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.
LINQ išraiškos nėra vykdomos jas aprašant — duomenų paėmimas vykdomas tik bandant pasinaudoti išraiškos rezultatu. Aprašyta LINQ 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 LINQ su duomenų bazėmis (DLINQ), ten LINQ 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ą — LINQ praplėtimą, kuris pilnai išnaudos paralelizmą, t.y. užklausų vykdymas bus paskirstytas visiems procesoriams ir procesorių branduoliams.
Taigi rašydami LINQ 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 (LINQ 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):
![]()
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 LINQ 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.
Konkursas
2007-10-29 | 20:34
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.
2007-10-29 | 20:39
Ten po “už” buvo <your favourite programming language/framework> - wordpressas nepraleido. Ir kodėl aš negaliu savo komentarų editinti ? :(
2007-10-29 | 21:24
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.
2007-10-29 | 21:38
+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”.
2007-10-30 | 11:51
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 :)
2007-10-30 | 12:33
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.
2007-10-30 | 18:37
Ir šiaip, gal reiktų tag’o .NET straipsniui - LINQ ir lambdos yra visose naujos framework’o versijos kalbose.
2007-10-31 | 12:42
o nuo kada cia genericsai negali tureti value typed parametru ? :)
2007-10-31 | 12:53
Gal nuo visada ? :)
2007-10-31 | 13:09
graziai parasyta, patiko :thumb up:
2007-11-13 | 17:07
[…] 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 […]
2008-06-16 | 2:13
O tu išmanai savo reikalą.