Optionale Parameter in .NET

Einleitung

Als optionale Parameter werden Prozedurparameter bezeichnet, die beim Prozeduraufruf in der Parameterliste nicht angeführt werden müssen. Optionale Parameter werden üblicherweise mit der Übergabe von Parameterstandardwerten für die nicht explizit aufgeführten und mit einem Wert versehenen Parameter verbunden. Die Standardwerte der Parameter werden als Teil der Prozedurspezifikation dokumentiert und sind somit Teil der Schnittstelle. Optionale Parameter sowie Parameterstandardwerte sind aus Visual Basic 6.0 und der Programmiersprache C bekannt. Die Programmiersprache Visual Basic .NET unterstützt ebenfalls optionale Parameter.

In Bezug auf .NET sind optionale Parameter und Parameterstandardwerte und ihre Umsetzung in Visual Basic Ausgangspunkt kontroverser Diskussionen. Ziel dieses Artikels ist es, die vorgebrachten Argumente gegen die Unterstützung und Verwendung optionale Parameter hinsichtlich ihrer Stichhaltigkeit zu untersuchen. Ferner wird die Implementierung optionaler Parameter in Visual Basic beleuchtet. Auf Basis einer Betrachtung der Versionierungsproblematik werden Leitlinien zur Nutzung optionaler Parameter vorgeschlagen.

Optionale Parameter in .NET

In der Common Intermediate Language (CIL) steht zum Markieren von Parametern als optional das Parameterattribut opt zur Verfügung. Der Visual-Basic-Compiler emittiert dieses Attribut für optionale Parameter. Die zu wählende Implementierung für optionale Parameter wird in der Common Language Specification (CLS) jedoch nicht vorgegeben. Obwohl Definition und Aufruf von Methoden mit optionalen Parametern nicht von allen .NET-Programmiersprachen gleichermaßen unterstützt werden, ist das Attribut opt CLS-konform ([1], Teil II Metadata Definition and Semantics, Kap. 15.4 Defining methods, S. 73).

Über die .param-Anweisung ([1], Teil II Metadata Definition and Semantics, Kap. 15.4.1.4 The .param type directive, S. 75) können konstante Werte mit Methodenparametern verknüpft werden. Die Spezifikation läßt die Semantik der angegebenen Werte offen, führt jedoch in einer Anmerkung Parameterstandardwerte als möglichen Nutzungszweck an. Der Visual-Basic-Compiler nutzt die .param-Anweisung, um die Parameterstandardwerte im ausgegebenen IL-Code zu hinterlegen. Außerdem ermittelt der Compiler daraus die bei Aufrufen von Methoden mit optionalen Parametern für weggelassene Parameter eingefügten Werte.

Folgendes Listing zeigt die Implementierung einer Methode Test in Visual Basic, die einen optionalen Parameter i des Datentyps Integer mit Standardwert 12 enthält. Der optionale Parameter wird mittels des Schlüsselwortes Optional gekennzeichnet und über den Operator = der Standardwert zugewiesen:

Private Sub Test(ByVal Optional i As Integer = 12)
End Sub

Da C# optionale Parameter nicht direkt in der Syntax unterstützt, erfolgen das Auszeichnen eines Parameters als optional und die Angabe seines Standardwertes über Attribute:

private void Test([Optional, DefaultParameterValue(12)] int i)
{
}

Die Compiler für Visual Basic und C# emittieren für die beiden gezeigten Implementierungen der Methode Test identischen IL-Code. Der Parameter i wird dabei mit dem Modifizierer opt versehen und der Standardwert 12 des Parameters im ersten Element der .param-Anweisung eingetragen. Der IL-Code im IL-Assembler-Format sieht wie folgt aus:

.method private hidebysig instance void Test([opt] int32 i) cil managed 
{
    .param [1] = int32(0x0000000C)
    .maxstack 8
    L_0000: nop
    L_0001: ret
}

Optionale Parameter vs. Überladung

