Pradžia / Programavimas / C# ir .NET kelias

C# ir .NET kelias

Nuo paprastos konsolės iki enterprise sistemų

Kai pirmą kartą paleidžiai Visual Studio ir parašei Console.WriteLine("Hello, World!");, tikriausiai negalvojai, kad tai tik pirmas žingsnis į vieną iš labiausiai išvystytų programavimo ekosistemų pasaulyje. C# ir .NET – tai ne tik kalba ir framework’as. Tai visas mąstymo būdas, kaip kurti programinę įrangą, kuri veikia, kuri plečiasi ir kuri nesugriūva po pirmo rimtesnio krūvio.

.NET istorija prasidėjo 2002-aisiais, kai Microsoft išleido pirmąją versiją. Tuo metu tai buvo atsakas į Java – bandymas sukurti kažką panašaus, bet geriau integruoto į Windows aplinką. Pradinė vizija buvo gana ribota: Windows aplikacijos, ASP.NET puslapiai, gal koks WinForms projektas. Bet laikas parodė, kad ši platforma turėjo potencialo kur kas daugiau.

Šiandien, kalbant apie .NET 8 ar .NET 9, situacija kardinaliai pasikeitė. Cross-platform palaikymas, cloud-native architektūros, microservices, Blazor, MAUI – visa tai yra vienos ekosistemos dalis. Ir C# kaip kalba evoliucionavo taip sparčiai, kad programuotojai, kurie paskutinį kartą ją naudojo prieš penkerius metus, gali tiesiog neatpažinti kai kurių naujų konstrukcijų.

C# kaip kalba: kodėl ji vis dar aktuali

Yra tokia tendencija technologijų pasaulyje – kas penkeri metai atsiranda nauja „revoliucinė” kalba, kuri turėtų viską pakeisti. Rust, Go, Kotlin, Dart… Visos jos turi savų privalumų, ir niekas neginčija jų vertės. Bet C# išlieka relevantiškas dėl kelių labai konkrečių priežasčių.

Pirma, kalba nuolat tobulėja, bet nepraranda atgalinio suderinamumo. Kodas, parašytas C# 2.0, vis dar kompiliuojasi šiandien. Tai skamba trivialiai, bet enterprise aplinkoje tai yra milžiniškas privalumas. Niekas nenori perrašyti milijono kodo eilučių vien todėl, kad kalba nusprendė pakeisti savo sintaksę.

Antra, C# turi vieną geriausių tipų sistemų tarp mainstream kalbų. Nullable reference types, pattern matching, records, generic constraints – tai ne tik sintaksinis cukrus. Tai įrankiai, leidžiantys rašyti kodą, kuriame klaidos sugaunamos kompiliacijos metu, o ne production aplinkoje trečią nakties valandą.

Štai paprastas pavyzdys, kaip pattern matching gali pakeisti tradicinį if-else chaosą:


string KlasifikuotiTemperatura(double temp) => temp switch
{
    < 0 => "Šalta kaip Sibire",
    >= 0 and < 15 => "Vėsu",
    >= 15 and < 25 => "Komfortiškai",
    >= 25 and < 35 => "Šilta",
    _ => "Karšta, bėk prie vandens"
};

Tai ne tik gražiau atrodo – tai ir saugiau, nes kompiliatorius perspės, jei neapdorosite kurio nors atvejo. Trečia priežastis – LINQ. Jei dar nedirbote su LINQ arba manote, kad tai tik paprastas foreach pakaitalas, labai klystate. LINQ yra funkcinio programavimo principų integravimas į imperatyvią kalbą, ir tai veikia stebėtinai gerai.

.NET ekosistema: kas po gaubtu

Daugelis pradedančiųjų programuotojų galvoja apie .NET kaip apie vieną didelę biblioteką. Realybė yra kur kas sudėtingesnė ir įdomesnė. .NET ekosistema šiandien susideda iš kelių sluoksnių, ir suprasti jų skirtumus yra esminis dalykas, jei norite rimtai dirbti šioje srityje.

CLR (Common Language Runtime) – tai pagrindas, ant kurio viskas stovi. Jis valdo atmintį per garbage collection, kompiliuoja IL (Intermediate Language) kodą į mašininį kodą per JIT (Just-In-Time) kompiliatorių, tvarko exception handling ir thread management. Daugelis programuotojų niekada tiesiogiai nesusiduria su CLR, bet suprasti, kaip jis veikia, padeda rašyti efektyvesnį kodą.

