A C# nyelv mindegyik korábbi verziója jelentős újításokat hozott, nincs ez másként a 4.0-ás verziónál sem. A nyelv fejlesztése során mindig is figyelembe vették az aktuális tendenciákat, így kerültek bele a korábbi verziókban dekleratív, funkcionális nyelvi elemek. A dinamikus, gyengén típusos programozási nyelvek (elsősorban szkriptnyelvek) térnyerése szintén egy ilyen tendencia és a 4.0-ás .NET platformot már dinamikus nyelvek támogatására is felkészítették (lásd IronRuby, IronPython). A platform motorja nem csak egy ráncfelvarrást kapott, hanem mélyreható változásokon esett keresztül. Ennek egyik fontos eleme, hogy mostantól kezdve lehetővé válik a side-by-side kódvégrehajtás, azaz egy processzben két különböző verziójú CLR-hez tartozó assembly-t is betölthetünk úgy, hogy mindegyik a saját verziójú futtatókörnyezetét használja, szemben a korábbi megoldással, ahol is a szóban forgó assembly-k a 2.0-ás CLR-t használták megosztva. A CLR mellé egy dinamikus programozási nyelveket kiszolgáló futtatókörnyezet is bekerül, ez a DLR.
A C# 4.0 újdonságai:
Egy megjegyzés azok számára, akiknek nem mindegyik újdonság tűnik "újnak": a Code Contracts és a deklaratív párhuzamosságot támogató Parallel Library korábban is elérhetők voltak, azonban mostantól a .NET keretrendszer részei.
A dinamikus típusok esetén fordítási időben nem ismert egy változó vagy kifejezés típusa, nem dönthető el, hogy milyen metódusokkal rendelkezik, azok milyen paramétereket fogadhatnak, stb. Egy ilyen, a típusbiztonságot ennyire felrugó konstrukció nagyon messze áll a C# nyelvtől, ahol is nem létezhet változó statikus típus nélkül. Ez – bár meglepő – továbbra is fennáll, ugyanis a dynamic kulcsszóval bevezetett változók rendelkeznek statikus típussal is, ami a dynamic. A dinamikus típusú változók valójában object típusúak, ahogy az az IL kódban látható is. Így biztosított, hogy a változó bármilyen típusú értéket megkapjon, ugyanis az object minden osztály őse. Megjegyzendő, hogy a dynamic kulcsszó ugyan, azonban nem foglalt szó, így változónévként használható.
A metódushívások fordításra kerülnek, azonban csak futásidőben derül ki, hogy a változó tényleges típusa megvalósítja-e a metódust az adott szignatúra mellett. A metódusok mellett ugyanez a helyzet az operátorokkal és indexerekkel, mező- és tulajdonságelérőkkel is. Amennyiben az adott metódus nem hívható az objektumon, futásidőben egy RuntimeBinderException típusú kivétel váltódik ki. A legtöbb olyan kifejezés típusa, amely dinamikus típusú elemet tartalmaz, maga is dinamikus típusú lesz:
Ahhoz, hogy dinamikus változót tartalmazó kifejezés típusa ne dinamikus legyen, konverzió szükséges:
A fenti példában a Pelda osztaly konstruktora egy int paraméterrel rendelkezik. Dinamikus és nem dinamikus típusok között a konverzió implicit végbemegy. A fenti példákban láthattuk, hogy egy dinamikusként deklarált változó explicit konverzió nélkül fogad el int-et (és amúgy bármi mást is), és ez történik paraméterátadás esetén is. Az implicit konverzió miatt az alábbi programsorok mindegyike helyes:
Az object-ben található GetType() metódus által visszaadott típus a dinamikus változó tényleges típusa, azaz a példában d1.GetType() által visszaadott típus System.Int32, a d2.GetType() által visszaadott System.String, a d3.GetType() által visszaadott pedig System.Collections.Generic.List`1[System.Int32]. Dinamikus típust nem csak lokális változók esetén használhatunk: lehet dynamic osztályváltozó is (természetesen akár statikus is), de tulajdonság, visszatérési érték vagy paraméter típusa is.
A dinamikus típusoktól ódzkodók számára a dinamikus típuskezelés veszélyeit ellensúlyozandó érdemes megemlíteni előnyöket: jelentősen átláthatóbbá teszi a COM-mal (erről részletesebben lent), HTML DOM-mal együttműködő kódunkat, illetve a dinamikus típusalkalmazás akár egy nagyságrenddel is gyorsabb, mint a normál reflexió. Ennek hátterében a DLR által biztosított cache megoldások és kifejezésfák használata áll, és a sebességnövekedésen kívül tisztább kódot is eredményez:
helyett:
A dinamikus típusokra használatára több megkötés is vonatkozik:
Régóta várt adóssága volt a nyelvnek az opcionális paraméterek bevezetése, más nyelvekben régóta – vagy kezdetektől fogva – létező nyelvi lehetőség most már a C#-ban is használható. Ezen két újdonság két különálló dolog, azonban általában együtt szokás őket használni, így egy alfejezetben lesz róluk szó.
Az opcionális paraméterek esetén lehetőségünk van arra, hogy egy paraméternek alapértelmezett értéket adjunk meg, így metódushíváskor az a paraméter elhagyható legyen. Ekkor a paraméter értéke az alapértelmezett érték lesz. Opcionális paraméterek használatával elkerülhető a sokszoros túlterhelés, így a kód átláthatóbb, kevesebb redunanciát tartalmazó lesz. Fontos megkötés, hogy az alapértékeknek fordítási idejű értékeknek kell lenniük, tehát dinamikusan kiértékelendő, objektumpéldányt feltételező kifejezés nem szerepelhet. Opcionális paraméterek csak a szükséges, nem opcionális paraméterek után állhatnak, így nem lehet opcionális paraméter két nem opcionális közé ékelve és fordítva (sem).
Az opcionális és nem opcionális paraméterek elkülönítésének szükségessége hosszú paraméterlistáknál nem igényel különösebb magyarázatot; a középről kihagyott elemeket elválasztó vesszőket semelyik programozó sem szeretné számolgatni. Így nem is lehetséges, hogy középről kihagyjunk opcionális paramétert akkor sem, ha utána már csak további egy vagy több opcionális paraméter áll. Azonban a név szerinti paraméterátadással lehetőség nyílik a paramétereknek nem sorrendben értéket adni. A fenti példában az y paraméter magában nem lenne elhagyható, mert található utána még egy, azonban meghívhatjuk úgy a metódusunkat, hogy az x változónak átadunk egy konkrét értéket (annak muszáj), majd a z-nek név szerint és végeredményben az y értéke annak alapértelmezettje lesz. A név szerinti paraméterátadás szintaxisa egyszerű, a paraméter nevét egy ':' követi, majd az átadandó érték. A nevesített paraméterek esetén természetesen eltérhetünk az eredeti sorrendtől is. Ilyenkor bár elvileg ekvivalens alakot kapunk, arra figyeljünk, hogy a paraméterek átadáskor sorban kerülnek kiértékelésre, így írhatunk olyan kódot, amelynél a cserélt paramétersorrendű név szerinti hívás esetén más lesz az eredmény a normál, mint a nevesített paramétereket nem tartalmazó hívásnál.
Az opcionális és név szerint átadott paraméterek nem csak metódusoknál használhatók, hanem megkötés nélkül konstruktorok és indexerek esetében is.
A ko- és kontravariancia kérdésköre a C# nyelv korábbi verziói kapcsán is aktuális volt, a C# 4.0 pedig a generikusokkal kapcsolatban vezeti be a ko- és kontravarianciát, azonban típusbiztos módon, igaz, ez számos megszorítást is von maga után. A kovariancia a nyelv legelső verzióitól jelen van a tömbök kapcsán, a visszatérési értékeknél azonban továbbra sem használható (egy ellenpélda a Java nyelv). A tömbökkel kapcsolatban a kovariancia azt jelenti, hogy egy A[] típusú változónak adhatunk értékül egy B[] típusút, amennyiben B altípusa A-nak. Ez a fajta megvalósítás annak köszönhető, hogy a .NET futtatókörnyezetet alkalmassá akarták tenni Java típusú nyelvek futtatására (lásd J#, stb. kezdeményezéseket), azonban a benne rejlő veszélyek miatt vigyázni kell a használatával. Így ugyanis lehetőségünk van olyan típushibát okozni, amely nem derül ki fordítási időben. Egy példa:
Fordítás során egy figyelmeztetést sem kapunk, futási időben azonban egy ArrayTypeMismatchException típusú kivételt dob az utolsó sor. Látható, hogy a kovariancia az olvasáskor még nem jelent problémát. Nem szorul különösebb magyarázatra az, hogy futási időben kivételt kapunk (egy valójában string típusú elemekből álló tömbbe nem fogunk tudni egy float típusú elemet beleerőltetni), és az sem, hogy fordítási időben ez nem okozhat gondot (az float őse az object, így egy object[] típusú tömb eleme lehet).
A kovariancia és kontravariancia a delegáltakkal kapcsolatban C# 2.0 óta van jelen. Kovariáns delegate típusú változó kaphat olyan típusú metódust értékül, amely visszatérési értéke altípusa az eredeti metódusreferencia visszatérési típusának. Kontravariáns esetben hívható olyan metódus, amely szignatúrájában egy adott paraméter őse az eredeti paraméter típusának. A kovariáns, illetve kontravariáns esetekhez egy-egy kódpélda:
A generikus kovariancia azt jelenti, hogy G<B> altípusa G<A>-nak, amennyiben B altípusa A-nak. Ilyen reláció a C# korábbi verzióiban, illetve más nyelvek (pl. Java) esetében jellemzően nem áll fenn, ugyanis általában megkövetelik a teljes típusegyezőséget, azaz a generikus típus típusparamétereinek is meg kell egyeznie. A generikus kontravariancia a fentiek fordítottja, tehát G<A> altípusa lesz G<B>-nek, amennyiben B altípusa A-nak. A generikus varianciát típusbiztosan vezették be, ezért kovariáns generikus típus esetén csak a visszatérési érték lehet típusparaméter típusú, míg kontravariáns esetben csak a paraméterek között fordulhatnak elő. A generikus variancia implicit invariáns, ha erről külön nem rendelkezünk, így ettől eltérő esetben nekünk kell explicit jelezni a kovarianciát out, a kontravarianciát in kulcsszóval. (Megjegyzés: ezen kulcsszavak több jelentéssel is bírnak a nyelvben, a new, using, where kulcsszavakhoz hasonlatosan.) A varianciamódosító kulcsszavak lehetnek különbözőek különböző típusparaméterek esetén, egyszerre azonban értelemszerűen nem lehetnek jelen.
A generikus variancia használata további korlátokba is ütközhet: kizárólag delegate-ek, illetve interfészek esetén adhatunk típusparaméternek varianciamódosítót, tehát osztályok esetén nem. A keretrendszer generikus elemeit a 4.0-ás változatban ezek szerint újították meg, így például az IEnumerable<T> helyett IEnumerable<out T>, a IComparable<T> helyett IComparable<in T> interfészeket is használhatunk. A megújított interfészek és delegate-k listája (out esetén T kovariáns, in esetén T kontravariáns):
A COM programozást jelentősen megkönnyítik a C# 4.0-ás újítások, így szintaktikus zaj nélküli, átláthatóbb kódbázisunk lehet. Megjegyzendő, hogy már korábban – például Visual Basic .NET nyelven – lehetőségünk volt a COM-ot elérni kevésbé nyakatekert módon is. A COM interfészek esetében a ref kulcsszó használata elhagyható, amennyiben nem változót írunk átadott paraméterként, hanem valami konkrét értéket, azaz valamilyen helyfoglaló objektumot helyettesítünk be. A COM-os hívások során sokszor futhatunk bele hosszú paraméterlistájú függvényekbe, ezen paraméterek opcionálisnak tekinthetők, ha referencia szerinti paraméterátadásra lenne szükség. A fordító ilyenkor egy lokális Type.Missing értékű object típusú változót generál, majd ezt adja át paraméterenként:
Az opcionális és név szerint átadott paraméterek használata értelemszerűen átláthatóbbá teszi a függvényhívásokat, a dinamikus típusok használata azonban még tovább csökkentheti a szintaktikus zajt:
A dinamikus típusok használata lehetővé teszi az explicit kasztolások elhagyását, bár igaz, hogy így fordítási időben nem derül ki, ha egy adott elemre nem hívható egy adott metódus, vagy típusinkompabilitás lép fel.
A számítási kapacitások bővítésére a processzor órajelek fokozása és a csíkszélesség csökkentése helyett/mellett a legalkalmasabb az egy lapkán előforduló magok számának növelése. A hardver ilyen irányú fejlődése a szoftvertechológia módszereiben is kijelöl egy utat: a jövőben a hardveres lehetőségek minél jobb kihasználása érdekében szükséges, hogy a számítások lehetőleg egyszerre fussanak, minden magot kihasználva és közben ügyelve a konkurrens memóriahasználat veszélyeire. Az ilyen jellegű szoftverfejlesztés a párhuzamosságból adódó nemdeterminisztikusság miatt nagyon nehéz, így szükségszerű volt a párhuzamosságot támogató eszközök elterjedése. A Parallel Library osztályai segítségével lehetőségünk van a korábbi szekvenciális kódunkat minimális változtatással átírni párhuzamosan futóra úgy, hogy a programozási részletekkel nem kell foglalkoznunk (hány mag?, thread pool, stb.). Az átírás rendkívül egyszerűen zajlik, például egy for ciklus esetén a Parallel osztály statikus For metódusának adjuk át a kezdő- és végértékeket, illetve egy olyan delegate-t, amely a ciklusmag számításait tartalmazza:
A programozónak mindösszesen ennyit kell változtatnia (ebben az esetben legalábbis, ahol nem számít a ciklusmag végrehajtási sorrendje, így nem kell foglalkozni a szinkronizációval), a keretrendszer futási időben létrehozza a megfelelő számú szálat, amik képesek lesznek párhuzamosan elvégezni a ciklus végrehajtását. A Parallel osztály a System.Threading.Tasks névtérben található. A normál for cikluson kívül könnyen párhuzamosítható a foreach ciklus is, így gyűjteményeket is feldolgozhatunk párhuzamosan. A LINQ PLINQ-vá való kiterjesztésével lehetőséget kaptunk a korábban szekvenciális feldolgozottságú lekérdezéseket is párhuzamosítani, a korábbi kódhoz képest itt is minimális változtatás szükséges csak:
A keretrendszer további, párhuzamos számításokat segítő osztályokat bocsájt a fejlesztők rendelkezésére, ezek közül a legfontosabb a szálaknál magasabb szintű Task osztály. A task-ok segítségével az amúgy bonyolult szinkonizációs feladatok elvégzése is egyszerűvé válik.
A System.Collections.Concurrent névtérben bevezetésre került több, szinkronizált, szálbiztos gyűjtemény is:
A Code Contracts könyvtár a C# 4.0, illetve a Visual Studio 2010 kiadását megelőzően is elérhető volt, most már viszont a keretrendszer része. A Code Contracts könyvtárról részletesebb itt olvashat.