In Diskussionen zu optionalen Parametern wird oftmals das Ersetzen einer Methode mit optionalen Parametern durch ein Bündel an überladenen Methoden unterschiedlicher Signatur empfohlen. Während dies in jenen Fällen, in denen es sich beim Standardwert um ein nicht feststehendes Implementierungsdetail handelt, eine sinnvolle Lösung darstellt, ist es problematisch, wenn dabei wichtige Vertragsbestandteile versteckt werden. Sollen beispielsweise beim Schließen eines Objekts durchgeführte Änderungen übernommen werden, bietet es sich an, der Methode einen optionalen Parameter des Datentyps Boolean zu übergeben, der gesetzt werden kann, um die Daten zu speichern:

Public Sub Close(Optional ByVal PersistChanges As Boolean = False)
    ⋮
End Sub

Sowohl die Implementierung von Close mit optionalem Parameter als auch die Lösung mittels Überladung erlaubt den Aufruf der Funktion ohne Angabe von Parametern und unter Übergabe des Wertes False für den Parameter PersistChanges. Trotzdem sind die beiden Lösungen nicht gleichwertig, da im Falle der Überladungen beim Aufruf der parameterlosen Methode nicht hervorgeht, welcher Wert für PersistChanges übergeben wird. Damit verliert der Quellcode seinen selbstdokumentierenden Charakter. Die genannte Lösung mit Überladung sieht folgendermaßen aus:

Public Sub Close()
    Me.Close(False)
End Sub

Public Sub Close(ByVal PersistChanges As Boolean)
    ⋮
End Sub

Bei einer größeren Anzahl an optionalen Parametern kann eine Umsetzung mittels Überladung zur Überladungshölle führen, die dadurch gekennzeichnet ist, daß eine hohe Anzahl verschiedener Überladungen mit geringen Unterschieden und stark ähnlicher Semantik existieren. Ein Beispiel für eine derartige Methode stellt MessageBox.Show mit seinen 21 Überladungen dar. Die in Visual Basic bekannte Funktion MsgBox bietet zwar etwas weniger Optionen, leidet jedoch nicht unter dieser Unübersichtlichkeit, da optionale Parameter eingesetzt werden. MsgBox hat zudem den Vorteil, daß die übergebenen Standardwerte einfach ermittelt werden können.

Kritik an optionalen Parametern

Eric Gunnerson, der an der Entwicklung von C# mitarbeitet, begründet im Artikel Does C# have default parameters? die Entscheidung dafür, den früh gebundenen Aufruf von Methoden mit Standardparametern unter Übergabe ihrer Standardwerte im C#-Compiler nicht zu unterstützen (diese Gründe wurden auch von Anders Hejlsberg in einem Interview auf der Konferenz Tech·Ed 2004 genannt). Er führt folgende Gründe an, die seiner Ansicht nach gegen die Unterstützung optionaler Parameter sprechen:

  1. Da die Standardwerte optionaler Parameter vom Compiler in Aufrufen der Methoden eingefügt werden, besteht keine Möglichkeit, den Standardwert des optionalen Parameters zu ändern, ohne aufrufende Bibliotheken neu zu kompilieren. Durch Einsatz von Überladung bleiben die Standardwerte einer Veränderung zugänglich, da sie nicht in Methodenaufrufe kopiert werden, sondern in der Bibliothek verbleiben, in der die Methode enthalten ist.

  2. Das Erstellen von Überladungen auf Basis der Methode mit optionalen Parametern bei deren Kompilierung ist mit zwei Problemen behaftet: Der Zusammenhang zwischen dem vom Benutzer geschriebenen Code und den vom Compiler ausgegebenen Methoden ist weniger offensichtlich. Außerdem wären für XML-Kommentare und IntelliSense eigene Regeln erforderlich, um Kommentare für die Überladungen zu erstellen und die Überladungen in IntelliSense zu einer einzigen Methode zusammenzufügen.

