Kursinhalt
C# Über die Grundlagen Hinaus
C# Über die Grundlagen Hinaus
Vererbung
Wir haben uns im letzten Abschnitt mit dem Konzept der abgeleiteten Klassen beschäftigt. Diese Eigenschaft einer Klasse, Eigenschaften von einer anderen Klasse zu erben, wird als Vererbung bezeichnet.
Obwohl wir das Konzept der Vererbung bereits kennen, werden wir es diesmal etwas umfassender durchgehen, um es gründlicher zu verstehen.
Als schnelle Wiederholung folgt ein Beispiel für Vererbung:
index
#pragma warning disable CS0169 // To disable some unnecessary compiler warnings for this example. Using this is not a recommended practice. using System; class Mammal { int age; float weight; // kilogram (1 kg = 2.2 pounds) } class Dog : Mammal { string breed; public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { Cat myCat = new Cat(); Dog myDog = new Dog(); myCat.speak(); myDog.speak(); } }
Der obige Code enthält eine Elternklasse namens Mammal
und zwei abgeleitete Klassen namens Cat
und Dog
.
Beachten Sie, dass keine der Klassen einen explizit definierten Konstruktor hat, was bedeutet, dass diese Klassen einen Standardkonstruktor verwenden, wenn ein Objekt erstellt wird.
Lassen Sie uns einen Konstruktor für die Mammal
-Klasse manuell erstellen, der ein Mammal
-Objekt mit einigen Daten initialisiert:
index
#pragma warning disable CS0169 // To disable some unnecessary compiler warnings for this example. Using this is not a recommended practice. using System; class Mammal { int age; float weight; // kg public Mammal(int age, float weight) { this.age = age; this.weight = weight; } } class Dog : Mammal { string breed; public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object with some data Mammal m1 = new Mammal(10, 42.0f); } }
Wenn wir versuchen, dieses Programm zu kompilieren, werden einige Fehler in der Konsole angezeigt. Um diese Fehler zu verstehen, müssen wir zuerst zwei wichtige Konzepte im Zusammenhang mit Konstruktoren verstehen.
Das erste ist, dass sobald wir einen Konstruktor für eine Klasse explizit definieren, diese Klasse keinen Standard-Konstruktor mehr hat, und daher wird der explizit definierte Konstruktor zum Hauptkonstruktor dieser Klasse, der in diesem Fall ist:
index
public Mammal(int age, float weight) { this.age = age; this.weight = weight; }
Daher müssen wir beim Erstellen eines neuen Objekts immer die erforderlichen Argumente des Konstruktors in der richtigen Reihenfolge übergeben:
index
// Incorrect ways to create 'Mammal', will show an error Mammal m1 = new Mammal(); Mammal m1 = new Mammal(10); Mammal m1 = new Mammal(42.0f); // Correct way to create 'Mammal', will execute fine. Mammal m1 = new Mammal(10, 42.0f);
Zweitens können abgeleitete Klassen ebenfalls Konstruktoren haben, jedoch wird vor dem Aufruf des Konstruktors einer abgeleiteten Klasse auch der Konstruktor der Basisklasse (Elternklasse) aufgerufen:
index
#pragma warning disable CS0169 // To disable some unnecessary warnings, using this is not a recommended practice. using System; class Mammal { int age; float weight; // kg // No attribute is initialized explicitly in this constructor // Hence, all attributes will take up "zero" values // It is similar to a "default" constructor except it outputs a message public Mammal() { Console.WriteLine("Mammal Constructor Called"); } } class Dog : Mammal { string breed; public Dog() { Console.WriteLine("Dog Constructor Called"); } } class ConsoleApp { static void Main() { Dog myDog = new Dog(); } }
Wenn wir diesen Code ausführen, sehen wir, dass die WriteLine()
-Methode aus dem 'Mammal'-Konstruktor, der die Elternklasse ist, automatisch aufgerufen wird. Das bedeutet, dass es eine Regel ist, dass der Konstruktor der Basisklasse (auch Basiskonstruktor genannt) immer vor dem Konstruktor der abgeleiteten Klasse aufgerufen wird.
Diese Regel gilt auch im Falle der mehrstufigen Vererbung:
In dem obigen Diagramm ruft der Kitten
-Konstruktor den Cat
-Konstruktor vor seinem eigenen auf, da Cat
jedoch auch eine abgeleitete Klasse ist, ruft er den Mammal
-Konstruktor vor sich selbst auf, und Mammal
ruft den Animal
-Konstruktor vor seinem Konstruktor auf, daher ist insgesamt der erste Konstruktor, der ausgeführt wird, der Konstruktor der Superklasse - das ist der Konstruktor der Animal
-Klasse, und dann geht es von dort aus weiter.
Wenn der Konstruktor der Elternklasse kein Argument benötigt, wird er automatisch vom Compiler aufgerufen, das ist der Grund, warum der 'Mammal'-Konstruktor im obigen Beispiel automatisch aufgerufen wurde. Schauen wir uns jedoch den fehlerhaften Code noch einmal an:
index
using System; class Mammal { int age; float weight; // kg public Mammal(int age, float weight) { this.age = age; this.weight = weight; } } class Dog : Mammal { string breed; public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object with some data Mammal m1 = new Mammal(10, 42.0f); } }
Im obigen Code erhalten wir zwei Fehler, die im Wesentlichen bedeuten, dass wir die Basiskonstruktoren nicht manuell aufgerufen haben - da sie einige Argumente erfordern, müssen wir sie manuell aufrufen. Die grundlegende Syntax für den manuellen Aufruf des Konstruktors der Elternklasse ist die folgende:
index
class DerivedClassName : ParentClassName { // ... attributes // ... methods public DerivedClassName(int arg1, int arg2, ...) : base(arg1, arg2, ...) { // code here } }
Beispiel:
index
using System; class ExampleParentClass { int value1; int value2; public ExampleParentClass(int value1, int value2) { this.value1 = value1; } } class ExampleDerivedClass : ExampleParentClass { int value3; // The value1 and value2 arguments are passed to the base class's contructor public ExampleDerivedClass(int value1, int value2, int value3) : base (value1, value2) { this.value3 = value3; } } class ConsoleApp { static void Main() { var testObject = new ExampleDerivedClass(5, 7, 9); } }
Mit dieser Syntax können wir alle erforderlichen Daten an den Mammal
-Konstruktor über die Cat
- und Dog
-Konstruktoren übergeben, um den Fehler zu beheben, den wir zuvor hatten:
index
using System; class Mammal { int age; float weight; // kg public Mammal(int age, float weight) { this.age = age; this.weight = weight; } } class Dog : Mammal { string breed; public Dog(int age, float weight, string breed) : base(age, weight) { this.breed = breed; } public void speak() { Console.WriteLine("Woof!"); } } class Cat : Mammal { string furPattern; public Cat(int age, float weight, string furPattern) : base(age, weight) { this.furPattern = furPattern; } public void speak() { Console.WriteLine("Meow!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object with some data Mammal m1 = new Mammal(10, 42.0f); // Creating a "Dog" object with some data Dog d1 = new Dog(10, 42.5f, "Dobermann"); Console.WriteLine("Executed Successfully"); } }
Ein weiteres wichtiges Merkmal von Konstruktoren ist, dass wir Konstruktoren überladen können, genau wie wir jede andere Methode überladen. Wir können mehrere Konstruktoren mit unterschiedlicher Anzahl von Argumenten erstellen:
index
class Mammal { int age; float weight; // kg // 1st constructor public Mammal() { // We leave it empty for this example // Since it's empty, it mimics the "default" constructor } // 2nd constructor public Mammal(int age) { this.age = age; } // 3rd constructor public Mammal(int age, float weight) { this.age = age; this.weight = weight; } }
In diesem Fall hat die Mammal
-Klasse 3 Konstruktoren. Wir können also ein Säugetierobjekt auf 3 verschiedene Arten initialisieren oder erstellen, und der Compiler wählt, welchen Konstruktor er basierend auf der Anzahl und dem Typ der Argumente aufruft:
index
// All Correct var m1 = new Mammal(); var m2 = new Mammal(10); var m3 = new Mammal(10, 42.5f);
Das bedeutet auch, dass wir jeden der 3 Konstruktoren aus den Konstruktoren der abgeleiteten Klasse aufrufen können. Zum Beispiel sind alle diese gültig:
index
// Using 3rd base constructor public Dog(int age, float weight, string breed) : base(age, weight) { this.breed = breed; } // Using 2nd base constructor public Dog(int age, string breed) : base(age) { this.breed = breed; } // Using 1st base constructor // If the base constructor has no arguments then it is automatically called (similar to the default constructor), so we don't necessarily need to write 'base()' public Dog(string breed) { this.breed = breed; }
Lassen Sie uns die obigen zwei Code-Snippets zusammenfügen und einige Console.WriteLine
-Anweisungen hinzufügen, um zu sehen, in welcher Reihenfolge die Konstruktoren ausgeführt werden, um die Ergebnisse praktisch zu sehen:
index
using System; class Mammal { int age; float weight; // kg // 1st Constructor public Mammal() { // We leave it empty for this example // Since it's empty, it mimics the "default" constructor // The attributes are initialized with zero values Console.WriteLine("Mammal - Constructor 1 Called"); } // 2nd Constructor public Mammal(int age) { this.age = age; Console.WriteLine("Mammal - Constructor 2 Called"); } // 3rd Constructor public Mammal(int age, float weight) { this.age = age; this.weight = weight; Console.WriteLine("Mammal - Constructor 3 Called"); } } class Dog : Mammal { string breed; public Dog() { Console.WriteLine("Dog - Constructor 1 Called"); } // Using 1st Mammal constructor // We don't necessarily need to write 'base()' in this case // It automatically finds and calls the constructor with no arguments public Dog(string breed) { this.breed = breed; Console.WriteLine("Dog - Constructor 2 Called"); } // Using 2nd Mammal constructor public Dog(int age, string breed) : base(age) { this.breed = breed; Console.WriteLine("Dog - Constructor 3 Called"); } // Using 3rd Mammal constructor public Dog(int age, float weight, string breed) : base(age, weight) { this.breed = breed; Console.WriteLine("Dog - Constructor 4 Called"); } public void speak() { Console.WriteLine("Woof!"); } } class ConsoleApp { static void Main() { // Creating a "Mammal" object using different constructors Mammal m1 = new Mammal(10, 42.0f); Mammal m2 = new Mammal(10); Mammal m3 = new Mammal(); Console.WriteLine("----------"); // Seperator, for ease of reading output // Creating a "Dog" object using different constructors Dog d1 = new Dog(10, 42.0f, "Dobermann"); Console.WriteLine(""); Dog d2 = new Dog(10, "Dobermann"); Console.WriteLine(""); Dog d3 = new Dog("Dobermann"); Console.WriteLine(""); Dog d4 = new Dog(); } }
Jetzt, da Sie über die verschiedenen Merkmale der Vererbung Bescheid wissen, sollten Sie auch wissen, wie oder wann Sie diese korrekt verwenden. Im Folgenden sind einige Dinge aufgeführt, die Sie beachten sollten, wenn Sie eine auf Vererbung basierende Klassenstruktur in Betracht ziehen:
Balance zwischen Einfachheit und Flexibilität: Konstruktorüberladung ermöglicht es Ihnen, viele verschiedene Konstruktoren zu haben, die unterschiedliche Arten von Argumenten annehmen, aber ein Übermaß kann den Code komplizierter und schwer wartbar machen. Es ist eine bewährte Praxis, den Klassen-Code kurz, prägnant und bequem zu halten. Vermeiden Sie es, zu viele Konstruktoren für eine Klasse zu erstellen, um ein Gleichgewicht zwischen Einfachheit und Flexibilität zu wahren.
Halten Sie Konstruktoren einfach: Konstruktoren sollten hauptsächlich dafür verantwortlich sein, ein Objekt mit Basisdaten zu initialisieren. Es ist eine bewährte Praxis, unnötige Verarbeitung und komplexe Logik innerhalb eines Konstruktors zu vermeiden. Wenn eine Berechnung oder Logik erforderlich ist, ist es besser, eine separate Methode dafür zu erstellen.
Schlechte Praxis:
index
class Customer { string name; string accountType; double balance; public Customer (string name, string accountType, double balance) { this.name = name; this.accountType = accountType; if (accountType == "Savings") { // Plus 1 Percent this.balance = balance + balance * 0.01; } else if (accountType == "HighYieldSavings") { // Plus 5 percent this.balance = balance + balance * 0.05; } else { this.balance = balance; } } }
Gute Praxis:
index
class Customer { string name; string accountType; double balance; public Customer (string name, string accountType, double balance) { this.name = name; this.accountType = accountType; this.balance = balance; monthlyInterest(); } // This method might be used in other places too private void monthlyInterest() { if(accountType == "Savings") { // Plus 1 Percent balance += balance * 0.01; } else if(accountType == "HighYieldSavings") { // Plus 5 percent balance += balance * 0.05; } } }
Wichtige Attribute initialisieren: Es ist notwendig, alle wichtigen Attribute eines Objekts mit korrekten Werten zu initialisieren, um sicherzustellen, dass sie korrekt funktionieren - selbst wenn es sich um einen Konstruktor ohne Argumente handelt.
Schlechte Praxis:
index
public class Car { private string brand; private string model; private int year; private double price; // Constructor does not initialize important attributes // It is also generally not a good idea to have constructors without any arguments if they're not needed. public Car() { // No initialization of attributes Console.WriteLine("Car Created"); } }
Gute Praxis:
index
public class Car { private string brand; private string model; private int year; private double price; // Good: Constructor initializes important attributes // It also checks if the values are correct // In this case the if-else statements are not unnecessary since they are important for ensuring that the object functions correctly. public Car(string brand, string model, int year, double price) { this.brand = brand; this.model = model; // Validate and set the year // The first public car was created in 1886 :) if (year > 1886) { this.year = year; } else { Console.WriteLine("Invalid year. Setting year to default."); this.year = DateTime.Now.Year; // Set to current year as default } // Validate and set the price if (price >= 0) { this.price = price; } else { Console.WriteLine("Invalid price. Setting price to default."); this.price = 0; // Set to a default value } } }
1. Welche Funktion ermöglicht es uns, mehrere Konstruktoren für eine Klasse zu erstellen?
2. Möglicherweise müssen Sie eines der Konzepte aus den vorherigen Abschnitten in diesem Quiz verwenden. Der folgende Code enthält einen Fehler in den Zeilen 15 und 16. Schauen Sie sich den Code genau an und entscheiden Sie, was eine effiziente Lösung für diesen Fehler ist:
Danke für Ihr Feedback!