Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Tip
Ez a cikk az Alapismeretek szakasz része, amely olyan fejlesztők számára készült, akik legalább egy programozási nyelvet ismernek, és C#-t tanulnak. Ha még csak most ismerkedik a programozással, kezdje az első lépésekkel. A gyorshivatkozási táblázatot a Típus kiválasztása című témakörben találhatja meg.
A C#-alkalmazások egyik első tervezési döntése a létrehozni kívánt típus kiválasztása. Legyen a menüelem class vagy record? Egy gyors számításnak tuple-t vagy elnevezett típust kellene visszaadnia? Minden választási lehetőség meghatározza, hogyan kezeli a kód az egyenlőséget, a megváltoztathatóságot és a polimorfizmust. A rossz választás sablonhoz, hibákhoz vagy mindkettőhöz vezet.
Ebben az oktatóanyagban létrehoz egy kis kávézómodellt, amely menüelemeket, rendeléseket, érzékelőolvasásokat és kedvezményszabályzatokat használ. Elemezheti a jellemzőket, és meghatározhatja az egyes fogalmakhoz tartozó legjobb C#-típust. Az út során megtanulhatja felismerni azokat a tervezési nyomásokat, amelyek az egyik típus felé mutatnak a másik felett.
Ebben az útmutatóban Ön:
- Felismerés arról, hogy egy tuple helyes választás-e több érték visszaadására.
- Nem módosítható adatokat modellezhet rekordosztályokkal, és megértheti az értékalapú egyenlőséget.
- Kis méretű, másolható adatokat jelöl egy rekordstruktúra segítségével.
- Kezelje az elváltozó állapotot és viselkedést egy osztállyal.
- Egy osztályt kiterjeszthetünk örökléssel, hogy szabályokat adjunk hozzá vagy szigorítsunk meg.
- Közös képességek definiálása a nem kapcsolódó típusok között egy felülettel.
Prerequisites
- Telepítse a .NET SDK-t.
Ideiglenes csoportosításhoz használjon tuple-t
A kávézónak olyan módszerre van szüksége, amely a rendelések teljes számát és a napi bevételt is visszaadja. Ehhez osztályt vagy szerkezetet is definiálhat, de az egyik metódus két értéke nem mindig indokolja az új típust.
(int TotalOrders, decimal Revenue) GetDailySummary(int orders, decimal revenue)
=> (orders, revenue);
Console.WriteLine("=== Tuple: daily summary ===");
var summary = GetDailySummary(42, 1234.50m);
Console.WriteLine($"Orders: {summary.TotalOrders}, Revenue: {summary.Revenue:F2}");
var (orders, revenue) = summary;
Console.WriteLine($"Deconstructed: {orders} orders, {revenue:F2}");
Miért működik a legjobban egy tuple ebben a példában?
A tömb azért működik itt, mert a csoportosítás helyi: egy függvény hozza létre, egy hívó pedig felhasználja. Az elnevezett elemek teljes típusú ceremónia nélkül teszik egyértelművé a szándékot. Ha azt tapasztalja, hogy ugyanazt a rekordalakzatot több metóduson is átengedi, az azt jelzi, hogy előlépteti azt egy rekordra vagy osztályra. Ezt az evolúciót az oktatóanyag későbbi részében láthatja. A tuple szintaxisáról és képességeiről további információt a Tuple-típusok című témakörben talál.
Nem módosítható adatok rekordjának használata
Minden kávézónak szüksége van egy menüre. A menüelemek neve, ára és táplálkozási megjegyzése van. Ezek az értékek nem változnak az elem listázása után. A két rendszer, amelyek egyaránt hivatkoznak a "Latte $4.50-ért", meg kell egyezniük abban, hogy ugyanarról a dologról beszélnek, még akkor is, ha különböző objektumokat hoztak létre.
Pozíciórekord deklarálása:
record class MenuItem(string Name, decimal Price, string NutritionalNote);
A fordító létrehoz egy konstruktort, destruktort, Equals, GetHashCode és ToString abból az egyetlen sorból. Helyezze a rekordot a működéshez:
Console.WriteLine("\n=== Record class: MenuItem ===");
var latte = new MenuItem("Latte", 4.50m, "Contains dairy");
var latte2 = new MenuItem("Latte", 4.50m, "Contains dairy");
var seasonal = latte with { Name = "Pumpkin Spice Latte", Price = 5.25m };
Console.WriteLine(latte);
Console.WriteLine(seasonal);
Console.WriteLine($"Same reference (latte vs latte2): {ReferenceEquals(latte, latte2)}");
Console.WriteLine($"Value equal (latte vs latte2): {latte == latte2}");
Console.WriteLine($"Value equal (latte vs seasonal): {latte == seasonal}");
Két MenuItem azonos adattal rendelkező példány egyenlő annak ellenére, hogy külön objektumok. Ez a viselkedés az értékalapú egyenlőséget szemlélteti. A with kifejezés szezonális változatot hoz létre az eredeti mutálása nélkül.
A rekordosztály akkor megfelelő, ha az identitás adatokból származik, nem objektumhivatkozásból, és a példányok ritkán változnak a létrehozás után. Olvasható ToString() kimenetet, szerkezeti egyenlőséget és with támogatást kap a dobozból. Részletesebb útmutatóért tekintse meg a Rekordok és a Rekordok oktatóanyagot.
Rekordstruktúra használata kis értéktípusokhoz
A kávéfőző beépített hőmérővel rendelkezik, amely hőmérsékleti értékeket jelez. Minden olvasás apró – szám és egység – és naplókba, riasztásokba és irányítópultokba másolódik. Nem szeretné, hogy az egyik példány módosítása áthatoljon a többien.
Rekordstruktúra deklarálása:
record struct Measurement(double Value, string Unit);
Használja a rekordstruktúrát:
Console.WriteLine("\n=== Record struct: Measurement ===");
var temp = new Measurement(72.5, "°F");
var copy = temp;
copy = copy with { Value = 23.0, Unit = "°C" };
Console.WriteLine($"Original: {temp.Value}{temp.Unit}");
Console.WriteLine($"Copy (converted): {copy.Value}{copy.Unit}");
Az temp hozzárendelése az copy-hez független értéket hoz létre. A with kifejezés új értéket hoz létre anélkül, hogy hozzányúl az eredetihez – ugyanaz a minta, mint egy rekordosztály, de másolás közbeni hozzárendelési viselkedéssel, nem pedig másolással.
A rekordstruktúra akkor illik, ha az adatok kicsik (néhány primitív mező), és a másolás olcsóbb, mint a halomfoglalás. Értékegyenlőséget és with támogatást kap, mint egy rekordosztály, alatta valódi érték szemantikával. A mérések, koordináták és hasonló könnyen kezelhető adatok természetes jelöltek. További információt a Rekordok és a Struktúratípusok című témakörben talál.
Használjon osztályt, ha változékony állapotra és viselkedésre van szüksége
Amikor egy ügyfél felmegy a számlálóhoz, a barista elindít egy rendelést, és egyenként hozzáad elemeket. Az összeg növekszik, az állapot "Függőben" állapotról "Kész" értékre változik, és az egyidejűleg leadott két rendelés – még azonos tételek esetén is – továbbra is eltérő megrendelések.
class Order : IOrder
{
public virtual string Status { get; set; } = "Pending";
private readonly List<(string Name, decimal Price)> _items = [];
public void AddItem(string name, decimal price) => _items.Add((name, price));
public decimal Total => _items.Sum(i => i.Price);
public override string ToString() =>
$"Order [{Status}]: {string.Join(", ", _items.Select(i => i.Name))} - Total: {Total:F2}";
}
Console.WriteLine("\n=== Class: Order ===");
var order = new Order();
order.AddItem("Latte", 4.50m);
order.AddItem("Croissant", 3.25m);
order.Status = "Ready";
Console.WriteLine(order);
Az Order osztály nyomon követi az elemeket, kiszámítja a folyamatosan frissülő összeget, és lehetővé tesz egy beállítható Status. Itt az osztály a megfelelő eszköz, mert az objektum változó állapotot hordoz, amely az élettartama során változik, a viselkedés (metódusok) a típus céljának középpontjában állnak, és az identitás számít – az azonos elemekkel rendelkező két rendelés még mindig eltérő sorrend. További információ: Osztályok, szerkezetek és rekordok.
Öröklés használata, ha ki kell terjesztenie egy osztályt
A kávézó vendéglátási eseményeket indít. A vendéglátási rendelés továbbra is rendelés – tételekkel és összegekkel is rendelkezik –, de nyomon követi a vendégszámot is, és a konyha előtt vezetői jóváhagyást igényel. Ahelyett, hogy duplikálja Ordera logikát, egy speciális osztályt kell létrehoznia.
class CateringOrder : Order
{
public int MinimumGuests { get; }
public string? ApprovedBy { get; private set; }
public CateringOrder(int minimumGuests) => MinimumGuests = minimumGuests;
public void Approve(string manager) => ApprovedBy = manager;
public override string Status
{
get => base.Status;
set
{
if (value == "Ready" && ApprovedBy is null)
throw new InvalidOperationException(
"A catering order requires manager approval before it can be marked ready.");
base.Status = value;
}
}
public override string ToString() =>
$"Catering [{Status}] for {MinimumGuests}+ guests, approved by: {ApprovedBy ?? "(none)"} - Total: {Total:F2}";
}
CateringOrder, AddItem és Total újrahasznosítja az alaposztályból. A Status felülbírálás meghúzza a szerződést – az előzetes jóváhagyás nélküli hívás Status = "Ready" kivételt eredményez:
Console.WriteLine("\n=== Inheritance: CateringOrder ===");
var catering = new CateringOrder(minimumGuests: 20);
catering.AddItem("Coffee (serves 20)", 45.00m);
catering.AddItem("Pastry platter", 60.00m);
try
{
catering.Status = "Ready";
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Blocked: {ex.Message}");
}
catering.Approve("Sam");
catering.Status = "Ready";
Console.WriteLine(catering);
Ez az egyetlen származtatott osztály három öröklési fogalmat szemléltet:
-
Hozzáadott állapot:
MinimumGuestsésApprovedBycsak a származtatott osztályban létezik. -
Hozzáadott viselkedés:
Approveúj – a bázisOrdernem tud a jóváhagyásokról. -
Felülbírált viselkedés: a
Statusbeállító olyan üzleti szabályt kényszerít ki, amellyel az alaposztály nem rendelkezik.
Az öröklés akkor illik, ha az új típus az alaptípus speciális verziója, és a szabályok hozzáadása vagy szigorítása során újra fel kell használnia a meglévő állapotot és viselkedést. A megosztott alaposztály természetesebb, mint egy felület, amikor a típusok implementálást osztanak meg, nem csak egy szerződést.
Megosztott képességek definiálása felülettel
A kávézó különböző promóciókat futtat – boldog órát, hűségjutalmakat, szezonális akciókat. A fizetési folyamatnak a mai napon érvényes kedvezményt kell alkalmaznia, anélkül hogy ismerné az egyes szabályzatok részleteit. Meg kell találnia egy módot arra, hogy elmondhassa: "bármi, ami kedvezményt alkalmazhat," anélkül, hogy a fizetési folyamatot egyetlen osztályhoz kötné.
interface IDiscountPolicy
{
decimal Apply(decimal total);
}
class HappyHourDiscount : IDiscountPolicy
{
public decimal Apply(decimal total) => total * 0.80m;
}
class LoyaltyDiscount : IDiscountPolicy
{
public decimal Apply(decimal total) => total - 1.00m;
}
A Checkout metódus bármilyen IDiscountPolicy elfogad, így új szabályzatokat vezethet be anélkül, hogy módosítaná a pénztári logikát.
static decimal Checkout(decimal total, IDiscountPolicy policy) => policy.Apply(total);
Console.WriteLine("\n=== Interface: discount policy ===");
decimal subtotal = 12.00m;
Console.WriteLine($"Happy hour (20% off): {Checkout(subtotal, new HappyHourDiscount()):F2}");
Console.WriteLine($"Loyalty ($1 off): {Checkout(subtotal, new LoyaltyDiscount()):F2}");
Az interfész deklarál egy szerződést – egy olyan tagcsoportot, amelyet minden implementálási típusnak meg kell adnia. A felület azért működik itt, mert a kedvezménytípusok nem kapcsolódnak egymáshoz (nem osztanak meg alaposztályt), a pénztárnak azonban egységesen kell kezelnie őket. Az interfészek megkönnyítik a tesztelést is: beilleszthet egy helyettesítő szabályzatot az éles kód érintése nélkül. További részletekért tekintse meg az Interfészek című témakört.
A típusválasztás fejlesztése
Ezek közül a döntések közül egyik sem végleges. Valójában könnyen módosíthatja őket, mielőtt kiad egy könyvtárat, ahol a jelentős változások válhatnak fontossá. Ahogy a követelmények növekednek, átalakíthat egy egyszerű típust egy gazdagabbra. Íme három gyakori evolúció.
Rekord →: a csoportosítás folyamatosan megjelenik
A GetDailySummary tuple egy metóduson belül jól működik, de amikor jelentésekben, irányítópultokban és tesztekben kezdi használni, egy névvel ellátott típus sokat segít. A tuple rekorddá előléptetése és számított tulajdonságok hozzáadása:
record class DailySummary(int TotalOrders, decimal Revenue)
{
public decimal AverageTicket => TotalOrders > 0 ? Revenue / TotalOrders : 0m;
}
Azok a hívók, amelyek korábban bontották a tupelt, mostantól ingyen kapnak ToString(), értékegyenlőséget, és egy természetes helyet a származtatott adatokhoz, mint például AverageTicket.
Console.WriteLine("\n=== Evolve: tuple -> record ===");
var daily = new DailySummary(120, 525.75m);
Console.WriteLine(daily);
Console.WriteLine($"Average ticket: {daily.AverageTicket:F2}");
Struct → osztály: szüksége van öröklésre
Az üzlet karbantartási csapata kalibrált értékeket kér: egy eltolással korrigált érzékelőértéket. A Measurement rekordstruktúra nagyszerűen használható nyers adatokhoz, de a szerkezetek nem támogatják az öröklést, így nem lehet kalibrált változatot származtatni. Előléptetés osztályhierarchiába:
class SensorReading(double value, string unit)
{
public double Value { get; } = value;
public string Unit { get; } = unit;
public virtual string Display() => $"{Value}{Unit}";
}
class CalibratedReading(double value, string unit, double offset)
: SensorReading(value, unit)
{
public double Offset { get; } = offset;
public override string Display() => $"{Value + Offset}{Unit} (offset {Offset:+0.0;-0.0})";
}
CalibratedReading örököl SensorReading-től, majd felülbírálja Display()-t, hogy tartalmazza az eltolást. Ez a mintázat nem valósítható meg struktúra vagy rekordstruktúra segítségével.
Console.WriteLine("\n=== Evolve: struct -> class ===");
var raw = new SensorReading(72.5, "°F");
var calibrated = new CalibratedReading(72.5, "°F", offset: -0.3);
Console.WriteLine($"Raw: {raw.Display()}");
Console.WriteLine($"Calibrated: {calibrated.Display()}");
Osztály → osztály + interfész: több típushoz is polimorfizmusra van szükség
Az Order osztály önmagában is jól működik, de ha CateringOrder létezik, akkor a pénztár, jelentéskészítés és nyomtatás képes legyen bármilyen megrendelést kezelni anélkül, hogy foglalkozna azzal, milyen konkrét típus. Nyerje ki a felületet azokkal a tagokkal, amelyektől a hívók ténylegesen függenek:
interface IOrder
{
string Status { get; set; }
decimal Total { get; }
}
Mindkettő Order , és CateringOrder már megfelel ennek a szerződésnek. Most egyetlen metódus kezeli a következő típusokat:
Console.WriteLine("\n=== Evolve: class -> class + interface ===");
static void PrintOrderSummary(IOrder o) =>
Console.WriteLine($" {o.Total:F2} [{o.Status}]");
var walkIn = new Order();
walkIn.AddItem("Mocha", 5.00m);
walkIn.Status = "Ready";
var banquet = new CateringOrder(minimumGuests: 50);
banquet.AddItem("Coffee service", 90.00m);
banquet.Approve("Alex");
banquet.Status = "Ready";
Console.WriteLine("All orders:");
foreach (IOrder o in new IOrder[] { walkIn, banquet })
PrintOrderSummary(o);
A felület kinyerése nem változtat Order-n vagy CateringOrder-n — csak egyértelművé teszi a megosztott alakzatot, ami a tesztelést is megkönnyíti.
Gyors döntési útmutató
Használja ezt a táblázatot kiindulási pontként, ha nem biztos abban, hogy melyik típust válassza:
| Kérdés | Optimális illesztés |
|---|---|
| Egy metódusból ad vissza néhány értéket? | Tuple |
| Nem módosítható adatok, ahol az egyenlőség értékek szerint történik? | Rekordosztály |
| Kicsi, másolható értékadatok egyenlőséggel? | Rekordstruktúra |
| Változtatható állapot, viselkedés vagy referenciaidentitás? | Class |
| Meglévő osztály speciális verziója? | Származtatott osztály |
| Megosztott képesség a nem kapcsolódó típusok között? | Interface |
Ha ezek közül egyik sem illeszkedik jól, fontolja meg a típusok kombinálását. Egy osztály például implementálhat egy felületet, a rekord pedig egy szerkezet. A teljes összehasonlításért tekintse meg a Típus kiválasztása című témakört.