C # 4.0: Posso usare un TimeSpan come parametro opzionale con un valore predefinito?


125

Entrambi generano un errore che dice che devono essere una costante di compilazione:

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

Prima di tutto, qualcuno può spiegare perché questi valori non possono essere determinati al momento della compilazione? E c'è un modo per specificare un valore predefinito per un oggetto TimeSpan opzionale?


11
Non correlato a ciò che chiedi, ma tieni presente che new TimeSpan(2000)non significa 2000 millisecondi, significa 2000 "tick" che è 0,2 millisecondi o un 10.000 di due secondi.
Jeppe Stig Nielsen,

Risposte:


173

Puoi aggirare questo problema molto facilmente cambiando la tua firma.

void Foo(TimeSpan? span = null) {

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

   ...

}

Dovrei approfondire: la ragione per cui quelle espressioni nel tuo esempio non sono costanti in fase di compilazione è perché in fase di compilazione, il compilatore non può semplicemente eseguire TimeSpan.FromSeconds (2.0) e inserire i byte del risultato nel tuo codice compilato.

Ad esempio, considera se hai provato a usare DateTime.Now. Il valore di DateTime.Now cambia ogni volta che viene eseguito. O supponiamo che TimeSpan.FromSeconds abbia preso in considerazione la gravità. È un esempio assurdo, ma le regole delle costanti del tempo di compilazione non fanno casi speciali solo perché sappiamo che TimeSpan.FromSeconds è deterministico.


15
Ora documenta il valore predefinito in <param>, perché non è visibile nella firma.
Colonnello Panic,

3
Non posso farlo, sto usando il valore speciale null per qualcos'altro.
Colonnello Panic,

4
@MattHickford - Quindi dovrai fornire un metodo sovraccarico o prendere millisecondi come parametro.
Josh,

19
Può essere utilizzato anche span = span ?? TimeSpan.FromSeconds(2.0);con il tipo nullable, nel metodo body. O var realSpan = span ?? TimeSpan.FromSeconds(2.0);per ottenere una variabile locale che non è nullable.
Jeppe Stig Nielsen,

5
La cosa che non mi piace di questo è che implica all'utente della funzione che questa funzione "funziona" con un intervallo nullo. Ma questo non è vero! Null non è un valore valido per span per quanto riguarda la logica effettiva della funzione. Vorrei che ci fosse un modo migliore che non sembrava un odore di codice ...
JoeCool

31

La mia eredità VB6 mi mette a disagio con l'idea di considerare "valore nullo" e "valore mancante" equivalenti. Nella maggior parte dei casi, probabilmente va bene, ma potresti avere un effetto collaterale non intenzionale o potresti ingoiare una condizione eccezionale (ad esempio, se l'origine di spanè una proprietà o una variabile che non dovrebbe essere nulla, ma lo è).

Vorrei quindi sovraccaricare il metodo:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}

1
+1 per quella grande tecnica. I parametri predefiniti dovrebbero essere usati solo con i tipi const, davvero. Altrimenti, non è affidabile.
Lazlo,

2
Questo è l'approccio "onorato nel tempo" che i valori predefiniti hanno sostituito, e per questa situazione penso che questa sia la risposta meno brutta;) Di per sé non funziona necessariamente così bene per le interfacce, perché vuoi davvero il valore predefinito in un posto. In questo caso ho trovato che i metodi di estensione sono uno strumento utile: l'interfaccia ha un metodo con tutti i parametri, quindi una serie di metodi di estensione dichiarati in una classe statica accanto all'interfaccia implementano i valori di default in vari sovraccarichi.
OlduwanSteve,

23

Funziona bene:

void Foo(TimeSpan span = default(TimeSpan))


4
Benvenuto in Stack Overflow. La tua risposta sembra essere che puoi fornire un valore di parametro predefinito, purché sia ​​l'unico valore molto specifico che il compilatore consente. L'ho capito bene? (Puoi modificare la tua risposta per chiarire.) Questa sarebbe una risposta migliore se mostrasse come sfruttare ciò che il compilatore consente di ottenere a ciò che la domanda originariamente cercava, che doveva avere altri TimeSpan valori arbitrari , come quello dato da new TimeSpan(2000).
Rob Kennedy,

2
Un'alternativa che utilizza un determinato valore predefinito sarebbe quella di utilizzare un file statico privato di sola lettura TimeSpan defaultTimespan = Timespan.FromSeconds (2) combinato con il costruttore predefinito e il costruttore che richiede un intervallo di tempo. public Foo (): this (defaultTimespan) e public Foo (Timespan ts)
johan mårtensson,

15