Die angeführten Gründe halten jedoch einer eingehenden Prüfung nicht stand. Der erste Kritikpunkt ist rein theoretischer Natur, da optionale Parameter mit Standardwerten in Szenarien nicht geeignet sind, in denen der Standardwert nicht unumstößlich feststeht. Über compilerseitige Versionierungsmechanismen wie der Möglichkeit, eine Bibliothek anzugeben, zu der das erstellte Kompilat kompatibel sein soll, kann das problematische Ändern des Parameterstandardwertes (wie auch andere inkompatible Änderungen) erkannt und verhindert werden.

Der zweite Kritikpunkt richtet sich nicht direkt gegen optionale Parameter, sondern eine bestimmte Form der Implementierung, die von anderen .NET-Programmiersprachen wie Visual Basic nicht gewählt wurde. Wie Gunnerson zurecht anmerkt, können Methoden mit optionalen Parametern nicht in allen Fällen durch das Erstellen von Überladungen durch den Compiler ersetzt werden. Dabei kann es nämlich zu mehreren Überladungen mit gleicher Signatur kommen, sofern zwei oder mehr optionale Parameter, die hintereinander in der Parameterliste stehen, den selben Datentyp besitzen:

Public Sub Test( _
    Optional ByVal Param1 As String = Nothing, _
    Optional ByVal Param2 As String = Nothing _
)
    ⋮
End Sub

Die im vorigen Listing gezeigte Methode Test kann folgendermaßen aufgerufen werden:

Test()                  ' Aufruf 'Test(Nothing, Nothing)'.
Test("Hello")           ' Aufruf 'Test("Hello", Nothing)'.
Test(, "World")         ' Aufruf 'Test(Nothing, "World")'.
Test("Hello", "World")  ' Aufruf 'Test("Hello", "World")'.

Beim Ersatz der Methode Test mit ihren beiden optionalen Parametern durch Überladung ist es nicht möglich, den Aufruf unter Auslassung des ersten optionalen Parameters nachzubilden, da die Methoden Test(Param1) und Test(Param2) aufgrund der Gleichheit des Parameterdatentyps die gleiche Signatur aufweisen würden. Lediglich die im nachstehenden Listing angeführten Methodendefinitionen sind möglich:

Public Sub Test(ByVal Param1 As String)
    ⋮
End Sub

Public Sub Test(ByVal Param1 As String, ByVal Param2 As String)
    ⋮
End Sub

Kunstgriffe wären notwendig, um derartige Szenarien mittels Überladung nachzubilden, etwa die für den Benutzer transparente Vergabe unterschiedlicher Methodennamen oder implizites und für den Entwickler transparentes Hinzufügen zusätzlicher Methodenparameter durch den Compiler. Dadurch würde jedoch die intuitive Nutzung der Methoden im vorgesehenen Sinn in Programmiersprachen, welche diese Implementierung optionaler Parameter nicht unterstützen, behindert.

Standardwerte als Teil der Schnittstelle

Da Standardwerte nicht in allen Fällen veränderliche Implementierungsdetails darstellen, die zurecht für den Benutzer transparent sein sollten, ist das Verstecken der Standardwerte in Überladungen nicht immer sinnvoll. Einige Klassen des .NET Frameworks nutzen Überladung, um vermeintliche „Details“ zu verstecken, obwohl die Kenntnis dieser Details für Auswahl und Verwendung der Methoden von Bedeutung ist. Die Folge sind Fehler im die Methode nutzenden Code.

Beispielsweise ist der Konstruktor der Klasse System.IO.FileStream überladen. Nicht alle Überladungen des Konstruktors besitzen einen Parameter des Typs FileShare, mittels dessen der gemeinsame Zugriff auf die Datei durch mehrere Prozesse eingestellt werden kann. Bei den Überladungen ohne FileShare-Parameter wird, so die Dokumentation, implizit FileShare.Read übergeben. Dabei ist das Wissen um den genutzten FileShare-Wert wichtig, um die Auswirkungen des Methodenaufrufs auf andere Programme zu erkennen und sie in der Fehlerbehandlung im eigenen Projekt berücksichtigen zu können.

