Kihagyás

C# alapok II

foreach

Java-ból már ismerős lehet a for ciklus for-each szerű használata, amikor nem az indexeken, hanem magukon az értékeken iterálunk végig.

1
2
3
4
5
6
List<String> someList = new ArrayList<String>();
// ...

for (String item : someList) {
    System.out.println(item);
}

Java-ban ehhez nem tartozik külön kulcsszó, viszont a C# nyelvben ez a foreach segítségével tehető meg.

1
2
3
foreach (var item in someList) {
    Console.WriteLine(item);
}

yield

A yield kifejezés segítségével egy ciklusból olyan osztályt generál a fordító, ami megvalósítja az IEnumarable interfészt. Egy adott metódusban használjuk a yield-et, majd ezt a metódust egy ciklusban többször meghívjuk. Ilyen esetekben a yield kétféleképpen működhet:

  • yield return <<expression>>;: megőrződik a metódus állapota, és a következő metódushívásnál innen folytatódik tovább a működés,
  • yield break;: return-nel egyenértékű lépést tesz, végleg kilép a metódusból és törlődik az állapot.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static int[] Func(int start, int number) {
    int[] _number = new int[number];
    for (int i = 0; i < number; i++)
        _number[i] = start + 2 * i;

    return _number;
}

static void Main(string[] args) {
    foreach(var item in Func(2, 10)) {
        Console.WriteLine(item);
    }
}

A fenti kódrészlet a yield segítségével a következőképpen egyszerűsödik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static IEnumerable Func(int start, int number) {
    for (int i = 0; i < number; i++) {
        yield return start + 2 * i;
    }
}

static void Main(string[] args) {
    foreach(var item in Func(2, 10)) {
        Console.WriteLine(item);
    }
}

A példa működése: első meghíváskor visszatér a 2-vel, megjegyzi az i index értékét, majd kilép. A következő meghíváskor az i -- megjegyzett -- értékével folytatja a futást, így a 4-es értékkel tér vissza, és így tovább. Ez a folyamat a yield break segítségével szakítható meg.

Feladat: Próbáld ki a fenti példát! Helyezz egy breakpointot a metódusokba, és ellenőrizd a működésüket!

Típusellenőrzés és konverzió

Az is és az as kulcsszavak típuslekérésre illetve típuskonverzióra használhatók. Az is segítségével ellenőrizhető egy változó típusa:

1
2
double szam = 6.76;
Console.WriteLine(szam is int);

Az as használatával típuskonverzió hajtható végre:

1
2
int? szam2 = szam as int?;
Console.WriteLine(szam2);

sealed

A sealed módosító segítségével osztályok, metódusok, properyk tehetők lezárttá. Sealed osztályból gyerekosztály nem származtatható.

1
2
3
4
5
6
class A { }
sealed class B { } // lezárt B osztály
sealed class C : A { }; // lezárt C osztály, amit A-ból származtatunk

// class D : B { } - hibás
// class E : C { } - szintén hibás

A metódusok és propertyk esetén pedig az öröklődés alatti felüldefiniálást akadályozza meg. A struktúrák C#-ban alapértelmezetten lezártan viselkednek, nem származtathatók.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class A {
    virtual void func() { Console.WriteLine("A func"); }
    virtual void func2() { Console.WriteLine("A func2"); }
}

class B : A {
    sealed override void func() { Console.WriteLine("B func"); }
    override void func2() { Console.WriteLine("B func2"); }
}

class C : B {
    // override void func() { Console.WriteLine("C func"); }
    override void func2() { Console.WriteLine("C func2"); }
}

A sealed kulcsszó csak az override-dal együtt használható, illetve az abstract módosítószóval nem használható, mivel akkor elvárás, hogy az adott metódus a gyermekosztályban implementálva legyen.

Kivételkezelés

Kivételkezelés a már megszokott try-catch-finally segítségével valósítható meg. A finally nyitja a cleanup blokkot, ami minden esetben le fog futni, ezt egy vagy több catch blokk előzheti meg, de nem kötelező a catch blokk használata sem, ha van finally. A finally blokk opcionálisan elhagyható, ha nincs szükségünk objektumok kézzel történő felszabadítására. A finally blokkra nem menedzselt kód esetén van szükség - ez az amit a GC nem takarít el helyettünk -, pl. ahogy már Javaban is találkozhattunk vele, pl. adatbázis műveletek során:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Java példa
try {
    Connection conn = DriverManager.getConnection([...]);
    // ...
}
catch (SQLException ex) {
    System.err.println(ex);
}
finally {
    if(conn != null) { conn.close(); }
}

Erre már Java-ban is volt egy kényelmi megoldás: a try-with-resource volt. Itt az az egy megkötés volt, hogy a nem menedzselt objektumnak a java.lang.AutoClosable interfészből kell származnia, így a keretrendszer automatikusan le tudta zárni. A try-with-resource a try blokk elhagyása után automatikusan meghívta az objektum -- jelen esetben adatbáziskapcsolat -- close metódusát:

1
2
3
4
5
try(Connection conn = DriverManager.getConnection([...]);) {
    // ...
} catch (SQLException ex) {
        System.err.println(ex);
}

Hasonló megoldást tartogat a C# is, ez is kényelmi funkció, mint a Java-s try-with-resource, a using kulcsszó. Fontos: ez a using és az a using, amivel namespace-eket használunk nem ugyanaz!

A using esetén az objektumnak az IDisposable interfészt kell megvalósítania, és a using biztosítani fogja, hogy az objektum Dispose() metódusa megfelelően legyen meghívva - akkor is ha kivétel keletkezik. Ugyanazt a logikát követi, mint a try-with-resource, itt a Dispose() metódus fog felelni az erőforrás felszabadításáról, bezárásáról. A Java és a C# kényelmi megoldásából is egy try-finally fog generálódni a háttérben.

1
2
3
using (SqlConnection connection = new SqlConnection(conStr)) {
    // ...
}

Delegatek

A delegate felfogható függvénymutatóként, mely olyan függvényekre tud referálni, amelyeknek megegyezik a deklarációja -- paraméterlistája és a visszatérési értéke -- a delegate objektuméval. Egy delegate létrehozása után bármilyen kompatibilis függvényhez hozzárendelhető, így függvény hívható a delegate objektumon keresztül. Statikus és példánymetódusok is használhatók delegateken keresztül. Általános használatuk: függvények átadása más függvényeknek argumentumként, így callback függvények hozhatók létre.

1
public delegate void Del(string message);

A Del delegate olyan függvényekre tud mutatni, melyek void visszatéréssel rendelkeznek és egy string paramétert várnak, mint pl:

1
2
3
public static void CustomMethod(string msg) {
    Console.WriteLine(msg);
}
1
2
Del delegateHandler = CustomMethod;
delegateHandler("John Doe");

Mivel a létrehozott delegateHandler objektum, így átadható függvényeknek, mint paraméter.

1
2
3
4
5
public void DoMagic(int param, Del callback) {
    callback(param * 3);
}

DoMagic(3, delegateHandler);

Egy delegate objektum több metódusra is hivatkozhat egyszerre. Amikor a delegate objektum meghívódik, az összes metódust meghívja feliratkozási sorrendben. Egy delegatehez hozzáadható egy függvény a += segítségével és eltávolítható belőle a -= segítségével.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class MethodClass {
    public void Method1(string message) { }
    public void Method2(string message) { }
}

var obj = new MethodClass();
Del d1 = obj.Method1;
Del d2 = obj.Method2;
Del d3 = DelegateMethod;

Del allMethodsDelegate = d1 + d2;
allMethodsDelegate += d3;
allMethodsDelegate -= d1;

Ez az eseménykezelés során hasznos, mikor egy eseményre feliratkozhatunk, illetve leiratkozhatunk saját eseménykezelővel a += és a -= segítségével.

Func

A Func egy speciális delegate, melynek lehetnek input paraméterei (akár nulla, de több is) és egy visszatérési értéke van.

1
2
3
4
5
6
static int Sum(int x, int y) {
    return x + y;
}

Func<int, int, int> add = Sum;
int result = add(10, 10);

Az utolsó paraméter a < > között a visszatérési érték, az előtte lévők az input paraméterek. Ha egy függvény nem vár paramétereket és pl. double-lel tér vissza, akkor a

1
 Func<double> funcWithDouble;
módon lehet létrehozni.

Action

Hasonló az előző szekcióhoz, viszont az Action-nak pontosan egy bemenő paramétere van és nincs visszatérési értéke.

1
2
3
4
5
6
7
public static void Logger(string message) {
    string timestamp = DateTime.Now.ToString("h:mm:ss tt") ;
    Console.WriteLine(timestamp + " " + message);
}

Action<string> logger = Logger;
logger("log");

A delegatek utolsó nagy csoportja az események, Events. Az események szemléltetése és megértése a WinForms környezetben egyszerűbb, mint konzolos alkalmazásokkal, így az ott kerül bemutatásra.

Feladatok delegate-ekkel

  1. Egészítsd ki a számológép alkalmazást, delegate-ekkel, minden műveletet delegate-ek segítségével végezz.
  2. Egészítsd ki a számológép alkalmazást, minden műveletet, illetve esetleges hibát logolj delegate segítségével.

Kollekciók

Több azonos típusú objektum együttes kezeléséhez eddig a tömbök nyújtották a megoldást. Amennyiben az objektumok száma előre ismert, akkor a tömb egy tökéletesen működő adatszerkezet, viszont ez a számosság nem mindig ismert előre. Ilyen esetekben az általános kollekciók rugalmasabb megoldást nyújtanak, méretben növekedni és zsugorodni tudnak attól függően, hogy hány objektum van tárolva.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var lista = new List<string>();
Console.WriteLine(lista.Capacity);