Garbage collector, pavyzdžiui, nėra paprastas „išvalo atmintį kai reikia” mechanizmas. Jis naudoja generacijų sistemą (Gen 0, Gen 1, Gen 2), ir žinojimas, kaip objektai juda tarp generacijų, gali padėti optimizuoti aplikacijas, kuriose atmintis yra kritinis resursas. Jei kuriate high-throughput sistemą ir pastebite, kad GC pauzės kelia problemų, galbūt verta pažvelgti į ArrayPool<T> arba Span<T> – įrankius, leidžiančius sumažinti heap alokacijas.

ASP.NET Core – tai web development istorija .NET pasaulyje. Nuo sunkaus, Windows-only ASP.NET iki lengvo, cross-platform ASP.NET Core – tai buvo rimta transformacija. Šiandien ASP.NET Core yra vienas greičiausių web framework’ų apskritai, ir tai nėra marketing’o teiginys – TechEmpower benchmark’ai tai patvirtina nuolat.

Minimal API, pristatytas .NET 6, pakeitė tai, kaip galvojame apie mažas HTTP aplikacijas:


var app = WebApplication.Create(args);

app.MapGet("/produktai/{id}", async (int id, IProductService service) =>
{
    var produktas = await service.GautiPagalId(id);
    return produktas is null ? Results.NotFound() : Results.Ok(produktas);
});

app.Run();

Jokių kontrolerių, jokio boilerplate. Tiesiog endpoint’as ir logika. Tai nereiškia, kad tradiciniai kontroleriai nebereikalingi – didelėse sistemose jie vis dar turi prasmę. Bet turėti pasirinkimą yra gerai.

Asinchroninis programavimas: async/await be mitų

Jei yra viena tema, kuri sukelia daugiausiai painiavos C# programuotojų bendruomenėje, tai yra async/await. Internete pilna straipsnių, kuriuose teigiama, kad async/await „sukuria naujus thread’us” arba kad tai „lygiagretumo mechanizmas”. Abu teiginiai yra neteisingi, ir šis nesusipratimas veda prie rimtų klaidų production kode.

Async/await yra asinchroniškumo, o ne lygiagretaus vykdymo mechanizmas. Skirtumas yra esminis. Kai rašote:


var duomenys = await httpClient.GetStringAsync("https://api.example.com/data");

…jūs nesakote „vykdyk tai kitame thread’e”. Jūs sakote „paleisk šią I/O operaciją ir grąžink thread’ą į thread pool, kol operacija baigsis”. Kai operacija baigsis, kodas bus tęsiamas – galbūt tame pačiame thread’e, galbūt kitame, priklausomai nuo konteksto.

Tai reiškia, kad ASP.NET Core serveris, naudojantis async/await teisingai, gali aptarnauti tūkstančius vienalaikių užklausų su labai nedideliu thread’ų skaičiumi. Serveris neblokuojamas laukiant duomenų bazės atsakymo – jis tiesiog tvarko kitas užklausas tuo tarpu.

Dažniausios klaidos, kurių reikia vengti:

  • .Result arba .Wait() naudojimas – tai sinchroniškai blokuoja thread’ą ir gali sukelti deadlock’us, ypač ASP.NET aplinkoje
  • async void metodai – išskyrus event handler’ius, to daryti nereikia. Exception’ai iš async void metodų negali būti sugauti įprastu būdu
  • Nereikalingas await – jei metodas tiesiog grąžina Task iš kito metodo, nebūtina rašyti await. return await SomeMethod(); galima pakeisti į return SomeMethod();, jei nereikia try/catch
  • ConfigureAwait(false) pamiršimas – bibliotekų kode tai svarbu, nes išvengiama nereikalingo konteksto fiksavimo

Praktinis patarimas: jei dirbate su CPU-intensive operacijomis (skaičiavimai, duomenų apdorojimas), async/await jums nepadės. Čia reikia Task.Run() arba Parallel klasės. Async/await skirtas I/O operacijoms – tinklas, failų sistema, duomenų bazės.

Dependency Injection ir architektūra, kuri nesubyra

Dependency Injection (DI) .NET Core aplinkoje yra built-in, ir tai vienas geriausių sprendimų, kuriuos Microsoft priėmė pertvarkydami platformą. Bet DI nėra magija – tai principas, ir jei jo nesuprantate, gausite ne lankstų kodą, o tiesiog kitokio tipo chaosą.