Ein weiteres Beispiel stellen die Überladungen des Konstruktors der Klasse System.IO.StreamReader dar, in denen die beim Lesen der Daten genutzte Codierung implizit auf Encoding.UTF8 festgelegt wird, obwohl zur Zeit in der Praxis die meisten Dateien Windows-ANSI-codiert sind und damit das Lesen zu unerwarteten Ergebnissen führt. In diesem Fall wäre eine Lösung mit optionalen Parametern nicht möglich, da es sich bei Encoding um einen Verweistyp handelt und kein konstanter Parameterstandardwert angegeben werden kann. Trotzdem ist die vorzufindende Situation unbefriedigend und verlangt nach einer weiterreichenden Lösung.

Versionierung von Methoden mit optionalen Parametern

Die Werte optionaler Parameter sind genauso als Teil der Objektschnittstelle anzusehen wie Methodensignaturen. Compiler, die den Aufruf von Methoden mit optionalen Parametern unter Übergabe ihrer Standardwerte unterstützen, ermitteln bei der Kompilierung den Wert des Standardparameters und fügen ihn im emittierten Methodenaufruf in der Argumentliste ein. Daher können Standardwerte optionaler Parameter nicht nach Belieben geändert werden, sondern ihre Veränderung darf nur über eine geordnete Versionierung erfolgen.

Gehen wir davon aus, daß der optionale Parameter i einer Methode Test den Standardwert 12 besitzt. In einer neuen Version der Bibliothek ist nun eine Änderung des Standardwertes auf den Wert 100 erforderlich. (Hierbei ist anzumerken, daß eine derartige Änderung immer ein Hinweis auf einen schlechten Entwurf der ursprünglichen Version der Methode darstellt und gänzlich vermeidbar ist.) Die Signatur der Methode vor der Änderung sieht folgendermaßen aus:

Public Sub Test(Optional ByVal i As Integer = 12)
    ⋮
End Sub

Das intuitive Ändern des Standardwertes von Parameter i in der Signatur von 12 auf 100 ist zwar möglich, jedoch nicht zulässig. Einerseits würde es das spezifizierte Verhalten der Methode verändern und deren Implementierung damit nicht mehr spezifikationskonform sein, andererseits kann es in Szenarien, in denen Bibliotheken zum Einsatz gelangen, die gegen unterschiedliche Versionen der Methode kompiliert wurden, zu inkonsistentem Verhalten führen. Die folgende Änderung darf deshalb nicht durchgeführt werden:

Public Sub Test(Optional ByVal i As Integer = 100)
   ⋮
End Sub

Stattdessen kann die bestehende Methode durch Markieren mit dem Attribut Obsolete für überholt erklärt und das geänderte Verhalten in einer neuen Methode bereitgestellt werden. Durch Übergabe des Wertes True für die Eigenschaft IsError des Attributs Obsolete wird beim Kompilieren ein Kompilierungsfehler ausgelöst, wenn die als überholt gekennzeichnete Methode aufgerufen wird. Ist der Einsatz der bestehenden Methode weiterhin sinnvoll, kann das Markieren als überholt unterbleiben. Der Quellcode in der neuen Version der Bibliothek könnte folgendermaßen aussehen:

<Obsolete("Use 'Test2(Integer)' instead.")> _
Public Sub Test(Optional ByVal i As Integer = 12)
    ⋮
End Sub

Public Sub Test2(Optional ByVal i As Integer = 100)
    ⋮
End Sub

Die Versionierung von Methoden mit optionalen Parametern gestaltet sich nicht komplizierter als jene mehrerer Überladungen, da optionale Parameter ohnehin nicht eingesetzt werden sollten, wenn der Standardparameterwert keinen festen Teil der Objektschnittstelle darstellt. Die Angabe eines Standardsteuersatzes in einem Parameter des Datentyps Double wäre nicht ratsam, wenn der Steuersatz im Laufe der Zeit einer Änderung unterworfen ist und dadurch der angegebene Parameterstandardwert nicht mehr dem Standardsteuersatz entsprechen würde. In diesem Fall ist eine Lösung mit Überladung vorzuziehen:

