Wie funktionieren Klassen in C#?
Hallo, ich habe große Verständniss Schwierigkeiten mit meinen momentanigen Thema in meinem IT Kurs. Wir machen aktuell C# und antscheind habe ich das Prinzip einer Klasse noch nicht ganz verstanden. Ich werde meinen Code unten rein packen, um Fragen geklärt zu bekommen.
Kontext zum Code
Ich soll im allgemeinen ein Programm schreiben, indem man 4 oder mehr Wiederstände miteinander verrechnet. Dazu soll eine Klasse Wiederstand erstellt werden. Eine Instanz der Klasse kann nur mit der Angabe des Widerstandswerts gebildet werden. Der Wert kann gelesen, aber nicht verändert werden. Instanzen der Klasse Widerstand können mit + addiert werden. Dann ergibt sich der Widerstand, als Gesamtwiderstand einer Reihenschaltung. Der Operator / berechnet den Gesamtwiderstand einer Parallelschaltung. Die Methode ToString() liefert den Widerstandswert als Text mit Einheit zurück. Dazu sollen wir zudem ein User Interface anlegen.
Meine Fragen in Bezug auf den Code:
- Wieso kann ich in Operatoren nur 2 Operatoren gleichzeitig überschreiben? Dazu soll ich ja mehrere Wiederstände beispielsweise in eine Reihenschaltung schalten. Den + operator überschreibe ich ja so, dass ich: R1 + R2 zusammen addieren kann. Das geht ja jedoch nur mit max: R1 + R2 + R3 und dann ist schluss. Die einziege Lösung, die mir da einfällt ist ein Array und eine Schleife zu benutzen.
- Als zusätzliche Lösung für mein 1 Problem, habe ich überlegt statt einen Operator zu überschreiben eine einfache Funktion anzulegen, die dies berechnet. Jedoch ist das nicht die Aufgabenstellung. Dazu meine Frage: Wieso keine Funktion benutzen sondern einen Operator überschreiben, wo liegt da der Sinn?
- Die Frage bezieht sich auf den Code hinter dem User Interface. Wie kann ich generell auf meine Klasse zugreifen und vom User eingebende Werte in meine Klassen Vabriable Wert schreiben? Muss ich dazu immer eine neue Klasse erstellen?
- Wie hängt der Operator mit der Vabriable Wert meiner Klasse zusammen? In den Operatoren habe ich ja Vabriablen wie R1 und R2. Wie werden also dort die verschiedenen Werte herrein geschrieben?
Hier mein Code: (ich kann den leider nur als Foto beilegen, da es sonst die Zeichengrenze überschreitet)
Vielen Dank an jeden, der sich die Geduld und Zeit nimmt mir zu helfen, da ich bald eine Klausur schreibe und schon ein bisschen Verzweifelt bin!
3 Antworten
Frage 1
Das hängt damit zusammen, wie die Operatoren ausgewertet werden.
Es wird von links nach rechts immer nur ein Operator ausgewertet und das Ergebnis wird dann für den nächsten Operator verwendet und so weiter.
Beispiel:
var a = new Test(1);
var b = new Test(2);
var c = new Test(3);
var d = new Test(4);
var x = a + b + c + d;
Console.WriteLine(x.Zahl);
class Test
{
public static Test operator +(Test x, Test y)
{
Console.WriteLine($"{x.Zahl} + {y.Zahl}");
return new Test(x.Zahl + y.Zahl);
}
public int Zahl { get; }
public Test(int zahl)
{
Zahl = zahl;
}
}
Ausgabe:
1 + 2
3 + 3
6 + 4
10
Frage 2:
Das musst Du den Lehrer fragen.
Ich halte es für sinnvoll, solche Funktionen zu kennen, aber für einen Anfänger ist das dann doch etwas unnötig, besonders da man es in der Praxis so gut wie nie braucht. Häufiger sind da die Operatoren "==" und "!=", das sehe ich tatsächlich immer mal wieder.
Generell halte ich das Beispiel für fragwürdig.
Frage 3:
Ich finde es etwas befremdlich, dass Du diese Frage stellen musst, das lässt an der Qualität des Kurses zweifeln.
Du kannst nicht direkt auf die Methoden zugreifen, Du brauchst dafür eine Instanz der Klasse.
Wiederstand a = new Wiederstand(Box1);
Wiederstand b = new Wiederstand(Box2);
Wiederstand c = a + b;
Wiederstand d = a / b;
Anders wäre das, wenn die Methode statisch ist, dann brauchst Du keine Instanz, sondern kannst sie direkt aufrufen. Aber Du sollst ja mit Operatoren arbeiten und dafür brauchst Du Instanzen.
Frage 4:
Ich hatte ja schon geschrieben, dass Operatoren von links nach rechts ausgewertet werden. Zur Laufzeit wird dann die Variable links vom Operator und die rechts davon verwendet. Der Operator selber ist am Ende nur eine besondere statische Methode, die vom Compiler eingesetzt wird.
Mein Beispiel von oben:
var a = new Test(1);
var b = new Test(2);
var c = new Test(3);
var d = new Test(4);
var x = a + b + c + d;
Das kann man auch so umschreiben:
var x = ((a + b) + c) + d;
Oder so:
var x1 = a + b;
var x2 = x1 + c;
var x3 = x2 + d;
Und der Compiler macht daraus:
var x1 = Test.op_Addition(a, b);
var x2 = Test.op_Addition(x1, c);
var x3 = Test.op_Addition(x2, d);
Die Methode heißt tatsächlich so, sieht man auch daran, dass folgender Code nicht funktioniert:
public static Test operator +(Test x, Test y)
{
return new Test(x.Zahl + y.Zahl);
}
public static Test op_Addition(Test x, Test y)
{
return new Test(x.Zahl + y.Zahl);
}
error CS0111: Type 'Test' already defines a member called 'op_Addition' with the same parameter types
PS:
Das alles gilt natürlich auch für die anderen Operatoren.
Vielen Dank, hat sehr viele Unklarheiten aufgeklärt!
zu Punkt 3:
Um aus einem Zahlenwert, der von User eingegeben wurde, einen Widerstand zu machen, musst du tatsächlich eine neue Instanz der Klasse aufrufen. Das geht mit einer Anweisung der Art:
Wiederstand w = new Wiederstand (<Zahlenwert hier>);
So wie du den Konstruktur definiert hast, sollte der Zahlenwert in einer Variablen vom Typ double stehen. In deinen Operatordefinitionen machst du das ja auch, und zwar in den Zeilen 34 und 40 (In Datei Wiederand.cs).
Zu Punkt 4:
In der Datei MainWindow.xaml.cs ist der Code unvollständig und es stehen in den Kommentaren einige Fragen. Hier meine Vorschläge zur Vervollständigung deines Codes:
Nachdem du die Zahlenwerte Box1, Box2 auf den Textfeldern ausgelesen hast, kannst du Widerstände erzeugen. Füge nach Zeile 34 ein:
Wiederstand r1 = new Wiederstand(Box1);
Wiederstand r2 = new Wiederstand(Box2);
Jetzt hast du alles, was du brauchst, um mit deinen Operatoren zu rechnen. Den Code von Zeile 41 bis 50 würde ich durch den folgenden Programmtext ersetzen:
Wiederstand gesamtwiderstand;
if (this.RadioButton_Rreihe.IsChecked.Value)
{
gesamtwiderstand = r1 + r2;
}
else if (this.RadioButton_Parallel.IsChecked.Value)
{
gesamtwiderstand = r1 / r2;
}
Du trägst also die erhaltenen Zahlenwerte in neu erzeugten Instanzen der Klasse Wiederstand ein, um anschließend mit den Operatoren, die für Instanzen dieser Klasse definiert sind, rechnen zu können.
Anmerkung: In den Programmfragmenten habe ich den Klassennamen "Wiederstand" übernommen, damit cut & paste funktioniert, ohne dass der Compiler Fehlermeldungen ausgibt. Natürlich ist der Klassenname mißraten, und falls du dein Programm irgendwo herzeigen musst, solltest du "Wiederstand" systematisch durch "Widerstand" ersetzen.
Dankeschön, hab jetzt verstanden wozu Operatoren überhaupt gut sind. Und das mit dem Wi"e"derstand, wäre mir so schnell nicht aufgefallen, vielen Dank für die Anmerkung 🙈!
Es lohnt sich, darauf hinzuweisen, dass für Operatoren in C# Vorrangregeln gelten, die auch auf selbst definierte Operatoren angewendet werden. Es gilt, dass der Operator "/" Vorrang hat vor dem Operator "+". Das hat eine Reihe interessanter Konsequenzen für Formeln, in denen mehr als zwei Widerstände und beide Operatoren vorkommen.
Wenn wir drei Widerstände r1, r2, r3 haben und den Ausdruck
gesamtwiderstand = r1 + r2/r3
anschreiben, wird auf der Grundlage der Vorrangregeln zuerst r2/r3 berechnet. Mit dem erhaltenen Ergebnis wird anschließend der Additionsoperator ausgeführt. Der Ausdruck berechnet den Gesamtwidersand für die folgende Schaltung:
+---------+
+------| r2 |----+
+-------+ | +---------+ |
--------| r1 | -----+ +-----
+-------+ | +---------+ |
+------| r3 |----+
+---------+
Man könnte den Rechenausdruck natürlich auch mit Klammern in der folgenden Form schreiben
gesamtwiderstand = r1 + (r2/r3)
wegen der Vorrangregeln sind diese Klammern allerdings nicht erforderlich.
In der Formel
gesamtwiderstand = (r1 + r2)/r3;
werden Klammern verwendet, um von den Vorrangregeln abzuweichen. Es wird nun erst die Reihenschaltung von r1 und r2 bestimmt und der erhaltene Ersatzwiderstand sodann mit r3 parallel geschaltet. Das zugehörige Schaltbild ist:
+-------+ +-------+
+----| r1 |---| r2 |---+
| +-------+ +-------+ |
-----------+ +---------
| +-------+ |
+----------| r3 |---------+
+-------+
Gucken wir uns noch eine kompliziertere Schaltung an:
+-------+ +-------+
+----| r2 |---| r3 |---+
+-------+ | +-------+ +-------+ |
------| r1 |-----+ +-----
+-------+ | +-------+ |
+----------| r4 |---------+
+-------+
Den Gesamtwiderstand erhalten wir mit den Ausdruck
gesamtwiderstand = r1 + (r2 + r3)/r4
Wenn das zu kompliziert erscheint, kann man natürlich auch kleinere Teilaufgaben rechnen und die erhaltenen Zwischenergebnisse speichern. Das geht dann so:
r23 = r2 +r3;
r234 = r23/r4;
gesamtwiderstand = r1 + r234;
So würde man den Gesamtwiderstand der gezeigten Schaltung nach alter Väter Sitte mit Bleistift und Papier ausrechnen.
Völlig gleichwertig ist der folgende Ausdruck:
gesamtwiderstand = (r2 + r3)/r4 + r1;
Auf die Klammern um den Teilausdruck "r2 + r3" kann nicht verzichtet werden, der Audruck
gesamtwiderstand = r2 + r3/r4 + r1;
berechnet nämlich den Widerstand der folgenden Schaltung:
+--------+
+----| r3 |----+
+------+ | +--------+ | +------+
-----| r2 |-----+ +---| r1 |---
+------+ | +--------+ | +------+
+----| r4 |----+
+--------+
Die Anwendung des Additionsoperators auf mehr als zwei Widerstände ist unproblematisch, weil sowohl die Addition als auch die Reihenschaltung kommutativ und assoziativ sind.
Die Parallelschaltung ist das auch, aber die Division ist nur linksassoziativ. Das genügt aber, denn der Compiler macht aus x / y / z dann immer (x / y) / z, obwohl auch x / (y / z) richtig wäre. Für den Compiler ist das nur eine Division.
Es geht auch R1 + R2 + R3 + R4, denn der Compiler macht daraus ((R1 + R2) + R3) + R4. Er kann die Klammern auch anders setzen (Assoziativität) oder Summanden vertauschen (Kommutativität).
Der Compiler tauscht da nichts, die von dir gesetzten Klammern sind so, wie es der Compiler ausspuckt.
Wenn es in C++ keinen Standardkonstruktor gibt, dann wird das vom Compiler sogar erzwungen. Über C# weiß ich zu wenig, und das dort bestätigen zu können.
WENN die Klasse gar keinen Konstruktur hat, nur dann wird ein Standardkonstruktor erstellt.
Wenn die Klasse Konstruktoren mit beliebigen Parametern hat, aber keinen Standardkonstruktor, dann wird kein Standardkonstruktor generiert.
Auch weiß ich nicht, ob es in C# Copy-Konstruktoren gibt
Du kannst einen Konstruktor mit einem Parameter vom Typ derselben Klasse erstellen und im Konstruktor dann alle Werte übernehmen, das wäre ein Copy-Konstruktor.
Ein Feature so in der Art gibt es (leider) nur bei records, da gibt es die "with" Syntax, die eine neue Instanz erstellt, aber die Werte im "with"-Block ändert.
ob diese oder Zuweisungsoperatoren den Widerstandswert eines bereits vorhandenen Widerstands ändern können
Ich bin nicht 100% sicher was Du meinst, aber ich würde behaupten: Nein.
Im Operator können zwar die Eigenschaften der übergebenen Parameter geändert werden, aber auch nur, wenn deren Typen das zulassen.
Es reicht also, die Eigenschaft der Klasse readonly zu machen (sprich: kein set) und ist abgesichert.
Danke. Da habe ich mal wieder was gelernt. 😀
Ich habe zumindest mit meinen C++ - Kenntnissen versucht, eine C# - Frage zu beantworten.
Nachträge:
Zu 1.
Es geht auch R1 + R2 + R3 + R4, denn der Compiler macht daraus ((R1 + R2) + R3) + R4. Er kann die Klammern auch anders setzen (Assoziativität) oder Summanden vertauschen (Kommutativität).
Zu 2.
Operatoren sehen eben hübscher (übersichtlicher) aus.
R1 + R2 + R3 sieht besser aus, als
(R1.Add(R2)).Add(R3)
Zu 3.
Du musst nicht eine neue Klasse erstellen, sondern nur eine neue Instanz der Klasse.
Wiederstand(3) erzeugt einen Widerstand (ohne e) von 3 Ohm. Der muss nicht mal einen Namen haben.
Zu 4.
Die Werte werden schon vom Konstruktior hineingeschrieben.
Wenn es in C++ keinen Standardkonstruktor gibt, dann wird das vom Compiler sogar erzwungen. Über C# weiß ich zu wenig, und das dort bestätigen zu können. Auch weiß ich nicht, ob es in C# Copy-Konstruktoren gibt und ob diese oder Zuweisungsoperatoren den Widerstandswert eines bereits vorhandenen Widerstands ändern können. In C++ muss man als Programmierer das explizit verhindern.