C # 4.0: Kann ich ein TimeSpan als optionalen Parameter mit einem Standardwert verwenden?

125

Beide erzeugen einen Fehler, der besagt, dass es sich um eine Konstante zur Kompilierungszeit handeln muss:

void Foo(TimeSpan span = TimeSpan.FromSeconds(2.0))
void Foo(TimeSpan span = new TimeSpan(2000))

Kann jemand zunächst erklären, warum diese Werte beim Kompilieren nicht ermittelt werden können? Und gibt es eine Möglichkeit, einen Standardwert für ein optionales TimeSpan-Objekt anzugeben?

Mike Pateras
quelle
11
Nicht im Zusammenhang mit dem, was Sie fragen, aber beachten Sie, dass new TimeSpan(2000)dies nicht 2000 Millisekunden bedeutet, sondern 2000 "Ticks", was 0,2 Millisekunden oder eine 10.000stel von zwei Sekunden entspricht.
Jeppe Stig Nielsen

Antworten:

172

Sie können dies sehr einfach umgehen, indem Sie Ihre Signatur ändern.

void Foo(TimeSpan? span = null) {

   if (span == null) { span = TimeSpan.FromSeconds(2); }

   ...

}

Ich sollte näher darauf eingehen - der Grund, warum diese Ausdrücke in Ihrem Beispiel keine Konstanten zur Kompilierungszeit sind, liegt darin, dass der Compiler zur Kompilierungszeit TimeSpan.FromSeconds (2.0) nicht einfach ausführen und die Bytes des Ergebnisses in Ihren kompilierten Code einfügen kann.

Überlegen Sie beispielsweise, ob Sie stattdessen DateTime.Now verwenden möchten. Der Wert von DateTime.Now ändert sich bei jeder Ausführung. Oder nehmen wir an, dass TimeSpan.FromSeconds die Schwerkraft berücksichtigt. Es ist ein absurdes Beispiel, aber die Regeln für Konstanten zur Kompilierungszeit machen keine Sonderfälle, nur weil wir zufällig wissen, dass TimeSpan.FromSeconds deterministisch ist.

Josh
quelle
15
Dokumentieren Sie nun den Standardwert in <param>, da er in der Signatur nicht sichtbar ist.
Colonel Panic
3
Ich kann das nicht tun, ich verwende den speziellen Wert null für etwas anderes.
Oberst Panik
4
@MattHickford - Dann müssen Sie eine überladene Methode angeben oder Millisekunden als Parameter verwenden.
Josh
19
Kann auch span = span ?? TimeSpan.FromSeconds(2.0);mit dem nullbaren Typ im Methodenkörper verwendet werden. Oder var realSpan = span ?? TimeSpan.FromSeconds(2.0);um eine lokale Variable zu erhalten, die nicht nullwertfähig ist.
Jeppe Stig Nielsen
5
Das, was mir daran nicht gefällt, ist, dass es für den Benutzer der Funktion impliziert, dass diese Funktion mit einer Nullspanne "funktioniert". Das stimmt aber nicht! Für die tatsächliche Logik der Funktion ist Null kein gültiger Wert für span. Ich wünschte, es gäbe einen besseren Weg, der nicht nach Code roch ...
JoeCool
31

Mein VB6-Erbe macht mich unruhig mit der Idee, "Nullwert" und "fehlender Wert" als gleichwertig zu betrachten. In den meisten Fällen ist dies wahrscheinlich in Ordnung, aber Sie haben möglicherweise eine unbeabsichtigte Nebenwirkung oder schlucken eine Ausnahmebedingung (z. B. wenn die Quelle spaneine Eigenschaft oder Variable ist, die nicht null sein sollte, aber ist).

Ich würde daher die Methode überladen:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}
Phoog
quelle
1
+1 für diese großartige Technik. Standardparameter sollten eigentlich nur mit const-Typen verwendet werden. Sonst ist es unzuverlässig.
Lazlo
2
Dies ist der "altehrwürdige" Ansatz, den Standardwerte ersetzt haben, und für diese Situation halte ich dies für die am wenigsten hässliche Antwort;) Für sich genommen funktioniert er für Schnittstellen jedoch nicht unbedingt so gut, da Sie wirklich den Standardwert in möchten ein Platz. In diesem Fall habe ich festgestellt, dass Erweiterungsmethoden ein nützliches Werkzeug sind: Die Schnittstelle verfügt über eine Methode mit allen Parametern. Anschließend implementieren eine Reihe von Erweiterungsmethoden, die in einer statischen Klasse neben der Schnittstelle deklariert sind, die Standardeinstellungen in verschiedenen Überladungen.
OlduwanSteve
23