Public Sub Calculate()

    ' 'DefaultTaxRate' könnte als 'ReadOnly'-Eigenschaft implementiert sein.
    Me.Calculate(Me.DefaultTaxRate)
End Sub

Public Sub Calculate(ByVal TaxRate As Double)
    ⋮
End Sub

Optionale Parameter in C#

Die Programmiersprache C# bietet in ihrer Syntax weder spezifische Unterstützung für die Definition noch den Aufruf von Methoden mit optionalen Parametern. Dennoch können zumindest Methoden mit optionalen Parametern problemlos über Attribute an den Parametern definiert werden. Der Aufruf ist zwar möglich, jedoch weitaus weniger komfortabel als in Visual Basic, da weder der Compiler Unterstützung bietet noch durch die Entwicklungsumgebung die Nutzung mittels IntelliSense und Hinweistexten vereinfacht wird.

Methoden mit optionalen Parametern und Parameterstandardwerten können in C# definiert werden. Der C#-Compiler emittiert für mit dem Attribut Optional gekennzeichnete und über das Attribut DefaultParameterValue mit einem Standardwert versehene Parameter die gleichen IL-Anweisungen wie der Visual-Basic-Compiler. Folgendes Listing zeigt die Definition einer Methode mit einem optionalen Parameter:

using System.Runtime.InteropServices;

⋮

public void Test([Optional, DefaultParameterValue(10)]int i)
{
    ⋮
}

Beim Aufruf von Methoden mit optionalen Parametern muß nach Nutzungsszenario unterschieden werden: Für optionale Parameter des Typs Object in COM Interop kann der Wert Type.Missing übergeben werden, um anzuzeigen, daß ihr Wert nicht angegeben wird. An die aufgerufene Methode wird dann im Parameter ein VARIANT des Typs VT_ERROR mit dem Wert DISP_E_PARAMNOTFOUND übergeben. Innerhalb von Visual Basic 6.0 könnte dann der Variant-Parameter mittels der Funktion IsMissing daraufhin geprüft werden, ob für ihn ein Wert übergeben wurde.

In anderen Fällen, etwa bei in Visual Basic implementierten Methoden mit optionalen Parametern, müssen ebenfalls alle Parameter im Aufruf aufscheinen; ihre Standardwerte können entweder vom Benutzer händisch im Code eingetragen oder zur Laufzeit über Reflection ermittelt werden. Erstere Vorgehensweise bietet sich an, da sie den Quellcode lesbarer hält. Das Ermitteln des Parameterwertes zur Laufzeit ist langsamer, bewirkt hingegen, daß selbst bei Änderung des Standardwertes immer der aktuelle Wert übergeben wird. Folgendes Listing zeigt den Aufruf der zuvor vorgestellten Methode Test unter Übergabe des Standardwertes für den optionalen Parameter:

Test(
    (int)this.GetType().GetMethod("Test").GetParameters()[0].DefaultValue
);

Schlußwort

Optionale Methodenparameter sind ein ausdrucksstarkes Mittel, das durch Überladung nicht vollständig ersetzt werden kann. Richtig eingesetzt stellen optionale Parameter keine Quelle für Versionierungsprobleme dar, da ihre Standardwerte Teil des durch die veröffentlichte Schnittstelle definierten Vertrages darstellen. In der Praxis ist besonders der Aufruf von Methoden mit optionalen Parametern wichtig, um Komponenten nutzen zu können, die mit verbreiteten .NET-Programmiersprachen erstellt wurden, welche die Definition von Methoden mit optionalen Parametern zulassen. Deshalb wäre es sinnvoll, in C# zumindest den Aufruf von Methoden mit optionalen Parametern direkt durch den Compiler zu unterstützen.

Literaturverzeichnis

[1]
Microsoft Corporation: Standard ECMA-335 – Common Language Infrastructure (CLI), 4th Edition, ECMA, Juni 2006.