Pagrindinis DI tikslas yra paprastas: klasė neturėtų pati kurti savo priklausomybių. Vietoj to, ji turėtų jas gauti iš išorės. Tai leidžia lengvai keisti implementacijas, testuoti klases izoliuotai ir aiškiai matyti, nuo ko kiekviena klasė priklauso.

.NET DI konteineryje yra trys gyvavimo ciklai, ir jų pasirinkimas yra kritiškai svarbus:

  • Singleton – vienas objektas visai aplikacijos gyvavimo trukmei. Tinka stateless servisams, konfigūracijai, cache’ui. Bet jei Singleton’as turi mutable state ir naudojamas iš kelių thread’ų – turite problemą
  • Scoped – vienas objektas vienai HTTP užklausai (arba vienam scope’ui). Tai standartinis pasirinkimas daugumai servisų ASP.NET Core aplikacijose. DbContext turėtų būti Scoped
  • Transient – naujas objektas kiekvieną kartą, kai prašoma. Tinka lengviems, stateless servisams. Bet jei Transient servisas yra sunkus (turi daug resursų), tai gali paveikti performance

Dažna klaida – „Captive Dependency” problema: Singleton servisas, kuris turi Scoped priklausomybę. Scoped objektas gyvena tol, kol gyvena Singleton – tai yra visą aplikacijos gyvavimo laiką, o tai sulaužo Scoped semantiką. .NET turi validacijos mechanizmą, kuris tai aptinka development aplinkoje, bet production aplinkoje jis išjungtas dėl performance priežasčių.

Kalbant apie architektūrą plačiau – Clean Architecture arba Vertical Slice Architecture yra dvi populiarios paradigmos .NET pasaulyje. Clean Architecture (kurią popularizavo Robert Martin) siūlo aiškų sluoksnių atskyrimą: Domain, Application, Infrastructure, Presentation. Vertical Slice Architecture siūlo organizuoti kodą pagal funkcionalumą, o ne pagal techninius sluoksnius. Nė viena nėra universaliai geresnė – pasirinkimas priklauso nuo projekto dydžio, komandos ir reikalavimų.

Testavimas: kodas, kuriuo galima pasitikėti

Kalbėti apie C# ir .NET be testavimo būtų kaip kalbėti apie automobilį be stabdžių. Techniškai jis važiuoja, bet ilgai neišvažiuosi. .NET ekosistema turi puikų testavimo įrankių rinkinį, ir naudoti juos yra ne tik „gera praktika” – tai investicija, kuri grįžta su kaupu.

Unit testams populiariausi framework’ai yra xUnit, NUnit ir MSTest. xUnit šiandien yra de facto standartas naujuose projektuose – jis turi švaresnę sintaksę ir geresnę izoliaciją tarp testų (kiekvienas testas gauna naują klasės instanciją).

Mocking’ui Moq arba NSubstitute yra standartiniai pasirinkimai. NSubstitute turi šiek tiek švaresnę sintaksę, bet Moq turi didesnę bendruomenę ir daugiau dokumentacijos.

Praktinis patarimas dėl testų rašymo: laikykitės AAA (Arrange, Act, Assert) struktūros, bet nesivaikykite 100% code coverage kaip tikslo. 100% coverage su prastos kokybės testais yra bevertis. Geriau turėti 70% coverage su testais, kurie tikrina realią logiką ir kraštines situacijas.

Integration testai ASP.NET Core aplinkoje tapo labai patogūs su WebApplicationFactory<T>:


public class ProduktaiApiTestai : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _klientas;

    public ProduktaiApiTestai(WebApplicationFactory<Program> fabrika)
    {
        _klientas = fabrika.CreateClient();
    }

    [Fact]
    public async Task GautiProduktą_EsantKorektiškaId_GrąžinaOk()
    {
        var atsakymas = await _klientas.GetAsync("/produktai/1");
        atsakymas.EnsureSuccessStatusCode();
    }
}

Tai leidžia testuoti visą HTTP pipeline’ą – middleware, routing, serialization – be realaus serverio paleidimo. Tai greita, patikima ir labai artima realiam scenarijui.

Performance: kai greitis tampa reikalavimu