Das funktioniert gut:

void Foo(TimeSpan span = default(TimeSpan))

Elena Lavrinenko
quelle
4
Willkommen bei Stack Overflow. Ihre Antwort scheint zu sein , dass Sie können einen Standardparameterwert liefern, solange es die eine sehr spezifische Wert ist , dass der Compiler ermöglicht. Habe ich das richtig verstanden? (Sie können Ihre Antwort zur Verdeutlichung bearbeiten .) Dies wäre eine bessere Antwort, wenn gezeigt würde, wie Sie die Vorteile des Compilers nutzen können, um zu der ursprünglich gesuchten Frage zu gelangen, die beliebige andere TimeSpan Werte haben soll, z. B. die von new TimeSpan(2000).
Rob Kennedy
2
Eine Alternative, die einen bestimmten Standardwert verwendet, wäre die Verwendung eines privaten statischen schreibgeschützten TimeSpan defaultTimespan = Timespan.FromSeconds (2) in Kombination mit einem Standardkonstruktor und einem Konstruktor, die eine Zeitspanne benötigen. public Foo (): dies (defaultTimespan) und public Foo (Timespan ts)
johan mårtensson
15

Die Werte, die als Standardwert verwendet werden können, sind dieselben wie für ein Attributargument. Der Grund dafür ist, dass Standardwerte in Metadaten innerhalb von codiert werden DefaultParameterValueAttribute.

Warum kann es zur Kompilierungszeit nicht ermittelt werden? Der Satz von Werten und Ausdrücken über solchen Werten, die zum Zeitpunkt der Kompilierung zulässig sind, ist in der offiziellen C # -Sprachenspezifikation aufgeführt :

C # 6.0 - Attributparametertypen :

Die Arten von Positions- und benannten Parametern für eine Attributklasse sind auf die Attributparametertypen beschränkt , die:

  • Einer der folgenden Typen: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • Der Typ object.
  • Der Typ System.Type.
  • Ein Aufzählungstyp.
    (vorausgesetzt, es ist öffentlich zugänglich und die Arten, in denen es verschachtelt ist (falls vorhanden), sind auch öffentlich zugänglich)
  • Eindimensionale Arrays der oben genannten Typen.

Der Typ TimeSpanpasst in keine dieser Listen und kann daher nicht als Konstante verwendet werden.

JaredPar
quelle
2
Leichte Fehlentscheidung: Das Aufrufen einer statischen Methode passt in keine der Listen. TimeSpankann passen, der letzte auf dieser Liste default(TimeSpan)ist gültig.
CodesInChaos
12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

unter der Voraussetzung default(TimeSpan) ist kein gültiger Wert für die Funktion.

Oder

//this works only for value types which TimeSpan is
void Foo(TimeSpan span = new TimeSpan())
{
    if (span == new TimeSpan()) 
        span = TimeSpan.FromSeconds(2); 
}

unter der Voraussetzung new TimeSpan() ist kein gültiger Wert.

Oder

void Foo(TimeSpan? span = null)
{
    if (span == null) 
        span = TimeSpan.FromSeconds(2); 
}

Dies sollte besser sein, da die Wahrscheinlichkeit, dass der nullWert ein gültiger Wert für die Funktion ist, selten ist.

nawfal
quelle
4

TimeSpanist ein Sonderfall für DefaultValueAttributeund wird mit einer beliebigen Zeichenfolge angegeben, die über die TimeSpan.ParseMethode analysiert werden kann.

[DefaultValue("0:10:0")]
public TimeSpan Duration { get; set; }
dahall
quelle
3

Mein Vorschlag:

void A( long spanInMs = 2000 )
{
    var ts = TimeSpan.FromMilliseconds(spanInMs);

    //...
}

Übrigens TimeSpan.FromSeconds(2.0)ist nicht gleich new TimeSpan(2000)- der Konstruktor nimmt Ticks.

Tymtam
quelle
2

