C#-Programmbausteine
Die im vorherigen Artikel in dieser C#-Serie beschriebenen Typen werden mithilfe folgender Bausteine erstellt:
- Elemente, z. B. Eigenschaften, Felder, Methoden und Ereignisse
- Ausdrücke
- Anweisungen
Members
Die Member einer class
sind entweder statische Member oder Instanzmember. Statische Member gehören zu Klassen, Instanzmember gehören zu Objekten (Instanzen von Klassen).
In der folgenden Liste finden Sie einen Überblick über die Memberarten, die eine Klasse enthalten kann.
- Konstanten: Konstante Werte, die der Klasse zugeordnet sind
- Felder: Variablen, die der Klasse zugeordnet sind
- Methoden: Aktionen, die von der Klasse ausgeführt werden
- Properties: Aktionen im Zusammenhang mit dem Lesen und Schreiben von benannten Eigenschaften der Klasse
- Indexer: Aktionen im Zusammenhang mit dem Indizieren von Instanzen der Klasse, z.B. einem Array
- Ereignisse: Benachrichtigungen, die von der Klasse generiert werden können
- Operatoren: Operatoren für Konvertierungen und Ausdrücke, die von der Klasse unterstützt werden
- Konstruktoren: Aktionen, die zum Initialisieren von Instanzen der Klasse oder der Klasse selbst benötigt werden
- Finalizer: Aktionen, die ausgeführt werden, bevor Instanzen der Klasse dauerhaft verworfen werden
- Typen: Geschachtelte Typen, die von der Klasse deklariert werden
Barrierefreiheit
Jeder Member einer Klasse ist mit einem Zugriff verknüpft, der die Regionen des Programmtexts steuert, die auf den Member zugreifen können. Es gibt sechs mögliche Formen des Zugriffs. Die Zugriffsmodifizierer werden im Folgenden zusammengefasst.
public
: Der Zugriff ist nicht eingeschränkt.private
: Der Zugriff ist auf diese Klasse beschränkt.protected
: Der Zugriff ist auf diese Klasse oder von dieser abgeleiteten Klassen beschränkt.internal
: Der Zugriff ist auf die aktuelle Assembly (.exe
oder.dll
) beschränkt.protected internal
: Der Zugriff ist auf diese Klasse, auf Klassen, die von dieser Klasse abgeleitet wurden, oder auf Klassen innerhalb der gleichen Assembly beschränkt.private protected
: Der Zugriff ist auf diese Klasse und auf Klassen in derselben Assembly beschränkt, die von diesem Typ abgeleitet wurden.
Felder
Ein Feld ist eine Variable, die einer Klasse oder einer Instanz einer Klasse zugeordnet ist.
Ein Feld, das mit dem static-Modifizierer deklariert wurde, definiert ein statisches Feld. Ein statisches Feld identifiziert genau einen Speicherort. Unabhängig davon, wie viele Instanzen einer Klasse erstellt werden, gibt es immer nur eine Kopie eines statischen Felds.
Ein Feld, das ohne den static-Modifizierer deklariert wurde, definiert ein Instanzfeld. Jede Instanz einer Klasse enthält eine separate Kopie aller Instanzfelder dieser Klasse.
Im folgenden Beispiel weist jede Instanz der Color
-Klasse eine separate Kopie der Instanzfelder R
, G
und B
auf, aber es gibt nur eine Kopie der statischen Felder Black
, White
, Red
, Green
und Blue
:
public class Color
{
public static readonly Color Black = new(0, 0, 0);
public static readonly Color White = new(255, 255, 255);
public static readonly Color Red = new(255, 0, 0);
public static readonly Color Green = new(0, 255, 0);
public static readonly Color Blue = new(0, 0, 255);
public byte R;
public byte G;
public byte B;
public Color(byte r, byte g, byte b)
{
R = r;
G = g;
B = b;
}
}
Wie im vorherigen Beispiel gezeigt, können schreibgeschützte Felder mit einem readonly
-Modifizierer deklariert werden. Zuweisungen zu einem schreibgeschützten Feld können nur im Rahmen der Deklaration des Felds oder in einem Konstruktor derselben Klasse auftreten.
Methoden
Eine Methode ist ein Member, das eine Berechnung oder eine Aktion implementiert, die durch ein Objekt oder eine Klasse durchgeführt werden kann. Auf statische Methoden wird über die Klasse zugegriffen. Auf Instanzmethoden wird über Instanzen der Klasse zugegriffen.
Methoden verfügen über eine Liste von Parametern, die Werte oder Variablenverweise darstellen, die an die Methode übergeben werden. Methoden besitzen einen Rückgabetyp, der den Typ des Werts festlegt, der von der Methode berechnet und zurückgegeben wird. Der Rückgabetyp einer Methode lautet void
, wenn kein Wert zurückgegeben wird.
Ebenso wie Typen können Methoden einen Satz an Typparametern aufweisen, für den beim Aufruf der Methode Typargumente angegeben werden müssen. Im Gegensatz zu Typen können die Typargumente häufig aus den Argumenten eines Methodenaufrufs abgeleitet werden und müssen nicht explizit angegeben werden.
Die Signatur einer Methode muss innerhalb der Klasse eindeutig sein, in der die Methode deklariert ist. Die Signatur einer Methode besteht aus dem Namen der Methode, der Anzahl von Typparametern und der Anzahl, den Modifizierern und den Typen der zugehörigen Parameter. Die Signatur einer Methode umfasst nicht den Rückgabetyp.
Wenn es sich bei einem Methodenkörper um einen einzelnen Ausdruck handelt, kann die Methode mithilfe eines kompakten Ausdrucksformat definiert werden. Dies wird im folgenden Beispiel veranschaulicht:
public override string ToString() => "This is an object";
Parameter
Parameter werden dazu verwendet, Werte oder Variablenverweise an Methoden zu übergeben. Die Parameter einer Methode erhalten ihre tatsächlichen Werte über Argumente, die angegeben werden, wenn die Methode aufgerufen wird. Es gibt vier Arten von Parametern: Wertparameter, Verweisparameter, Ausgabeparameter und Parameterarrays.
Ein Wertparameter wird zum Übergeben von Eingabeargumenten verwendet. Ein Wertparameter entspricht einer lokalen Variablen, die ihren Anfangswert von dem Argument erhält, das für den Parameter übergeben wurde. Änderungen an einem Wertparameter wirken sich nicht auf das Argument aus, das für den Parameter übergeben wurde.
Wertparameter können optional sein, indem ein Standardwert festgelegt wird, damit die zugehörigen Argumente weggelassen werden können.
Ein Verweisparameter wird zum Übergeben von Argumenten als Verweis verwendet. Das für einen Verweisparameter übergebene Argument muss eine Variable mit einem definitiven Wert sein. Währen der Ausführung der Methode stellt der Verweisparameter denselben Speicherort wie die Argumentvariable dar. Ein Verweisparameter wird mit dem ref
-Modifizierer deklariert. Das folgende Beispiel veranschaulicht die Verwendung des ref
-Parameters.
static void Swap(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
public static void SwapExample()
{
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine($"{i} {j}"); // "2 1"
}
Ein Ausgabeparameter wird zum Übergeben von Argumenten als Verweis verwendet. Er ist einem Verweisparameter ähnlich, außer dass er nicht erfordert, dass Sie explizit dem vom Aufrufer bereitgestellten Argument einen Wert zuweisen. Ein Ausgabeparameter wird mit dem out
-Modifizierer deklariert. Das folgende Beispiel veranschaulicht die Verwendung des out
-Parameters.
static void Divide(int x, int y, out int quotient, out int remainder)
{
quotient = x / y;
remainder = x % y;
}
public static void OutUsage()
{
Divide(10, 3, out int quo, out int rem);
Console.WriteLine($"{quo} {rem}"); // "3 1"
}
Ein Parameterarray ermöglicht es, eine variable Anzahl von Argumenten an eine Methode zu übergeben. Ein Parameterarray wird mit dem params
-Modifizierer deklariert. Nur der letzte Parameter einer Methode kann ein Parameterarray sein, und es muss sich um ein eindimensionales Parameterarray handeln. Die Methoden Write
und WriteLine
der Klasse System.Console sind gute Beispiele für die Nutzung eines Parameterarrays. Sie werden folgendermaßen deklariert.
public class Console
{
public static void Write(string fmt, params object[] args) { }
public static void WriteLine(string fmt, params object[] args) { }
// ...
}
Innerhalb einer Methode mit einem Parameterarray verhält sich das Parameterarray wie ein regulärer Parameter des Arraytyps. Beim Aufruf einer Methode mit einem Parameterarray ist es jedoch möglich, entweder ein einzelnes Argument des Parameterarraytyps oder eine beliebige Anzahl von Argumenten des Elementtyps des Parameterarrays zu übergeben. Im letzteren Fall wird automatisch eine Arrayinstanz erstellt und mit den vorgegebenen Argumenten initialisiert. Dieses Beispiel:
int x, y, z;
x = 3;
y = 4;
z = 5;
Console.WriteLine("x={0} y={1} z={2}", x, y, z);
...entspricht dem folgenden Code:
int x = 3, y = 4, z = 5;
string s = "x={0} y={1} z={2}";
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);
Methodenkörper und lokale Variablen
Der Methodenkörper gibt die Anweisungen an, die beim Aufruf der Methode ausgeführt werden sollen.
Ein Methodenkörper kann Variablen deklarieren, die für den Aufruf der Methode spezifisch sind. Diese Variable werden lokale Variablen genannt. Die Deklaration einer lokalen Variable gibt einen Typnamen, einen Variablennamen und eventuell einen Anfangswert an. Im folgenden Beispiel wird eine lokale Variable i
mit einem Anfangswert von 0 und einer lokalen Variablen j
ohne Anfangswert deklariert.
class Squares
{
public static void WriteSquares()
{
int i = 0;
int j;
while (i < 10)
{
j = i * i;
Console.WriteLine($"{i} x {i} = {j}");
i++;
}
}
}
In C# muss eine lokale Variable definitiv zugewiesen sein, bevor ihr Wert abgerufen werden kann. Wenn die vorherige Deklaration von i
beispielsweise keinen Anfangswert enthält, würde der Compiler bei der späteren Verwendung von i
einen Fehler melden, weil i
zu diesen Zeitpunkten im Programm nicht definitiv zugewiesen ist.
Eine Methode kann return
-Anweisungen verwenden, um die Steuerung an den zugehörigen Aufrufer zurückzugeben. In einer Methode, die void
zurückgibt, können return
-Anweisungen keinen Ausdruck angeben. In einer Methode, die nicht „void“ zurückgibt, müssenreturn
-Anweisungen einen Ausdruck enthalten, der den Rückgabewert berechnet.
Statische Methoden und Instanzmethoden
Eine Methode, die mit einem static
-Modifizierer deklariert wird, ist eine statische Methode. Eine statische Methode führt keine Vorgänge für eine spezifische Instanz aus und kann nur direkt auf statische Member zugreifen.
Eine Methode, die ohne einen static
-Modifizierer deklariert wird, ist eine Instanzmethode. Eine Instanzmethode führt Vorgänge für eine spezifische Instanz aus und kann sowohl auf statische Member als auch auf Instanzmember zugreifen. Auf die Instanz, für die eine Instanzmethode aufgerufen wurde, kann explizit als this
zugegriffen werden. Es ist ein Fehler, in einer statischen Methode auf this
zu verweisen.
Die folgende Entity
-Klasse umfasst sowohl statische Member als auch Instanzmember.
class Entity
{
static int s_nextSerialNo;
int _serialNo;
public Entity()
{
_serialNo = s_nextSerialNo++;
}
public int GetSerialNo()
{
return _serialNo;
}
public static int GetNextSerialNo()
{
return s_nextSerialNo;
}
public static void SetNextSerialNo(int value)
{
s_nextSerialNo = value;
}
}
Jede Entity
-Instanz enthält eine Seriennummer (und vermutlich weitere Informationen, die hier nicht angezeigt werden). Der Entity
-Konstruktor (der einer Instanzmethode ähnelt) initialisiert die neue Instanz mit der nächsten verfügbaren Seriennummer. Da der Konstruktor ein Instanzmember ist, kann er sowohl auf das _serialNo
-Instanzfeld als auch auf das statische s_nextSerialNo
-Feld zugreifen.
Die statischen Methoden GetNextSerialNo
und SetNextSerialNo
können auf das statische Feld s_nextSerialNo
zugreifen, aber es wäre ein Fehler, über diese Methoden direkt auf das Instanzfeld _serialNo
zuzugreifen.
Im folgenden Beispiel wird die Verwendung der Entity
-Klasse veranschaulicht.
Entity.SetNextSerialNo(1000);
Entity e1 = new();
Entity e2 = new();
Console.WriteLine(e1.GetSerialNo()); // Outputs "1000"
Console.WriteLine(e2.GetSerialNo()); // Outputs "1001"
Console.WriteLine(Entity.GetNextSerialNo()); // Outputs "1002"
Die statischen Methoden SetNextSerialNo
und GetNextSerialNo
werden für die Klasse aufgerufen, während die GetSerialNo
-Instanzmethode für Instanzen der Klasse aufgerufen wird.
Virtuelle, überschriebene und abstrakte Methoden
Sie verwenden virtuelle, Außerkraftsetzungs- und abstrakte Methoden, um das Verhalten für eine Hierarchie von Klassentypen zu definieren. Da eine Klasse von einer Basisklasse abgeleitet werden kann, müssen diese abgeleiteten Klassen möglicherweise das in der Basisklasse implementierte Verhalten ändern. Eine virtuelle Methode ist eine Methode, die in einer Basisklasse deklariert und implementiert wird, wobei jede abgeleitete Klasse eine spezifischere Implementierung bereitstellen kann. Eine Außerkraftsetzungsmethode ist eine in einer abgeleiteten Klasse implementierte Methode, die das Verhalten der Implementierung der Basisklasse ändert. Eine abstrakte Methode ist eine Methode, die in einer Basisklasse deklariert ist, die in allen abgeleiteten Klassen überschrieben werden muss. Tatsächlich definieren abstrakte Methoden keine Implementierung in der Basisklasse.
Methodenaufrufe von Instanzmethoden können entweder in Basisklassenimplementierungen oder Implementierungen abgeleiteter Klassen aufgelöst werden. Der Typ einer Variablen bestimmt ihren Kompilierzeittyp. Der Kompilierzeittyp ist der Typ, den der Compiler verwendet, um seine Member zu bestimmen. Eine Variable kann jedoch einer Instanz eines beliebigen Typs zugewiesen werden, der von seinem Kompilierzeittyp abgeleitet ist. Der Laufzeittyp ist der Typ der tatsächlichen Instanz, auf die von dieser Variablen verwiesen wird.
Beim Aufruf einer virtuellen Methode bestimmt der Laufzeittyp der Instanz, für die der Aufruf erfolgt, die tatsächlich aufzurufende Methodenimplementierung. Beim Aufruf einer nicht virtuellen Methode ist der Kompilierzeittyp der bestimmende Faktor.
Eine virtuelle Methode kann in einer abgeleiteten Klasse überschrieben werden. Wenn eine Instanzmethodendeklaration einen override-Modifizierer enthält, überschreibt die Methode eine geerbte virtuelle Methode mit derselben Signatur. Mit der virtuellen Methodendeklaration wird eine neue Methode eingeführt. Eine Deklaration einer Überschreibungsmethode spezialisiert eine vorhandene geerbte virtuelle Methode, indem eine neue Implementierung dieser Methode bereitgestellt wird.
Eine abstrakte Methode ist eine virtuelle Methode ohne Implementierung. Eine abstrakte Methode wird mit dem Modifizierer abstract
deklariert und ist nur in einer abstrakten Klasse zulässig. Eine abstrakte Methode muss in jeder nicht abstrakten abgeleiteten Klasse überschrieben werden.
Im folgenden Beispiel wird die abstrakte Klasse Expression
deklariert, die einen Ausdrucksbaumstrukturknoten sowie drei abgeleitete Klassen repräsentiert: Constant
, VariableReference
und Operation
. Diese implementieren Ausdrucksbaumstrukturknoten für Konstanten, variable Verweise und arithmetische Operationen. (Dieses Beispiel ähnelt den Ausdrucksbaumstrukturtypen, ist aber nicht mit diesen verwandt.)
public abstract class Expression
{
public abstract double Evaluate(Dictionary<string, object> vars);
}
public class Constant : Expression
{
double _value;
public Constant(double value)
{
_value = value;
}
public override double Evaluate(Dictionary<string, object> vars)
{
return _value;
}
}
public class VariableReference : Expression
{
string _name;
public VariableReference(string name)
{
_name = name;
}
public override double Evaluate(Dictionary<string, object> vars)
{
object value = vars[_name] ?? throw new Exception($"Unknown variable: {_name}");
return Convert.ToDouble(value);
}
}
public class Operation : Expression
{
Expression _left;
char _op;
Expression _right;
public Operation(Expression left, char op, Expression right)
{
_left = left;
_op = op;
_right = right;
}
public override double Evaluate(Dictionary<string, object> vars)
{
double x = _left.Evaluate(vars);
double y = _right.Evaluate(vars);
switch (_op)
{
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return x / y;
default: throw new Exception("Unknown operator");
}
}
}
Die vorherigen vier Klassen können zum Modellieren arithmetischer Ausdrücke verwendet werden. Beispielsweise kann mithilfe von Instanzen dieser Klassen der Ausdruck x + 3
folgendermaßen dargestellt werden.
Expression e = new Operation(
new VariableReference("x"),
'+',
new Constant(3));
Die Evaluate
-Methode einer Expression
-Instanz wird aufgerufen, um den vorgegebenen Ausdruck auszuwerten und einen double
-Wert zu generieren. Die Methode verwendet ein Dictionary
-Argument, das Variablennamen (als Schlüssel der Einträge) und Werte (als Werte der Einträge) enthält. Da Evaluate
eine abstrakte Methode ist, müssen nicht-abstrakte Klassen, die von Expression
abgeleitet sind, Evaluate
außer Kraft setzen.
Eine Implementierung von Constant
für Evaluate
gibt lediglich die gespeicherte Konstante zurück. Eine Implementierung von VariableReference
sucht im Wörterbuch nach dem Variablennamen und gibt den Ergebniswert zurück. Eine Implementierung von Operation
wertet zunächst (durch einen rekursiven Aufruf der zugehörigen Evaluate
-Methoden) den linken und rechten Operanden aus und führt dann die vorgegebene arithmetische Operation aus.
Das folgende Programm verwendet die Expression
-Klassen zum Auswerten des Ausdrucks x * (y + 2)
für verschiedene Werte von x
und y
.
Expression e = new Operation(
new VariableReference("x"),
'*',
new Operation(
new VariableReference("y"),
'+',
new Constant(2)
)
);
Dictionary<string, object> vars = new();
vars["x"] = 3;
vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); // "21"
vars["x"] = 1.5;
vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); // "16.5"
Methodenüberladung
Das Überladen von Methoden macht es möglich, dass mehrere Methoden in derselben Klasse denselben Namen verwenden, solange sie eindeutige Signaturen aufweisen. Beim Kompilieren des Aufrufs einer überladenen Methode verwendet der Compiler die Überladungsauflösung, um die spezifische Methode zu ermitteln, die aufgerufen werden soll. Die Überladungsauflösung ermittelt die Methode, die den Argumenten am besten entspricht. Wenn keine optimale Übereinstimmung gefunden wird, wird ein Fehler gemeldet. Das folgende Beispiel zeigt die Verwendung der Überladungsauflösung. Der Kommentar für jeden Aufruf in der UsageExample
-Methode zeigt, welche Methode aufgerufen wird.
class OverloadingExample
{
static void F() => Console.WriteLine("F()");
static void F(object x) => Console.WriteLine("F(object)");
static void F(int x) => Console.WriteLine("F(int)");
static void F(double x) => Console.WriteLine("F(double)");
static void F<T>(T x) => Console.WriteLine($"F<T>(T), T is {typeof(T)}");
static void F(double x, double y) => Console.WriteLine("F(double, double)");
public static void UsageExample()
{
F(); // Invokes F()
F(1); // Invokes F(int)
F(1.0); // Invokes F(double)
F("abc"); // Invokes F<T>(T), T is System.String
F((double)1); // Invokes F(double)
F((object)1); // Invokes F(object)
F<int>(1); // Invokes F<T>(T), T is System.Int32
F(1, 1); // Invokes F(double, double)
}
}
Wie im Beispiel gezeigt, kann eine bestimmte Methode immer ausgewählt werden, indem die Argumente explizit in die exakten Parametertypen und Typargumente umgewandelt werden.
Andere Funktionsmember
Member, die ausführbaren Code enthalten, werden als Funktionsmember einer Klasse bezeichnet. Im vorherigen Abschnitt wurden Methoden beschrieben, die die Haupttypen von Funktionsmembern sind. In diesem Abschnitt werden die weiteren Funktionsmember behandelt, die C# unterstützt: Konstruktoren, Eigenschaften, Indexer, Ereignisse, Operatoren und Finalizer.
Im folgenden Beispiel wird eine generische Klasse namens MyList<T>
gezeigt, die eine wachsende Liste von Objekten implementiert. Die Klasse enthält verschiedene Beispiele der gängigsten Arten von Funktionsmembern.
public class MyList<T>
{
const int DefaultCapacity = 4;
T[] _items;
int _count;
public MyList(int capacity = DefaultCapacity)
{
_items = new T[capacity];
}
public int Count => _count;
public int Capacity
{
get => _items.Length;
set
{
if (value < _count) value = _count;
if (value != _items.Length)
{
T[] newItems = new T[value];
Array.Copy(_items, 0, newItems, 0, _count);
_items = newItems;
}
}
}
public T this[int index]
{
get => _items[index];
set
{
if (!object.Equals(_items[index], value)) {
_items[index] = value;
OnChanged();
}
}
}
public void Add(T item)
{
if (_count == Capacity) Capacity = _count * 2;
_items[_count] = item;
_count++;
OnChanged();
}
protected virtual void OnChanged() =>
Changed?.Invoke(this, EventArgs.Empty);
public override bool Equals(object other) =>
Equals(this, other as MyList<T>);
static bool Equals(MyList<T> a, MyList<T> b)
{
if (Object.ReferenceEquals(a, null)) return Object.ReferenceEquals(b, null);
if (Object.ReferenceEquals(b, null) || a._count != b._count)
return false;
for (int i = 0; i < a._count; i++)
{
if (!object.Equals(a._items[i], b._items[i]))
{
return false;
}
}
return true;
}
public event EventHandler Changed;
public static bool operator ==(MyList<T> a, MyList<T> b) =>
Equals(a, b);
public static bool operator !=(MyList<T> a, MyList<T> b) =>
!Equals(a, b);
}
Konstruktoren
C# unterstützt sowohl Instanzkonstruktoren als auch statische Konstruktoren. Ein Instanzkonstruktor ist ein Member, der die erforderlichen Aktionen zum Initialisieren einer Instanz einer Klasse implementiert. Ein statischer Konstruktor ist ein Member, der die zum Initialisieren einer Klasse erforderlichen Aktionen implementiert, um die Klasse beim ersten Laden selbst zu initialisieren.
Ein Konstruktor wird wie eine Methode ohne Rückgabetyp und mit demselben Namen wie die enthaltende Klasse deklariert. Wenn eine Konstruktordeklaration einen static
-Modifizierer enthält, deklariert diese einen statischen Konstruktor. Andernfalls wird ein Instanzkonstruktor deklariert.
Instanzkonstruktoren können überladen werden und optionale Parameter verwenden. Die MyList<T>
-Klasse deklariert z.B. einen Instanzkonstruktor mit einem einzelnen optionalen int
-Parameter. Instanzkonstruktoren werden über den new
-Operator aufgerufen. Die folgenden Anweisungen weisen zwei Instanzen von MyList<string>
unter Verwendung des Konstruktors der MyList
-Klasse zu, mit dem optionalen Argument und ohne das optionale Argument.
MyList<string> list1 = new();
MyList<string> list2 = new(10);
Im Gegensatz zu anderen Membern werden Instanzkonstruktoren nicht geerbt. Eine Klasse weist keine anderen Instanzkonstruktoren auf als diejenigen, die tatsächlich in der Klasse deklariert wurden. Wenn kein Instanzkonstruktor für eine Klasse angegeben ist, wird automatisch ein leerer Instanzkonstruktor ohne Parameter bereitgestellt.
Eigenschaften
Eigenschaften sind eine natürliche Erweiterung der Felder. Beide sind benannte Member mit zugeordneten Typen, und für den Zugriff auf Felder und Eigenschaften wird dieselbe Syntax verwendet. Im Gegensatz zu Feldern geben Eigenschaften jedoch keine Speicherorte an. Stattdessen verfügen Eigenschaften über Zugriffsmethoden, die die ausgeführten Anweisungen angeben, wenn ihre Werte gelesen oder geschrieben werden. Ein get-Accessor liest den Wert. Ein set-Accessor schreibt den Wert.
Eine Eigenschaft wird wie ein Feld deklariert, abgesehen davon, dass die Deklaration nicht auf ein Semikolon, sondern auf einen get- oder set-Accessor endet, der von den Trennzeichen {
und }
umschlossen wird. Eine Eigenschaft, die eine Get-Zugriffsmethode und eine Set-Zugriffsmethode umfasst, ist eine Eigenschaft mit Lese-/Schreibzugriff. Eine Eigenschaft, die nur eine Get-Zugriffsmethode umfasst, ist eine schreibgeschützte Eigenschaft. Eine Eigenschaft, die nur eine Set-Zugriffsmethode umfasst, ist eine lesegeschützte Eigenschaft.
Ein get-Accessor entspricht einer Methode ohne Parameter mit einem Rückgabewert des Eigenschaftstyps. Ein set-Accessor entspricht einer Methode mit einem einzigen Parameter namens „value“ ohne Rückgabetyp. Die get-Zugriffsmethode berechnet den Wert der Eigenschaft. Die set-Zugriffsmethode stellt einen neuen Wert für die Eigenschaft bereit. Wenn die Eigenschaft das Ziel einer Zuweisung oder der Operand von ++
oder --
ist, wird die set-Zugriffsmethode aufgerufen. In anderen Fällen, in denen die Eigenschaft referenziert wird, wird die get-Zugriffsmethode aufgerufen.
Die MyList<T>
-Klasse deklariert die beiden Eigenschaften „Count
“ und „Capacity
“, von denen die eine schreibgeschützt ist und die andere Lese- und Schreibzugriff besitzt. Im folgenden Beispielcode wird die Verwendung dieser Eigenschaften veranschaulicht:
MyList<string> names = new();
names.Capacity = 100; // Invokes set accessor
int i = names.Count; // Invokes get accessor
int j = names.Capacity; // Invokes get accessor
Ähnlich wie bei Feldern und Methoden unterstützt C# sowohl Instanzeigenschaften als auch statische Eigenschaften. Statische Eigenschaften werden mit dem static-Modifizierer, Instanzeigenschaften werden ohne static-Modifizierer deklariert.
Die Accessors einer Eigenschaft können virtuell sein. Wenn eine Eigenschaftendeklaration einen virtual
-, abstract
- oder override
-Modifizierer enthält, wird dieser auf den Accessor der Eigenschaft angewendet.
Indexer
Ein Indexer ist ein Member, mit dem Objekte wie ein Array indiziert werden können. Ein Indexer wird wie eine Eigenschaft deklariert, abgesehen davon, dass der Name des Members this
ist, gefolgt von einer Parameterliste, die zwischen die Trennzeichen [
und ]
geschrieben wird. Die Parameter stehen im Accessor des Indexers zur Verfügung. Ähnlich wie Eigenschaften können Indexer Lese-/Schreibzugriff besitzen, schreibgeschützt und lesegeschützt sein und virtuelle Accessors verwenden.
Die MyList<T>
-Klasse deklariert einen einzigen Indexer mit Lese-/Schreibzugriff, der einen int
-Parameter akzeptiert. Der Indexer ermöglicht es, Instanzen von MyList<T>
mit int
-Werten zu indizieren. Beispiel:
MyList<string> names = new();
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++)
{
string s = names[i];
names[i] = s.ToUpper();
}
Indexer können überladen werden. Eine Klasse kann mehrere Indexer deklarieren, solange sich die Anzahl oder die Typen ihrer Parameter unterscheiden.
Events
Ein Ereignis ist ein Member, der es einer Klasse oder einem Objekt ermöglicht, Benachrichtigungen bereitzustellen. Ein Ereignis wird wie ein Feld deklariert, abgesehen davon, dass es ein event
-Schlüsselwort enthält und einen Delegattyp aufweisen muss.
Innerhalb einer Klasse, die einen Ereignismember deklariert, verhält sich das Ereignis wie ein Feld des Delegattyps (vorausgesetzt, das Ereignis ist nicht abstrakt und deklariert keine Zugriffsmethoden). Das Feld speichert einen Verweis auf einen Delegaten, der die Ereignishandler repräsentiert, die dem Ereignis hinzugefügt wurden. Wenn keine Ereignishandler vorhanden sind, ist das Feld null
.
Die MyList<T>
-Klasse deklariert ein einzelnes Ereignismember mit dem Namen Changed
, das angibt, dass der Liste ein neues Element hinzugefügt wurde oder ein Listenelement mit der Zugriffsmethode für eine Indexergruppe geändert wurde. Das Changed-Ereignis wird durch die virtuelle Methode OnChanged
ausgelöst, die zunächst prüft, ob das Ereignis null
ist (d.h. nicht über Handler verfügt). Das Auslösen eines Ereignisses entspricht exakt dem Aufrufen des Delegats, der durch das Ereignis repräsentiert wird. Es gibt keine speziellen Sprachkonstrukte zum Auslösen von Ereignissen.
Clients reagieren über Ereignishandler auf Ereignisse. Ereignishandler werden unter Verwendung des +=
-Operators angefügt und mit dem -=
-Operator entfernt. Im folgenden Beispiel wird dem Changed
-Ereignis von MyList<string>
ein Ereignishandler hinzugefügt.
class EventExample
{
static int s_changeCount;
static void ListChanged(object sender, EventArgs e)
{
s_changeCount++;
}
public static void Usage()
{
var names = new MyList<string>();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(s_changeCount); // "3"
}
}
In fortgeschrittenen Szenarios, in denen die zugrunde liegende Speicherung eines Ereignisses gesteuert werden soll, kann eine Ereignisdeklaration explizit die Zugriffsmethoden add
und remove
bereitstellen, die der Zugriffsmethode set
einer Eigenschaft ähneln.
Operatoren
Ein Operator ist ein Member, der die Bedeutung der Anwendung eines bestimmten Ausdrucksoperators auf Instanzen einer Klasse definiert. Es können drei Arten von Operatoren definiert werden: unäre Operatoren, binäre Operatoren und Konvertierungsoperatoren. Alle Operatoren müssen als public
und static
deklariert werden.
Die MyList<T>
-Klasse deklariert zwei Operatoren: operator ==
und operator !=
. Diese überschriebenen Operatoren verleihen Ausdrücken eine neue Bedeutung, die diese Operatoren auf MyList
-Instanzen anwenden. Insbesondere die Operatoren definieren die Gleichheit für zwei Instanzen von MyList<T>
, indem alle enthaltenen Objekte mithilfe ihrer Equals
-Methoden verglichen werden. Im folgenden Beispiel wird der ==
-Operator verwendet, um zwei Instanzen von MyList<int>
zu vergleichen.
MyList<int> a = new();
a.Add(1);
a.Add(2);
MyList<int> b = new();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b); // Outputs "True"
b.Add(3);
Console.WriteLine(a == b); // Outputs "False"
Die erste Methode Console.WriteLine
gibt True
aus, weil die zwei Listen dieselbe Anzahl von Objekten mit denselben Werten in derselben Reihenfolge enthalten. Wenn MyList<T>
nicht operator ==
definieren würde, würde die Ausgabe der ersten Console.WriteLine
-Methode False
lauten, weil a
und b
auf unterschiedliche MyList<int>
-Instanzen verweisen.
Finalizer
Ein Finalizer ist ein Member, der die erforderlichen Aktionen zum Bereinigen einer Instanz einer Klasse implementiert. In der Regel ist ein Finalizer erforderlich, um nicht verwaltete Ressourcen freizugeben. Finalizer können weder Parameter noch Zugriffsmodifizierer aufweisen und können nicht explizit aufgerufen werden. Der Finalizer für eine Instanz wird bei der Garbagecollection automatisch aufgerufen. Weitere Informationen finden Sie im Artikel zu Finalizern.
Der Garbage Collector kann weitestgehend selbst über den Zeitpunkt der Objektbereinigung und die Ausführung der Finalizer entscheiden. Insbesondere der Zeitpunkt für den Aufruf der Finalizer ist nicht festgelegt, und Finalizer können für beliebige Threads ausgeführt werden. Aus diesen und weiteren Gründen sollten Klassen Finalizer nur dann implementieren, wenn keine andere Lösung möglich ist.
Die using
-Anweisung bietet einen besseren Ansatz für die Objektzerstörung.
Ausdrücke
Ausdrücke bestehen aus Operanden und Operatoren. Die Operatoren eines Ausdrucks geben an, welche Operationen auf die Operanden angewendet werden. Beispiele für Operatoren sind +
, -
, *
, /
und new
. Beispiele für Operanden sind Literale, Felder, lokale Variablen und Ausdrücke.
Wenn ein Ausdruck mehrere Operatoren enthält, steuert die Rangfolge der Operatoren die Reihenfolge, in der die einzelnen Operatoren ausgewertet werden. Der Ausdruck x + y * z
wird z.B. als x + (y * z)
ausgewertet, da der *
-Operator Vorrang vor dem +
-Operator hat.
Tritt ein Operand zwischen zwei Operatoren mit gleicher Rangfolge auf, steuert die Assoziativität der Operatoren die Reihenfolge, in der die Vorgänge ausgeführt werden:
- Mit Ausnahme der Zuweisungs- und NULL-Sammeloperatoren sind alle binären Operatoren linksassoziativ, was bedeutet, dass Vorgänge von links nach rechts ausgeführt werden.
x + y + z
wird beispielsweise als(x + y) + z
ausgewertet. - Die Zuweisungsoperatoren, die NULL-Sammeloperatoren
??
und??=
und der bedingte Operator?:
sind rechtsassoziativ, d.h., die Operationen werden von rechts nach links ausgeführt.x = y = z
wird beispielsweise alsx = (y = z)
ausgewertet.
Rangfolge und Assoziativität können mit Klammern gesteuert werden. In x + y * z
wird beispielsweise zuerst y
mit z
multipliziert und dann das Ergebnis zu x
addiert, aber in (x + y) * z
werden zunächst x
und y
addiert, und dann wird das Ergebnis mit z
multipliziert.
Die meisten Operatoren können überladen werden. Das Überladen von Operatoren ermöglicht die Angabe benutzerdefinierter Operatorimplementierungen für Vorgänge, in denen einer der Operanden oder beide einer benutzerdefinierten Klasse oder einem benutzerdefinierten Strukturtyp angehören.
C# bietet Operatoren für arithmetische, logische, bitweise und Verschiebungsvorgänge sowie Vergleiche von Gleichheit und Reihenfolge.
Die vollständige Liste der nach Rangfolgenebene sortierten C#-Operatoren finden Sie unter C#-Operatoren.
Anweisungen
Die Aktionen eines Programms werden mit Anweisungen ausgedrückt. C# unterstützt verschiedene Arten von Anweisungen, von denen ein Teil als eingebettete Anweisungen definiert ist.
- Ein Block ermöglicht, mehrere Anweisungen in Kontexten zu schreiben, in denen eine einzelne Anweisung zulässig ist. Ein Block besteht aus einer Liste von Anweisungen, die zwischen den Trennzeichen
{
und}
geschrieben sind. - Deklarationsanweisungen werden verwendet, um lokale Variablen und Konstanten deklarieren.
- Ausdrucksanweisungen werden zum Auswerten von Ausdrücken verwendet. Ausdrücke, die als Anweisungen verwendet werden können, enthalten Methodenaufrufe, Objektzuordnungen mit dem
new
-Operator, Zuweisungen mit=
und den Verbundzuweisungsoperatoren, Inkrementier- und Dekrementiervorgänge unter Verwendung des++
- und--
-Operators undawait
-Ausdrücke. - Auswahlanweisungen werden verwendet, um eine Anzahl von möglichen Anweisungen für die Ausführung anhand des Werts eines Ausdrucks auszuwählen. Diese Gruppe enthält die Anweisungen
if
undswitch
. - Iterationsanweisungen werden verwendet, um eine eingebettete Anweisung wiederholt auszuführen. Diese Gruppe enthält die Anweisungen
while
,do
,for
undforeach
. - Sprunganweisungen werden verwendet, um die Steuerung zu übertragen. Diese Gruppe enthält die Anweisungen
break
,continue
,goto
,throw
,return
undyield
. - Mit der
try
...catch
-Anweisung werden Ausnahmen abgefangen, die während der Ausführung eines Blocks auftreten, und mit dertry
...finally
-Anweisung wird Finalisierungscode angegeben, der immer ausgeführt wird, unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht. - Mit den Anweisungen
checked
undunchecked
wird der Überlaufüberprüfungs-Kontext für arithmetische Operationen für Ganzzahltypen und Konvertierungen gesteuert. - Die
lock
-Anweisung wird verwendet, um die Sperre für gegenseitigen Ausschluss für ein bestimmtes Objekt abzurufen, eine Anweisung auszuführen und die Sperre aufzuheben. - Die
using
-Anweisung wird verwendet, um eine Ressource abzurufen, eine Anweisung auszuführen und dann diese Ressource zu verwerfen.
In der folgenden Liste werden die Arten von Anweisungen aufgeführt, die verwendet werden können:
- Deklaration lokaler Variablen
- Deklaration lokaler Konstanten
- Ausdrucksanweisung
if
-Anweisungswitch
-Anweisungwhile
-Anweisungdo
-Anweisungfor
-Anweisungforeach
-Anweisungbreak
-Anweisungcontinue
-Anweisunggoto
-Anweisungreturn
-Anweisungyield
-Anweisungthrow
-Anweisungen undtry
-Anweisungenchecked
- undunchecked
-Anweisungenlock
-Anweisungusing
-Anweisung