Yra mitas, kad .NET yra lėtas. Jis buvo iš dalies teisingas prieš dešimt metų. Šiandien tai tiesiog netiesa. .NET 8 benchmarkuose reguliariai lenkia Go ir yra artimas C++ tam tikruose scenarijuose. Bet tai nereiškia, kad galite rašyti bet kaip ir tikėtis gerų rezultatų.

Keletas konkrečių rekomendacijų, kurios realiai veikia:

Span<T> ir Memory<T> – šios struktūros leidžia dirbti su atminties regionais be papildomų alokacijų. Jei turite metodą, kuris apdoroja string’ą ir kuria daug substring’ų, pakeiskite juos į ReadOnlySpan<char> ir pastebėsite reikšmingą skirtumą GC spaudime.

StringBuilder vietoj string concatenation – tai klasika, bet vis dar aktualu. String’ai .NET yra immutable, todėl kiekvienas + operatorius sukuria naują objektą. Cikle tai gali tapti rimta problema.

ValueTask vietoj Task – kai metodas dažnai grąžina jau užbaigtą rezultatą sinchroniškai, ValueTask gali sumažinti heap alokacijas. Bet nenaudokite jo visur – jis turi savo apribojimų ir neteisingas naudojimas gali sukelti subtilių klaidų.

BenchmarkDotNet – jei rimtai domitės performance, šis įrankis yra privalomas. Jis teisingai šildo JIT, atlieka statistiškai reikšmingus matavimus ir parodo ne tik laiką, bet ir atminties alokacijas. Neoptimizuokite pagal intuiciją – matuokite.

Svarbu suprasti, kad premature optimization yra tikras pavojus. Prieš optimizuojant reikia profiliuoti. Dotnet-trace, dotnet-counters, Visual Studio Profiler – tai įrankiai, kurie parodo, kur realiai eikvojamas laikas, o ne kur jums atrodo, kad jis eikvojamas. Dažnai paaiškėja, kad bottleneck yra duomenų bazės užklausa, o ne C# kodas.

Kur .NET kelias veda toliau

Žiūrint į tai, kur .NET ekosistema juda, sunku nepastebėti kelių aiškių tendencijų. Cloud-native development tampa ne pasirinkimu, o standartu. .NET turi puikią integraciją su Kubernetes per Aspire – naują Microsoft stack’ą, skirtą distributed aplikacijų kūrimui ir orchestravimui development aplinkoje. Tai dar jauna technologija, bet kryptis aiški.

Blazor ir toliau evoliucionuoja. Blazor United (dabar žinomas kaip Blazor Web App .NET 8 kontekste) sujungia server-side ir client-side rendering’ą į vieną modelį, leidžiantį rinktis rendering strategiją net komponento lygmeniu. Tai ambicingas projektas, ir nors WebAssembly performance vis dar atsilieka nuo JavaScript tam tikrose srityse, skirtumas mažėja.

AI integracijos – Microsoft Semantic Kernel yra .NET biblioteka, skirta AI aplikacijų kūrimui. Jei dirbate su LLM modeliais, RAG (Retrieval Augmented Generation) architektūromis ar AI agentais, Semantic Kernel suteikia struktūruotą būdą tai daryti C# aplinkoje.

MAUI, nors ir turėjo nelengvą pradžią, toliau tobulėja kaip cross-platform UI framework’as. Jei reikia kurti native aplikacijas Windows, macOS, iOS ir Android iš vieno kodo bazės, MAUI yra vienintelis oficialus .NET kelias. Alternatyvos kaip Avalonia turi savo bendruomenę ir tam tikrais aspektais yra brandesnės.

Svarbiausia, ką galima pasakyti apie C# ir .NET kelią – tai nėra sprint’as. Tai maratonas. Ekosistema yra tokia plati, kad net patyrę programuotojai nuolat atranda naujų dalykų. Pradėkite nuo pagrindų, supraskite, kaip veikia CLR, išmokite async/await ne tik sintaksiškai, bet ir konceptualiai, rašykite testus nuo pat pradžių. Ir svarbiausia – kurkite realius projektus. Teorija be praktikos šioje srityje yra bevertė. Kiekviena klaida production aplinkoje (arba bent jau staging) išmoko daugiau nei dešimt straipsnių. Ir tai, kad šiandien kažkas neveikia taip, kaip tikėjotės – tai ne nesėkmė. Tai tiesiog kitas žingsnis tame pačiame kelyje.