Andere Antwort haben großartige Erklärungen gegeben, warum ein optionaler Parameter kein dynamischer Ausdruck sein kann. Um es noch einmal zu erzählen, verhalten sich Standardparameter wie Kompilierungszeitkonstanten. Das bedeutet, dass der Compiler sie auswerten und eine Antwort finden muss. Es gibt einige Leute, die möchten, dass C # den Compiler unterstützt, der dynamische Ausdrücke auswertet, wenn er auf konstante Deklarationen stößt. Diese Art von Funktion würde sich auf Markierungsmethoden als „rein“ beziehen, aber das ist derzeit keine Realität und wird es möglicherweise nie sein.

Eine Alternative zur Verwendung eines C # -Standardparameters für eine solche Methode wäre die Verwendung des durch beispielhaft dargestellten Musters XmlReaderSettings. Definieren Sie in diesem Muster eine Klasse mit einem parameterlosen Konstruktor und öffentlich beschreibbaren Eigenschaften. Ersetzen Sie dann alle Optionen durch Standardeinstellungen in Ihrer Methode durch ein Objekt dieses Typs. Machen Sie dieses Objekt sogar optional, indem Sie einen Standardwert nulldafür angeben. Beispielsweise:

public class FooSettings
{
    public TimeSpan Span { get; set; } = TimeSpan.FromSeconds(2);

    // I imagine that if you had a heavyweight default
    // thing you’d want to avoid instantiating it right away
    // because the caller might override that parameter. So, be
    // lazy! (Or just directly store a factory lambda with Func<IThing>).
    Lazy<IThing> thing = new Lazy<IThing>(() => new FatThing());
    public IThing Thing
    {
        get { return thing.Value; }
        set { thing = new Lazy<IThing>(() => value); }
    }

    // Another cool thing about this pattern is that you can
    // add additional optional parameters in the future without
    // even breaking ABI.
    //bool FutureThing { get; set; } = true;

    // You can even run very complicated code to populate properties
    // if you cannot use a property initialization expression.
    //public FooSettings() { }
}

public class Bar
{
    public void Foo(FooSettings settings = null)
    {
        // Allow the caller to use *all* the defaults easily.
        settings = settings ?? new FooSettings();

        Console.WriteLine(settings.Span);
    }
}

Verwenden Sie zum Aufrufen diese seltsame Syntax, um Eigenschaften in einem Ausdruck zu instanziieren und zuzuweisen:

bar.Foo(); // 00:00:02
bar.Foo(new FooSettings { Span = TimeSpan.FromDays(1), }); // 1.00:00:00
bar.Foo(new FooSettings { Thing = new MyCustomThing(), }); // 00:00:02

Nachteile

Dies ist ein wirklich schwerer Ansatz zur Lösung dieses Problems. Wenn Sie eine schnelle und fehlerhafte interne Schnittstelle schreiben und die TimeSpanNullbar machen und Null wie Ihren gewünschten Standardwert behandeln, funktioniert dies einwandfrei.

Wenn Sie eine große Anzahl von Parametern haben oder die Methode in einer engen Schleife aufrufen, hat dies auch den Aufwand für Klasseninstanziierungen. Wenn Sie eine solche Methode in einer engen Schleife aufrufen, kann es natürlich natürlich und sogar sehr einfach sein, eine Instanz des FooSettingsObjekts wiederzuverwenden .

Leistungen

Wie ich im Kommentar im Beispiel erwähnt habe, ist dieses Muster für öffentliche APIs ideal. Das Hinzufügen neuer Eigenschaften zu einer Klasse ist eine nicht unterbrechende ABI-Änderung. Sie können also neue optionale Parameter hinzufügen, ohne die Signatur Ihrer Methode mithilfe dieses Musters zu ändern. So erhalten Sie neueren kompilierten Code mehr Optionen, während Sie weiterhin alten kompilierten Code ohne zusätzliche Arbeit unterstützen .

Da die in C # integrierten Standardmethodenparameter als Kompilierungszeitkonstanten behandelt und in die Aufrufseite eingebrannt werden, werden Standardparameter nur dann vom Code verwendet, wenn sie neu kompiliert werden. Durch Instanziieren eines Einstellungsobjekts lädt der Aufrufer beim Aufrufen Ihrer Methode dynamisch die Standardwerte. Dies bedeutet, dass Sie die Standardeinstellungen aktualisieren können, indem Sie einfach Ihre Einstellungsklasse ändern. Auf diese Weise können Sie Standardwerte ändern, ohne Anrufer neu kompilieren zu müssen, um die neuen Werte anzuzeigen, falls dies gewünscht wird.

Binki
quelle