C# und Übergabemechanismen: call by value vs. call by reference
Motivation
Wissen Sie wie call by value und call by reference in C# funktioniert? Was…? Sie sind ein alter C++ Programmierer und in C# ist doch alles genau so? Hier ein Motivationsbeispiel:
C#-Code: |
using System;
class Auto
{
public Auto(int AnzahlReifen)
{
m_AnzahlReifen = AnzahlReifen;
}
public int m_AnzahlReifen;
}
public class MyApp
{
public static void Main()
{
Auto MeinAuto = new Auto(4);
foo(MeinAuto);
System.Console.WriteLine(MeinAuto.m_AnzahlReifen);
}
static void foo(Auto irgendeinAuto)
{
irgendeinAuto = new Auto(6);
}
}
|
Wenn sie jetzt verwundert vor dem Bildschirm hocken und sich fragen warum obiges Programm die Zahl 4 ausgibt und nicht die Zahl 6, dann sind Sie ein Leser meine Zielgruppe. Falls nicht, werden sie im folgendem auch nichts neues mehr erfahren.
Alles call by value
Um es kurz zu machen: Parameterübergabe in C# erfolgt implizit immer in Form von call by value.
Jetzt fragen Sie sich sicherlich warum folgendes geht und warum das nicht call by reference ist:
C#-Code: |
using System;
class Auto
{
public Auto(int AnzahlReifen)
{
m_AnzahlReifen = AnzahlReifen;
}
public int m_AnzahlReifen;
}
public class MyMain
{
public static void Main()
{
Auto MeinAuto = new Auto(4);
foo(MeinAuto);
System.Console.WriteLine(MeinAuto.m_AnzahlReifen);
}
static void foo(Auto irgendeinAuto)
{
irgendeinAuto.m_AnzahlReifen = 6;
}
}
|
Die Erklärung ist ganz einfach. Übergibt man eine Referenz an eine Methode, so wird ihr Inhalt kopiert. Was ist der Inhalt eine Referenz? Der Inhalt einer Referenz ist nur ein Verweis, der angibt, wo sich das zugehörige Objekt befindet. Wenn man also eine Referenz an eine Methode übergibt wird eine Kopie der Referenz an die Funktion übergeben.
C#-Code: |
Auto A = new Auto();
doSomething(A);
void doSomething(Auto B)
{
}
|
Bei dem Aufruf der Methode doSomething wird eine zweite Referenz angelegt, die auf das Objekt Auto verweist. Verändert die Referenz B das Objekt so wirkt sich dies auf das Auto Objekt aus.
Man kann in der Methode doSomething das Auto Objekt verändern, aber nicht die Referenz A.
Sehen Sie sich jetzt noch einmal das Motivationsbeispiel an und stellen Sie fest warum 4 und nicht 6 ausgegeben wird.
Man kann in der Methode doSomething nicht die Referenz A auf ein anderes Objekt zeigen lassen, da man nur mit einer Kopie arbeitet.
C#-Code: |
Auto A = new Auto();
doSomething(A);
void doSomething(Auto B)
{
B = new Aute();
}
|
Es bleibt festzuhalten: C# arbeitet mit call-by-value, auch wenn man Referenzen übergibt. Bei Java ist es genau so.
Call by refernece
In C# ist auch call by reference bei der Paramterübergabe möglich. Dabei wird vor jedem Parameter der per call by reference übergeben werden soll das Schlüsselwort ref gestellt:
C#-Code: |
using System;
class Auto
{
public Auto(int AnzahlReifen)
{
m_AnzahlReifen = AnzahlReifen;
}
public int m_AnzahlReifen;
}
public class MyMain
{
public static void Main()
{
Auto MeinAuto = new Auto(4);
foo(ref MeinAuto);
System.Console.WriteLine(MeinAuto.m_AnzahlReifen);
}
static void foo(ref Auto irgendeinAuto)
{
irgendeinAuto = new Auto(6);
}
}
|
Das Schlüsselwort out
Häufig besitzen Methoden Ergebnisparameter. Ergebnisparameter sind Parameter, die nur auf der linken Seite einer Anweisung auftreten. Ihr Wert vor der Ausführung der Methode ist also bedeutungslos. Beispiel:
C#-Code: |
using System;
class MyClass
{
static void Main(string[] args)
{
int ergebnis = 0;
add(3, 2, ref ergebnis);
System.Console.WriteLine(ergebnis);
}
static void add(int x, int y, ref int ergebnisparameter)
{
ergebnisparameter = x + y;
}
}
|
Der Wert von ergebnis ist bedeutungslos, da dieser sowieso mit dem Wert 5 (x+y) überschrieben wird. Die Initialisierung der Variablen ergebnis mit einem bestimmten Wert (z. B. 0) ist unsinnig, da das Ergebnis von x und y abhängig ist. Lässt man die Initialisierung weg, bekommt man eine Fehlermeldung der Art „Verwendung einer nicht zugewiesener lokalen Variablen“:
C#-Code: |
using System;
class MyClass
{
static void Main(string[] args)
{
int ergebnis;
add(3, 2, ref ergebnis);
System.Console.WriteLine(ergebnis);
}
static void add(int x, int y, ref int ergebnisparameter)
{
ergebnisparameter = x + y;
}
}
|
Abhilfe schafft hier das Schlüsselwert out, welches einen Parameter als Ergebnisparameter kennzeichnet. Parameter, die mit out gekennzeichnet sind, müssen nicht initialisiert werden:
C#-Code: |
using System;
class MyClass
{
static void Main(string[] args)
{
int ergebnis;
add(3, 2, out ergebnis);
System.Console.WriteLine(ergebnis);
System.Console.ReadLine();
}
static void add(int x, int y, out int ergebnisparameter)
{
ergebnisparameter = x + y;
}
}
|
Grundsätzlich ist es guter Stil Variablen gleich zu initialisieren, damit man davor bewahrt wird eine nicht initialisierte Variable zu verwenden. Ein Compiler, der eine Warnung bei benutzen einer nicht initialisierten Variablen ausspuckt, ist ein wahrer Segen. Mit bedacht kann man aber das Schlüsselwort out ruhigen Gewissens einsetzen.
Ein weiterer Vorteil des out Parameters ist die Rückgabe von mehreren Ergebnissen. Natürlich kann man dies auch mit Rückgabewerten von Methoden erreichen, jedoch ist man hier auf einen einzelnen Rückgabewert beschränkt. Eine Methode kann nicht zwei Werte zurückgeben. In solchen Fällen kann man zwar überlegen, ob es nicht sinnvoll ist beide Rückgabeparameter zu einen neuen Typ zusammenfassen und dadurch einen einzelnen Rückgabewert zu schaffen, jedoch gibt es Situationen, in denen eine Zusammenfassung aufgrund der Semantik der Daten einfach nicht sinnvoll ist, da die Daten logisch nicht zusammengehören oder es ist einfach bequemer zwei „Rückgabewerte“ bereitzustellen.
Beispiel:
C#-Code: |
private void getProxySettings(out string IP, out int Port)
{
}
|
Schlusswort
Ich hoffe der Unterschied zwischen call by value und call by reference ist klar geworden. Der direkte Zugriff auf als public Elemente, wie in den obigen Beispielen gezeigt (MeinAuto.m_AnzahlReifen) ist natürlich kein sauberer Stil. Die Art des Zugriffs auf public Elemente, wurde gewählt um den Umfang der Beispielprogramme möglichst gering und übersichtlich zu halten. Anstelle von direktem Zugriff auf Datenelemente sollte man so genannte Getter- und Settermethoden benutzen. In C# sind diese als Properties bekannt. Wer diese nicht schon lange kennt, sollte danach mal googeln.