Objektorientiertes Programmieren in C#
Theorie
Auftrag 1: Vektoren
Vektoren sind eine praktische Sache (Tatsache!). Ziel dieses Auftrags ist, eine Vector-Klasse in C# zu implementieren, damit du in deinem C# gut mit vektoriellen Grössen umgehen kannst. Man könnte meinen, dass ein Array bereits so etwas ist wie ein Vektor, allerdings kann man mit diesen nicht rechnen.
Teil I: Konstruktor
Erstelle auf GitHub ein Repo CsharpVectorMath (private, gitignore: Visual Studio) und klone es auf deinen Computer.
Erstelle darin ein C#-Projekt VectorMath.
Erstelle darin eine Klasse Vector:
wichtig in C#: jede Klasse in eigenem File!
Name der Klasse soll genau zu den davon erzeugten Objekten passen, deshalb Vector und nicht z.B. Vectors (ein Objekt dieser Klasse ist ein einzelner Vektor.
neue Klasse: Projektmappe → Rechte Maustaste auf Projekt → Hinzufügen → Neue Klasse
Informiere dich darüber, was der Konstruktor ist und wozu dieser gut ist.
Erstelle den Konstruktor für die Klasse. Diesem soll ein double-Array übergeben werden, welches dann in einer Eigenschaft Components
gespeichert wird.
In der Main-Methode (in Program.cs) sollte man jetzt Objekte der Vector-Klasse erstellen können:
Vector v = new Vector(new double[] { 3.0, -7.0, 42.0 });
Teil II: Eigenschaften, getter & setter
Informiere dich darüber, was die Eigenschaften einer Klasse sind und wozu, wann und wie man getter und setter verwendet.
Implementiere (über getter) die Eigenschaften unten. Achte darauf, dass es nicht möglich ist, diese Werte von Hand zu setzen. Stattdessen sollen sie jedesmal aus den Komponenten bestimmt werden.
Dimension
gibt Dimension des Vektors an, also z.B. 3
für einen Vektor mit drei Komponenten
Magnitude
gibt Länge des Vektors an
UnitVector
gibt den zugehörigen Einheitsvektor (wieder ein Vektor!!!) an. Erinnerung: Einheitsvektor zeigt in gleiche Richtung, hat aber Länge $1$.
IsZeroVector
gibt true
(false
) zurück, wenn Vektor (nicht) ein Nullvektor ist. Achtung: Doubles sind auf etwa $16$ Stellen genau, d.h. sehr kleine Zahlen $<10^{-15}$ sollte man auch als $0$ interpretieren. Definiere dazu ein private Feld (z.B. _accuracy
, Underline weil private) die die Genauigkeit festlegt.
IsUnitVector
gibt true
(false
) zurück, wenn Vektor (nicht) ein Einheitsvektor ist. Beachte wieder, dass doubles ungenau sind.
Teil III: Klassen-Methoden
Informiere dich darüber, was die Methoden einer Klasse sind und wie man sie implementiert. Finde weiter heraus, wie man eine Ausnahme ausgelöst (Exception handling) kann (throw new Exception(...)
).
Implementiere die Vektoraddition, Vektorsubtraktion und das Skalarprodukt (dot product) in den Methoden Add
, Sub
und DotProduct
. Diesen Methoden wird jeweils ein Vektor übergeben und mit diesem wird dann die entsprechende Operation ausgeführt. Mit Vector vSum = v1.Add(v2)
soll man dann die Vektorsumme der beiden Vektoren v1
und v2
bestimmen und in einem neuen Vektor vSum
speichern können. Die Methoden sollen natürlich die passenden Rückgabetypen haben. Achte darauf, dass diese Operationen nur durchgeführt werden können, wenn beide Vektoren die gleiche Dimension haben. Ansonsten soll eine Ausnahme/Exception ausgelöst werden.
Implementiere weiter die skalare Multiplikation (ScalarMultiplication()
), mit der ein Vektor mit einer Zahl (double) multipliziert wird. Damit kann man einen Vektor skalieren also seine Länge ändern, ohne dass seine Richtung geändert wird. Ausnahme: entgegengesetzte Richtung bei Multiplikation mit negativen Zahlen.
Teil IV: Überladung von Methoden und Konstruktor
Im Code unten werden drei Methoden mit dem gleichen Namen SayHi
definiert. Die drei Funktionen nehmen aber unterschiedliche Argumente entgegen. Rufe ich SayHi("Fritz")
auf, so erkennt C# anhand des Arguments, dass die mittlere Funktion gemeint ist und führt diese entsprechend aus. Dies nennt man eine Überladung von Methoden.
public static void SayHi()
{
Console.WriteLine("Hi, person with no name!");
}
public static void SayHi(string name)
{
Console.WriteLine("Hi, person with name " + name);
}
public static void SayHi(int nr)
{
Console.WriteLine("Hi, person with number " + nr);
}
Überlade nun den Konstruktor: Einmal soll diesem nur das Double-Array übergeben werden. Einmal soll zusätzlich ein Wert für die Genauigkeit übergeben werden.
Die Genauigkeit soll auch im Nachhinein angepasst werden können, dazu muss diese aber public gemacht werden. Wir wollen aber eine Obergrenze setzen, z.B. soll für die Genauigkeit kein Wert $> 10^{-5}$ (oder was auch immer) gesetzt werden können. Dazu gehen wir wie folgt vor: Wir haben ein privates Feld
_accuracy
und eine public Eigenschaft
Accuracy
. Über dessen
getter/setter wird der Wert von
_accuracy
gesetzt, wobei sichergestellt wird, dass ein erlaubter Wert gesetzt wird (Stichword: setter!). Siehe z.B. hier für mehr Infos dazu:
https://codeasy.net/lesson/properties
In Realität benötigt man meist 2D- und 3D-Vektoren. Daher macht es Sinn, für diese Fälle weitere Überladungen des Konstruktors zu machen: Vecor(3,-42)
und Vector(3,-42,7)
soll dann einen entsprechenden 2D- resp. 3D-Vektor erzeugen. Die Genauigkeit wird dabei jeweils auf den Standardwert gesetzt.
Teil V: Method Overriding
Schön wäre doch, wenn man einfach mit Console.Writeline(v)
die Komponenten des Vektors schön ausgeben könnte. Wie du dich selbst vergewissern kannst, funktioniert dies (noch) nicht.
Console.Writeline
ruft jeweils die Klassenmethode ToString()
auf. Für gewissen Datentypen wie ints wurde diese Methode bereits implementiert. Deshalb können solche Datentypen mit Console.Writeline
angezeigt werden, obwohl sie keine Strings sind - sie werden vor der Anzeige in Strings umgewandelt.
Implementiere die Methode ToString()
für unsere Vektorklasse:
public override String ToString()
{
// YOUR CODE HERE
}
Ein Vektor soll dann wie folgt ausgegeben werden: [3,-42,7]
Vergewissere dich, dass Console.Writeline
nun für Vektoren funktioniert.
Teil VI: Statische Methoden
Gegeben seien zwei Vektoren v1
und v2
. Möchten wir nun die Vektorsumme der beiden bestimmen, so haben wir zwei Möglichkeiten: v1.Add(v2)
oder v2.Add(v1)
. Im ersten Fall verwenden wir also die Klassenmethode Add
des ersten Vektors und übergeben den zweiten Vektor als Argument. Schön wäre aber, wenn wir dies ohne die Klassenmethode machen könnten und einfach Add(v1,v2)
schreiben könnten. Eine solche Methode heisst statische Methode. Sie ist objektunabhängig, gehört also nicht direkt zum Objekt. Wir könnten diese neue Add-Methode (Add(v1,v2)
) deshalb in der Main-Methode deklarieren, was aber nicht sehr viel Sinn ergibt. Die Methode passt thematisch zur Vector-Klasse, da sie zwei Vektoren entgegennimmt, diese Addiert und dann wieder einen Vector zurückgibt. Deshalb liegt es auf der Hand, diese Methode als statische Methode in der Vector-Klasse zu definieren.
Beachte, dass du dann zwei Vektoren auf drei verschiedene Arten miteinander addieren kannst: v1.Add(v2)
, v2.Add(v1)
und Add(v1,v2)
. Die drei Methoden heissen zwar immer gleich, es sind aber unterschiedliche Methoden: Im ersten und zweiten Fall handelt es sich um eine Klassenmethode von v1
resp. v2
, im dritten Fall um eine statische Methode. Wie oben (Überladung von Methoden) besprochen, erkennt C# aufgrund der Art und Weise, wie die Funktion aufgerufen wird, um welche der drei Möglichkeiten es sich handelt. Man kann also also den gleichen Namen für verschiedene Methoden verwenden.
Tipp: Definiere die statischen Methoden wenn immer möglich durch Klassenmethoden.
Implementiere statische Methoden Add(...,...)
, Sub(...,...)
, DotProduct(...,...)
.
Implementiere die statische Methode ScalarMultiplication
. Diese soll unabhängig der Reihenfolge der Argumente funktionieren ScalarMultiplication(s,v)
und ScalarMultiplication(v,s)
.
-
Teil VII: Überladung von Operatoren
Noch praktischer wäre, wenn man einfach v1 + v2
anstelle von Add(v1,v2)
schreiben könnte. Dies ist möglich und heisst Überladung von Operatoren: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading
Implementiere diese Überladungen so dass man einfach kann:
addieren & subtrahieren: v1 + v2
und v1 - v2
Vektoren mit Vorzeichen: +v
und -v
Multiplizieren: v1*v2
ist Skalarprodukt (dot product) und s*v
und v*s
sind skalare Multiplikation (ScalarMultiplication)
Teil VIII: Weitere Vektormethoden
Implementiere die folgenden, nützlichen Methoden für Vektoren.
Tipps:
Verwende wann immer möglich bereits implementierte Methoden und Eigenschaften.
Definiere die Methode zuerst als Klassenmethode und dann (mithilfe dieser) als statische Methode - so kann sie auf zwei Arten genutzt werden.
AngleInRad(v1,v2)
und
AngleInDeg(v1,v2)
: Bestimmt den Winkel (im Bogenmass und in Grad) zwischen den beiden Vektoren, siehe
https://de.wikipedia.org/wiki/Skalarprodukt. Achtung: Der Winkel zwischen zwei Vektoren ist nur definiert, falls
keiner der beiden ein
Nullvektor ist.
AreParallel(v1,v2)
, AreAntiparallel (v1,v2)
, AreParallelOrAntiparallel(v1,v2)
, ArePerpendicular(v1,v2)
geben true/false zurück. Parallel heisst: zeigen in gleiche Richtung, Antiparallel heisst: zeigen in entgegengesetzte Richtung. Achtung auch wieder vor Nullvektoren!
SameLength(v1,v2)
: true/false
Eigene Ideen
Teil IX: Aufgaben
Öffne dein Dossier oder deine Aufgabenblätter aus dem Matheunterricht zum Thema Vektoren. Löse einige Aufgaben mithilfe deiner Vector-Klasse.
Lösung
using System;
namespace VectorMath
{
public class Vector
{
public double[] Components;
private double _accuracyDefault = 1e-15;
private double _accuracyMax = 1e-1;
private double _accuracy;
// PROPERTIES
public double Accuracy
{
get
{
return _accuracy;
}
set
{
if (value <= _accuracyMax)
{
_accuracy = value;
}
else
{
_accuracy = _accuracyDefault;
}
}
}
public int Dimension
{
get
{
return Components.Length;
}
}
public double Magnitude
{
get
{
double magn = 0;
foreach (var c in Components)
{
magn += c * c;
}
return Math.Sqrt(magn);
}
}
public Vector UnitVector
{
get
{
double[] A = new double[Dimension];
for (int i = 0; i < A.Length; i++)
{
A[i] = Components[i] / Magnitude;
}
return new Vector(A);
}
}
public bool IsZeroVector
{
get
{
if (Magnitude < Accuracy)
{
return true;
}
return false;
}
}
public bool IsUnitVector
{
get
{
double a = Math.Abs(Magnitude - 1);
if (Math.Abs(Magnitude - 1) < Accuracy)
{
return true;
}
return false;
}
}
// CONSTRUCTOR
public Vector(double[] _components)
{
Components = _components;
Accuracy = _accuracyDefault;
}
public Vector(double[] _components, double _acc)
{
Components = _components;
Accuracy = _acc;
}
public Vector(double x, double y)
{
Components = new double[] { x, y };
Accuracy = _accuracyDefault;
}
public Vector(double x, double y, double z)
{
Components = new double[] { x, y, z };
Accuracy = _accuracyDefault;
}
// CLASS METHODS
public override String ToString()
{
// can now print vector with Console.Writeline
string l = "[";
for (int i = 0; i < Dimension - 1; i++)
{
l += Components[i].ToString() + ",";
}
l += Components[Dimension - 1] + "]";
return l;
}
public Vector Add(Vector V)
{
checkIfSameDimension(V);
double[] A = new double[Dimension];
for (int i = 0; i < A.Length; i++)
{
A[i] = Components[i] + V.Components[i];
}
return new Vector(A);
}
public Vector Sub(Vector V)
{
checkIfSameDimension(V);
double[] A = new double[Dimension];
for (int i = 0; i < A.Length; i++)
{
A[i] = Components[i] - V.Components[i];
}
return new Vector(A);
}
public double DotProduct(Vector V)
{
checkIfSameDimension(V);
double d = 0;
for (int i = 0; i < Dimension; i++)
{
d += Components[i] * V.Components[i];
}
return d;
}
public Vector ScalarMultiplication(double s)
{
double[] A = new double[Dimension];
for (int i = 0; i < A.Length; i++)
{
A[i] = s * Components[i];
}
return new Vector(A);
}
public double AngleInRad(Vector v)
{
if (IsZeroVector)
{
raiseZeroVectorError();
}
else if (v.IsZeroVector)
{
v.raiseZeroVectorError();
}
return Math.Acos(DotProduct(this, v) / (this.Magnitude * v.Magnitude));
}
public double AngleInDeg(Vector v)
{
return AngleInRad(v) * 180 / Math.PI;
}
public bool AreParallel(Vector v)
{
if (Math.Abs(AngleInRad(v)) < Accuracy) { return true; }
else { return false; }
}
public bool AreAntiparallel(Vector v)
{
if (Math.Abs(AngleInRad(v) - Math.PI) < Accuracy) { return true; }
else { return false; }
}
public bool AreParallelOrAntiparallel(Vector v)
{
if (AreParallel(v) || AreAntiparallel(v)) { return true; }
else { return false; }
}
public bool ArePerpendicular(Vector v)
{
if (Math.Abs(AngleInRad(v) - Math.PI / 2) < Accuracy) { return true; }
else { return false; }
}
public bool HaveSameMagnitude(Vector v)
{
if (Math.Abs(Magnitude - v.Magnitude) < Accuracy) { return true; }
else { return false; }
}
private void checkIfSameDimension(Vector V)
{
if (V.Dimension != Dimension)
{
throw new Exception("Wrong dimension ERROR!");
}
}
private void raiseZeroVectorError()
{
throw new Exception("Zero Vector Exception!");
}
// STATIC METHODS
public static Vector ZeroVector(int n)
{
double[] a = new double[n];
for (int i = 0; i < n; i++)
{
a[i] = 0;
}
return new Vector(a);
}
public static Vector Add(Vector v1, Vector v2)
{
return v1.Add(v2);
}
public static Vector Sub(Vector v1, Vector v2)
{
return v1.Sub(v2);
}
public static double DotProduct(Vector v1, Vector v2)
{
return v1.DotProduct(v2);
}
public static Vector ScalarMultiplication(double s, Vector v)
{
return v.ScalarMultiplication(s);
}
public static Vector ScalarMultiplication(Vector v, double s)
{
return v.ScalarMultiplication(s);
}
public static Vector ScalarProduct(Vector v1, Vector v2)
{
if (v1.Dimension != 3 && v2.Dimension != 3)
{
throw new Exception("Wrong dimension ERROR!");
}
double[] a = new double[3];
a[0] = v1.Components[1] * v2.Components[2] - v1.Components[2] * v2.Components[1];
a[1] = v1.Components[2] * v2.Components[0] - v1.Components[0] * v2.Components[2];
a[2] = v1.Components[0] * v2.Components[1] - v1.Components[1] * v2.Components[0];
return new Vector(a);
}
public static double AngleInRad(Vector v1, Vector v2)
{
return v1.AngleInRad(v2);
}
public static double AngleInDeg(Vector v1, Vector v2)
{
return v1.AngleInDeg(v2);
}
// OPERATOR OVERLOADING
public static Vector operator +(Vector v) => v;
public static Vector operator -(Vector v) => ZeroVector(v.Dimension) - v;
public static Vector operator +(Vector v1, Vector v2) => Add(v1, v2);
public static Vector operator -(Vector v1, Vector v2) => Sub(v1, v2);
public static double operator *(Vector v1, Vector v2) => DotProduct(v1, v2);
public static Vector operator *(Vector v, double s) => ScalarMultiplication(s, v);
public static Vector operator *(double s, Vector v) => ScalarMultiplication(s, v);
}
}