lista.Add("alkalmazas");
lista.Add("fejlesztes");
lista.Add("alkfejl");
lista.Add("-.^");

Console.WriteLine(lista.Capacity);
foreach (var item in lista)
    Console.WriteLine(item);

A példában egy generikus List objektum van létrehozva, amely string típusú értékeket tud tárolni. A konstruktorában egy számot vár, amely az iniciális kapacitást jelenti, alapértelmezett a 0. Az Add metódussal lehet hozzáadni elemeket a listához, illetve foreach segítségével végig lehet rajta iterálni.

A példában látott Capacity a tárolható objektumok számát adja vissza (azaz, hogy mennyi objektumnak foglalt már helyet a keretrendszer), míg a Count azt, hogy ténylegesen hány objektum található a kollekcióban. Ha foreach helyett for ciklus iterál végig a kollekción, akkor a Count tulajdonságot kell használni.

A fenti példával egyenértékű írásmód érhető el az úgynevezett inicializáló listával:

1
2
3
4
5
var lista = new List<string> {
    "alkalmazas",
    "fejlesztes",
    "alkfejl",
    "-.^" };

Törölni a Remove(obj), illetve a RemoveAt(index) metódusokkal lehet. Az első egy objektumot vár, a második a kollekcióban egy indexet.

Kollekció típusok

A List-en kívül más típusú kollekciók is elérhetők, mind más megvalósítással és működéssel, melyeket a következő táblázat mutatja be:

Osztály Leírás
Dictionary <TKey, TValue>       kulcs-érték párokat tárol
List <T> objektumok listáját tárolja, támogatja az indexelést, keresést, rendezést és a lista módosítását
Queue <T> FIFO lista
Stack <T> LIFO lista

A különböző kollekciók különböző metódusokat használnak az elemek kezelésére, a fejlesztőkörnyezet mindig a megfelelőt ajánlja fel használatukkor.

LINQ

A Language-Integrated Query, vagyis a LINQ lehetővé teszi a kollekciókban (bármiben, ami az IEnumerable interfészt implementálja) történő keresést, rendezést és csoportosítást.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int[] scores = new int[] { 1, 2, 3, 5, 8, 13, 21, 34, 97, 92, 81, 60 };

IEnumerable<int> scoreQuery =
    from score in scores
    where score > 80
    orderby score // descending
    select score;

foreach (int i in scoreQuery)
    Console.Write(i + " ");

A kifejezés nem értékelődik ki addig, amíg a foreach (vagy egy más konstrukció) nem iterál végig a scoreQuery-n.

1
2
3
4
5
public class Elem {
    public string Symbol { get; set; }
    public string Name { get; set; }
    public int AtomicNumber { get; set; }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private static List<Elem> BuildList() {
    return new List<Elem> {
        { new Elem() { Symbol="K", Name="Potassium", AtomicNumber=19}},
        { new Elem() { Symbol="Ca", Name="Calcium", AtomicNumber=20}},
        { new Elem() { Symbol="Sc", Name="Scandium", AtomicNumber=21}},
        { new Elem() { Symbol="Ti", Name="Titanium", AtomicNumber=22}}
    };
}

private static void ShowLINQ() {
    List<Elem> Elems = BuildList();

    var subset = from theElem in Elems
                 where theElem.AtomicNumber < 22
                 orderby theElem.Name
                 select theElem;

    foreach (Elem theElem in subset)
        Console.WriteLine(theElem.Name + " " + theElem.AtomicNumber);
}

Feladatok LINQ-el I

Készíts egy listát ami Pokémon objektumokat tárol. Egy Pokémon a következő tulajdonságokkal kell rendelkezzen: név, fajta, erő (szám). Ld. Pokédex, Complete Pokémon Pokédex.

  1. Listázd ki LINQ segítségével az összes sárkányt.
  2. Számold meg őket LINQ segítségével (1.).
  3. Keresd meg a legerősebbet LINQ segítségével. Az eredményben csak a neve és az ereje látszódjon. (1.)
  4. Listázd ki a 600-nál nagyobb erővel rendelkező összes Pokémont, csak a nevük szerepeljen az eredményben.
  5. Listázd ki őket (4.) abc sorrendben.
  6. Átlagold az összes Pokémon erejét.

Lambda kifejezések

A lambda kifejezéseknek két formája lehet:

1
2
(bemenő-paraméterek) => kifejezés
(bemenő-paraméterek) => { több utasítás }

Hasonló szintaktikával rendelkeznek, mint a függvények, viszont tömörebb írásmóddal érik el ugyanazt. Minden lambda kifejezés delegate-té alakítható.

1
2
3
4
5
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));

