Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Tip
Dieser Artikel ist Teil des Abschnitts "Grundlagen" , der für Entwickler geschrieben wurde, die mindestens eine Programmiersprache kennen und C# erlernen. Wenn Sie noch nicht mit der Programmierung vertraut sind, beginnen Sie mit "Erste Schritte". Eine Kurzübersichtstabelle finden Sie unter Wählen Sie die Art des Typs aus.
Eine Ihrer ersten Entwurfsentscheidungen in einer beliebigen C#-Anwendung besteht darin, welche Art von Typ erstellt werden soll. Sollte ein Menüelement ein class oder ein record? Sollte eine schnelle Berechnung einen tuple oder einen benannten Typ zurückgeben? Jede Auswahl gestaltet, wie Ihr Code Die Gleichheit, Veränderbarkeit und Polymorphität behandelt. Die falsche Auswahl führt zu Textbausteinen, Fehlern oder beiden.
In diesem Lernprogramm erstellen Sie ein kleines Cafémodell, das Menüelemente, Bestellungen, Sensorlesevorgänge und Rabattrichtlinien verwendet. Sie analysieren die Merkmale und bestimmen den besten C#-Typ für jedes Konzept. Auf dem Weg lernen Sie, die Entwurfsanforderungen zu erkennen, die auf einen bestimmten Typ hinweisen.
In diesem Tutorial lernen Sie:
- Erkennen, wann ein Tupel für die Rückgabe mehrerer Werte geeignet ist.
- Modellieren Sie unveränderliche Daten mit einer Datensatzklasse und verstehen Sie die wertbasierte Gleichheit.
- Stellen Sie kleine, kopierbare Daten mit einer Datensatzstruktur dar.
- Verwalten Sie den veränderbaren Zustand und das Verhalten mit einer Klasse.
- Erweitern einer Klasse durch Vererbung, um Regeln hinzuzufügen oder zu straffen.
- Definieren Sie gemeinsame Fähigkeiten für nicht verwandte Typen mit einer Schnittstelle.
Voraussetzungen
- Installieren Sie das .NET SDK.
Verwenden Sie ein Tupel für eine temporäre Gruppierung
Der Café benötigt eine Methode, die sowohl die Gesamtzahl der Bestellungen als auch den Umsatz für den Tag zurückgibt. Sie könnten eine Klasse oder Struktur dafür definieren, aber zwei Werte aus einer Methode rechtfertigen nicht immer einen neuen Typ.
(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}");
GetDailySummary gibt ein (int TotalOrders, decimal Revenue)Tupel zurück. Der Aufrufer greift auf jedes Element anhand des Namens zu oder zerlegt beide in lokale Variablen. Sie benötigen keine Klassen- oder Strukturdefinition.
Warum ein Tupel in diesem Beispiel am besten funktioniert
Ein Tupel funktioniert hier, da die Gruppierung lokal ist: Eine Methode erzeugt sie, und ein Aufrufer verwendet es. Benannte Elemente machen die Absicht ohne die Zeremonie eines vollständigen Typs klar. Wenn Sie feststellen, dass Sie dieselbe Tupelform über mehrere Methoden übergeben, ist dies ein Signal, um es in einen Datensatz oder eine Klasse höher zu stufen. Diese Entwicklung wird später in diesem Lernprogramm angezeigt. Weitere Informationen zur Tupelsyntax und -funktionen finden Sie unter Tupeltypen.
Verwende einen Record für unveränderliche Daten
Jeder Café benötigt ein Menü. Ein Menüelement hat einen Namen, einen Preis und eine Ernährungsnotiz. Diese Werte werden nicht geändert, nachdem das Element aufgelistet wurde. Zwei Systeme, die beide auf einen "Latte Macchiato zum Preis von 4,50 $" verweisen, sollten übereinstimmen, dass es sich um dasselbe handelt, auch wenn sie separate Objekte kreiert haben.
Deklarieren eines Positionsdatensatzes:
record class MenuItem(string Name, decimal Price, string NutritionalNote);
Der Compiler generiert einen Konstruktor, Dekonstruktor, Equals, GetHashCode und ToString aus dieser einzelnen Zeile. Legen Sie den Datensatz so fest, dass er funktioniert:
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}");
Zwei MenuItem Instanzen mit denselben Daten sind gleich, obwohl sie separate Objekte sind. Dieses Verhalten veranschaulicht die wertbasierte Gleichheit. Der with Ausdruck erstellt eine saisonale Variante, ohne das Original zu verändern.
Eine Datensatzklasse ist der richtige Wert, wenn die Identität aus Daten stammt, nicht aus Objektverweis, und Instanzen ändern sich nach der Erstellung selten. Sie erhalten sofort lesbare ToString() Ausgabe, strukturelle Gleichheit und with Unterstützung ohne zusätzliche Anpassungen. Für eine ausführlichere Anleitung sehen Sie Datensätze und das Datensätze-Tutorial an.
Verwenden einer Datensatzstruktur für kleine Werttypen
Die Kaffeemaschine verfügt über ein integriertes Thermometer, das Temperaturwerte meldet. Jeder Lesevorgang ist klein – eine Zahl und eine Einheit – und wird in Protokolle, Warnungen und Dashboards kopiert. Sie möchten nicht, dass eine Änderung in einer Kopie sich auf die anderen auswirkt.
Deklarieren sie eine Datensatzstruktur:
record struct Measurement(double Value, string Unit);
Verwenden Sie die Datensatzstruktur:
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}");
temp zu copy zuweisen erstellt einen unabhängigen Wert. Der with Ausdruck erzeugt einen neuen Wert, ohne das Original zu berühren – dasselbe Muster wie bei einer Aufzeichnungsklasse, jedoch mit kopieren-bei-Zuweisung-Verhalten anstelle von Kopieren durch Verweis.
Eine Datensatzstruktur passt, wenn die Daten klein sind (einige primitive Felder), und das Kopieren ist günstiger als die Heap-Zuordnung. Sie erhalten die Wertgleichheit und with Unterstützung genauso wie eine Datensatzstruktur mit echter Wertsemantik darunter. Messungen, Koordinaten und ähnliche leichte Daten sind natürliche Kandidaten. Weitere Informationen finden Sie unter "Datensätze und Strukturtypen".
Verwenden Sie eine Klasse, wenn Sie änderbaren Zustand und Verhalten benötigen.
Wenn ein Kunde zur Theke geht, beginnt der Barista eine Bestellung und fügt Artikel nacheinander hinzu. Die Summe wächst, der Status ändert sich von "Ausstehend" in "Bereit", und zwei Aufträge, die gleichzeitig – auch bei identischen Artikeln – getätigt wurden, sind immer noch unterschiedliche Bestellungen.
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);
Die Order Klasse verfolgt Elemente, berechnet eine laufende Summe und macht eine settable Statusverfügbar. Eine Klasse ist hier das richtige Tool, da das Objekt einen veränderbaren Zustand trägt, der sich über die Lebensdauer ändert, das Verhalten (Methoden) für den Zweck des Typs von zentraler Bedeutung ist und identitätsfragend ist – zwei Bestellungen mit denselben Elementen sind immer noch unterschiedliche Reihenfolgen. Weitere Informationen finden Sie unter "Klassen", "Strukturen" und "Datensätze".
Verwenden Sie Vererbung, wenn Sie eine Klasse erweitern müssen
Das Café startet Catering-Veranstaltungen. Ein Cateringauftrag ist immer noch eine Bestellung – es umfasst Artikel und eine Summe – aber es verfolgt auch die Anzahl der Gäste und erfordert eine Genehmigung des Managers, bevor die Küche sie als bereit markiert. Statt die Logik von Order zu duplizieren, leiten Sie eine spezialisierte Klasse ab.
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 verwendet AddItem und Total aus der Basisklasse. Die Status Außerkraftsetzung verschärft den Vertrag – Aufrufe Status = "Ready" ohne vorherige Genehmigung lösen eine Ausnahme aus:
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);
Diese einzelne abgeleitete Klasse veranschaulicht drei Vererbungskonzepte:
-
Status hinzugefügt:
MinimumGuestsundApprovedBynur für die abgeleitete Klasse vorhanden. -
Hinzugefügtes Verhalten: ist neu –
ApproveBasisOrderkennt keine Genehmigungen. -
Außerkraftsetzungsverhalten: Der
StatusSetter erzwingt eine Geschäftsregel, die von der Basisklasse nicht verwendet wird.
Die Vererbung passt, wenn der neue Typ eine spezielle Version des Basistyps ist und Sie beim Hinzufügen oder Straffen von Regeln vorhandene Status und Verhalten wiederverwenden müssen. Eine freigegebene Basisklasse ist natürlicher als eine Schnittstelle, wenn die Typen die Implementierung gemeinsam nutzen, nicht nur einen Vertrag.
Verwenden einer Schnittstelle zum Definieren freigegebener Funktionen
Der Café führt verschiedene Werbeaktionen aus – Happy Hour, Treueprämien, saisonale Specials. Der Auscheckvorgang muss jeden Rabatt anwenden, der heute aktiv ist, ohne die Besonderheiten jeder Richtlinie zu kennen. Sie benötigen eine Möglichkeit, "alles, was einen Rabatt anwenden kann" zu sagen, ohne das Auschecken an eine einzelne Klasse zu binden.
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;
}
Die Checkout Methode akzeptiert alle IDiscountPolicy, sodass Sie neue Richtlinien einführen können, ohne die Auschecklogik zu ändern:
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}");
Eine Schnittstelle deklariert einen Vertrag – eine Gruppe von Membern, die jeder implementierungstyp bereitstellen muss. Die Schnittstelle funktioniert hier, da die Rabatttypen nicht miteinander verknüpft sind (sie teilen keine Basisklasse), aber das Auschecken muss sie einheitlich behandeln. Schnittstellen vereinfachen auch das Testen: Eine Stub-Policy einfügen, ohne den Produktionscode zu berühren. Weitere Details finden Sie unter Schnittstellen.
Verbessern Sie Ihre Schriftwahl
Keiner dieser Entscheidungen ist dauerhaft. Tatsächlich können Sie sie einfach ändern, bevor Sie eine Bibliothek freigeben, in der bahnbrechende Änderungen berücksichtigt werden müssen. Wenn die Anforderungen wachsen, erweitern Sie einen einfachen Typen zu einem reichhaltigeren. Hier sind drei allgemeine Entwicklungen.
Tupel → Datensatz: Die Gruppierung wird weiterhin angezeigt.
Das GetDailySummary Tupel funktioniert innerhalb einer Methode gut, aber sobald Sie es an Berichte, Dashboards und Tests übergeben, zahlt es sich aus, einen benannten Typ zu verwenden. Fördern Sie das Tupel zu einem Datensatz und fügen Sie berechnete Eigenschaften hinzu.
record class DailySummary(int TotalOrders, decimal Revenue)
{
public decimal AverageTicket => TotalOrders > 0 ? Revenue / TotalOrders : 0m;
}
Aufrufer, die das Tupel zuvor dekonstruiert haben, erhalten ToString() jetzt kostenlos, Wertgleichheit und einen natürlichen Ort für abgeleitete Daten wie AverageTicket:
Console.WriteLine("\n=== Evolve: tuple -> record ===");
var daily = new DailySummary(120, 525.75m);
Console.WriteLine(daily);
Console.WriteLine($"Average ticket: {daily.AverageTicket:F2}");
Struktur → Klasse: Sie benötigen Vererbung
Das Wartungsteam des Ladens fordert kalibrierte Messwerte an: einen Sensorwert, der mit einem Offset angepasst wird. Die Measurement Datensatzstruktur eignet sich hervorragend für Rohdaten, aber Strukturen unterstützen keine Vererbung, sodass Sie keine kalibrierte Variante ableiten können. Höherstufen zu einer Klassenhierarchie:
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 erbt von SensorReading und überschreibt Display(), um den Offset einzuschließen. Dieses Muster ist mit einer Struktur oder Datensatzstruktur nicht möglich:
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()}");
Klasse → Klasse + Schnittstelle: Sie benötigen Polymorphismus über Typen hinweg
Die Order Klasse funktioniert gut für sich selbst, aber sobald CateringOrder existiert, müssen Auschecken, Berichterstattung und Drucken jede Bestellung bearbeiten, ohne sich um den konkreten Typ zu kümmern. Extrahieren Sie eine Schnittstelle mit den Elementen, von denen Anrufer tatsächlich abhängig sind:
interface IOrder
{
string Status { get; set; }
decimal Total { get; }
}
Sowohl Order als auch CateringOrder erfüllen bereits diesen Vertrag. Jetzt behandelt eine einzelne Methode einen der folgenden Typen:
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);
Das Extrahieren der Schnittstelle ändert Order oder CateringOrder nicht – es macht einfach ihre gemeinsame Form deutlich, was auch das Testen erleichtert.
Schnellentscheidungsleitfaden
Verwenden Sie diese Tabelle als Ausgangspunkt, wenn Sie nicht sicher sind, welchen Typ Sie auswählen möchten:
| Frage | Optimal geeignet |
|---|---|
| Gibt ein paar Werte aus einer Methode zurück? | Tupel |
| Unveränderliche Daten, bei denen die Gleichheit auf Grundlage der Werte erfolgt? | Record-Klasse |
| Kleine, kopierbare Wertdaten mit Gleichheit? | Datensatzstruktur |
| Veränderbarer Zustand, Verhalten oder Referenzidentität? | Class |
| Spezialisierte Version einer vorhandenen Klasse? | Abgeleitete Klasse |
| Gemeinsame Fähigkeit über nicht verwandte Typen hinweg? | Interface |
Wenn keiner dieser Typen ordentlich passt, sollten Sie erwägen, die Typen zu kombinieren. Beispielsweise kann eine Klasse eine Schnittstelle implementieren, und ein Datensatz kann eine Struktur sein. Den vollständigen Vergleich finden Sie unter Wählen Sie die Art des Typs.