L'insieme di valori che è possibile utilizzare come valore predefinito sono gli stessi utilizzati per un argomento di attributo. Il motivo è che i valori predefiniti sono codificati in metadati all'interno di DefaultParameterValueAttribute.

Quanto al perché non può essere determinato al momento della compilazione. L'insieme di valori ed espressioni su tali valori consentiti al momento della compilazione è elencato nelle specifiche ufficiali del linguaggio C # :

C # 6.0 - Tipi di parametri di attributo :

I tipi di parametri posizionali e denominati per una classe di attributi sono limitati ai tipi di parametro di attributo , che sono:

  • Uno dei seguenti tipi: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • Il tipo object.
  • Il tipo System.Type.
  • Un tipo enum.
    (purché abbia accessibilità pubblica e i tipi in cui è nidificato (se presente) hanno anche accessibilità pubblica)
  • Matrici monodimensionali dei tipi precedenti.

Il tipo TimeSpannon rientra in nessuno di questi elenchi e quindi non può essere utilizzato come costante.


2
Leggero nit-pick: la chiamata di un metodo statico non rientra in nessuno degli elenchi. TimeSpanpuò andare bene l'ultimo in questo elenco default(TimeSpan)è valido.
Codici InCos

12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

fornito default(TimeSpan)non è un valore valido per la funzione.

O

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

fornito new TimeSpan()non è un valore valido.

O

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

Questo dovrebbe essere migliore considerando che le possibilità di nullvalore essendo un valore valido per la funzione sono rare.


4

TimeSpanè un caso speciale per DefaultValueAttributee viene specificato usando qualsiasi stringa che può essere analizzata tramite il TimeSpan.Parsemetodo

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

3

Il mio consiglio:

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

    //...
}

BTW TimeSpan.FromSeconds(2.0)non è uguale new TimeSpan(2000): il costruttore prende le zecche.


2

Altre risposte hanno dato grandi spiegazioni sul perché un parametro opzionale non può essere un'espressione dinamica. Ma, per raccontare, i parametri predefiniti si comportano come costanti di tempo di compilazione. Ciò significa che il compilatore deve essere in grado di valutarli e trovare una risposta. Ci sono alcune persone che vogliono che C # aggiunga supporto per il compilatore che valuta espressioni dinamiche quando si incontrano dichiarazioni costanti: questo tipo di funzionalità sarebbe collegata ai metodi di marcatura "puri", ma al momento non è una realtà e potrebbe non esserlo mai.

Un'alternativa all'utilizzo di un parametro predefinito C # per tale metodo sarebbe quella di utilizzare il modello esemplificato da XmlReaderSettings. In questo modello, definire una classe con un costruttore senza parametri e proprietà scrivibili pubblicamente. Quindi sostituire tutte le opzioni con i valori predefiniti nel metodo con un oggetto di questo tipo. Anche rendere questo oggetto facoltativo specificando un valore predefinito nullper esso. Per esempio:

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);
    }
}

Per chiamare, usa quella strana sintassi per creare un'istanza e assegnare proprietà in un'unica espressione:

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

Svantaggi

Questo è un approccio davvero pesante per risolvere questo problema. Se stai scrivendo un'interfaccia interna veloce e sporca e rendendo TimeSpannullable e trattando null come il tuo valore predefinito desiderato funzionerebbe bene, fallo invece.

Inoltre, se si dispone di un gran numero di parametri o si sta chiamando il metodo in un ciclo stretto, questo avrà il sovraccarico delle istanze di classe. Naturalmente, se si chiama un metodo simile in un ciclo stretto, potrebbe essere naturale e persino molto semplice riutilizzare un'istanza FooSettingsdell'oggetto.

Benefici

Come ho detto nel commento nell'esempio, penso che questo modello sia ottimo per le API pubbliche. L'aggiunta di nuove proprietà a una classe è una modifica ABI senza interruzioni, quindi è possibile aggiungere nuovi parametri opzionali senza modificare la firma del metodo utilizzando questo modello, offrendo più codice compilato più recentemente più opzioni continuando a supportare il vecchio codice compilato senza lavoro aggiuntivo .

Inoltre, poiché i parametri del metodo predefinito incorporato in C # sono trattati come costanti del compiletime e inseriti nel sito di chiamata, i parametri predefiniti verranno utilizzati dal codice solo dopo essere stati ricompilati. Creando un'istanza di un oggetto settings, il chiamante carica dinamicamente i valori predefiniti quando chiama il metodo. Ciò significa che è possibile aggiornare le impostazioni predefinite semplicemente modificando la classe delle impostazioni. Pertanto, questo modello consente di modificare i valori predefiniti senza dover ricompilare i chiamanti per visualizzare i nuovi valori, se desiderato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.