Action line = () => Console.WriteLine();
line();
1
2
3
4
5
Func<int, int, bool> isEqual = (x, y) => x == y;
Console.WriteLine(isEqual(5,4));

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
isTooLong(7, "Alkalmazasfejlesztes II");
1
2
3
4
5
Action<string> greet = name => {
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};
greet("World");

LINQ kifejezések egy részét metódushívásos szintaktikával is lehet hívni, ilyenkor alkalmazhatók lambda kifejezések.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int[] numbers = { 1, 2, 3, 5, 8, 13, 21, 34, 97, 92, 81, 60 };
var sqLINQ = from num in numbers
             where num > 80
             select num * num;

var squaredNumbers = numbers
    .Where(x => x > 80)
    .Select(x => x * x);

Console.WriteLine(string.Join(" ", squaredNumbers));
Console.WriteLine(string.Join(" ", sqLINQ));

Hasznos metódusok:

  • Where: szűrési feltételek megadhatók, pl. x => x > 80,
  • OrderBy: rendezés az objektum (vagy valamely tulajdonsága szerint) pl. .OrderBy(x => x.Age),
  • ThenBy: OrderBy rendezés után egy újabb rendezés végrehajtása az előző megtartásával,
  • Select: kiválasztás, mi legyen az eredmény kollekcióban pl. a számok négyzete, anonym típus, stb pl. .Select(x => (x.Age, x.Name)) ,
  • Any: a kollekcióban van-e a feltételt kielégítő elem,
  • All: a kollekció minden eleme kielégíti-e a feltételt,
  • Contains: a kollekció tartalmaz-e egy specifikus elemet pl numbers.Contains(60),
  • Average: átlagszámítás,
  • Count: elemek számosságát határozza meg,
  • Min: minimum érték keresése,
  • Max: maximum érték keresése,
  • Sum: a kollekcióban fellelhető elemek összege,
  • First: a feltételeknek eleget tévő objektumok közül az elsőt adja vissza (kivételt dob ha nincs egy sem),
  • FirstOrDefault: mint az előző, csak kivétel helyett alapértelmezett értéket szolgáltat.

Feladatok LINQ-el II

A Pokémonos feladat összes pontját oldd meg metódushívásos formában is.

Kifejezésfák

LINQ használata esetén olyan tárolókon dolgozunk, amelyek vagy az IEnumerable vagy az IQueryable interfészt valósítják meg. Általános esetben az IEnumerable típusokon készített query-k delegate-ekké fordulnak, az IQueryable típusokon alapulók pedig expression tree-vé, azaz kifejezésfákká.

A kifejezésfák olyan formában reprezentálják a kódot, ami lehetőséget ad a futásidejű manipulációra. Ugyanazokat a struktúrákat használja, amit a fordító használ a kód analizálására és az lefordított output generálására. A segítségével módosíthatunk épp futó algoritmusokat stb., tehát láthatóan kiszélesíti a lehetőségeinket.

Természetesen mi magunk is előállíthatunk ilyen kifejezésfákat (ehhez a System.Linq.Expression névtér használatára lesz szükségünk), ha speciálisan velük szeretnénk dolgozni adott szituációkban. Ez egy olyan kódot jelent, ami nincs lefordítva és nem futtatható. Bizonyos típusokat (a LambdaExpression típusúakat, vagy az ebből származókat) le tudjuk fordítani egy köztes nyelvre, hogy direktbe futtatóvá tehessük (IL, intermediate language). Expression tree-k írásával dinamikusan készítünk lambdákat, és nem a végeredményre tesszük a hangsúlyt, hanem arra, hogy milyen lépésekkel jutunk el oda. Az ereje pedig nem abban áll, hogy C# kódban felhasználható, hanem hogy más rendszerekbe beintegrálható.

Most példaképpen egy ilyen LambdaExpression típussal fogunk megismerkedni:

1
2
3
4
Expression<Func<int>> add = () => 1 + 2;
var func = add.Compile();
var answer = func();
Console.WriteLine(answer);

Tehát ő egy expression tree-t fog előállítani, ami visszatér egy delegate-tel, és ezen a delegate-n keresztül tudjuk meghívni és futtatni.

Hivatkozások

Using type dynamic

foreach, in

Type-testing operators and cast expression

yield

sealed

try-catch-finally

using statement

Using Delegates

Action <T> Delegate

C# - Action Delegate

Collections

Lambda expressions

Language Integrated Query (LINQ)

Introduction to LINQ Queries

LINQ and Generic Types

Query Syntax and Method Syntax in LINQ

Expression Trees

Framework Types Supporting Expression Trees

Expression Trees Explained

C# Introduction to Expression Trees

Extra

Really silly things to do with C# expression trees


Utolsó frissítés: 2023-08-07 09:17:05