web-dev-qa-db-fra.com

C # 4.0: Puis-je utiliser TimeSpan comme paramètre optionnel avec une valeur par défaut?

Tous les deux génèrent une erreur en disant qu'ils doivent être une constante de compilation:

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

Tout d’abord, quelqu'un peut-il expliquer pourquoi ces valeurs ne peuvent pas être déterminées au moment de la compilation? Et existe-t-il un moyen de spécifier une valeur par défaut pour un objet TimeSpan facultatif?

104
Mike Pateras

Vous pouvez contourner ce problème très facilement en modifiant votre signature.

void Foo(TimeSpan? span = null) {

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

   ...

}

Je devrais préciser - la raison pour laquelle ces expressions dans votre exemple ne sont pas des constantes à la compilation est qu’au moment de la compilation, le compilateur ne peut pas simplement exécuter TimeSpan.FromSeconds (2.0) et coller les octets du résultat dans votre code compilé.

Par exemple, demandez-vous si vous avez plutôt essayé d'utiliser DateTime.Now. La valeur de DateTime.Now change à chaque exécution. Ou supposons que TimeSpan.FromSeconds prenne en compte la gravité. C'est un exemple absurde, mais les règles des constantes de compilation ne font pas de cas particuliers simplement parce que nous savons que TimeSpan.FromSeconds est déterministe.

151
Josh

Mon héritage VB6 me met mal à l'aise avec l'idée de considérer "valeur nulle" et "valeur manquante" comme équivalentes. Dans la plupart des cas, c'est probablement bien, mais vous pouvez avoir un effet secondaire non souhaité ou avaler une condition exceptionnelle (par exemple, si la source de span est une propriété ou une variable qui ne devrait pas être null, mais l'est).

Je surchargerais donc la méthode:

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

Cela fonctionne bien:

void Foo(TimeSpan span = default(TimeSpan))

19
Elena Lavrinenko

L'ensemble de valeurs pouvant être utilisées comme valeur par défaut est identique à celui qui peut être utilisé pour un argument d'attribut. La raison en est que les valeurs par défaut sont codées dans des métadonnées à l'intérieur de la variable DefaultParameterValueAttribute

Pourquoi ne peut-il pas être déterminé au moment de la compilation? L'ensemble des valeurs et des expressions sur ces valeurs autorisées au moment de la compilation est répertorié dans la spécification officielle du langage C # :

C # 6.0 - Types de paramètre d'attribut :

Les types de paramètres de position et de paramètres nommés d'une classe d'attribut sont limités aux types de paramètre d'attribut , à savoir:

  • L'un des types suivants: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • Le type object.
  • Le type System.Type.
  • Un type enum.
    (à condition qu'il soit accessible au public et que les types dans lesquels il est imbriqué (le cas échéant) le soient également)
  • Tableaux unidimensionnels des types ci-dessus.

Le type TimeSpan ne rentre dans aucune de ces listes et ne peut donc pas être utilisé comme constante. 

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

default(TimeSpan) fourni n'est pas une valeur valide pour la fonction.

Ou 

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

new TimeSpan() fourni n'est pas une valeur valide.

Ou

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

Cela devrait être mieux compte tenu des chances rares que la valeur null soit une valeur valide pour la fonction.

10
nawfal

TimeSpan est un cas spécial pour DefaultValueAttribute et est spécifié à l'aide de toute chaîne pouvant être analysée via la méthode TimeSpan.Parse.

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

Autres réponses ont bien expliqué pourquoi un paramètre facultatif ne peut pas être une expression dynamique. Mais pour recompter, les paramètres par défaut se comportent comme des constantes de temps de compilation. Cela signifie que le compilateur doit être capable de les évaluer et de trouver une réponse. Certaines personnes souhaitent que C # ajoute un support pour que le compilateur évalue les expressions dynamiques lorsqu’il rencontre des déclarations constantes. Ce type de fonctionnalité serait lié aux méthodes de marquage «pure», mais ce n’est pas une réalité et pourrait ne jamais l'être.

Une alternative à l'utilisation d'un paramètre par défaut C # pour une telle méthode serait d'utiliser le modèle illustré par XmlReaderSettings . Dans ce modèle, définissez une classe avec un constructeur sans paramètre et des propriétés accessibles en écriture. Remplacez ensuite toutes les options par défaut dans votre méthode par un objet de ce type. Même rendre cet objet facultatif en spécifiant une valeur par défaut de null pour celui-ci. Par exemple:

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

Pour appeler, utilisez cette syntaxe étrange pour instancier et affecter des propriétés dans une seule expression:

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

Inconvénients

C'est une approche très lourde pour résoudre ce problème. Si vous écrivez une interface interne rapide et sale et rendant la TimeSpan nullable et traitant null comme votre valeur par défaut souhaitée fonctionnerait correctement, faites-le plutôt.

De même, si vous avez un grand nombre de paramètres ou appelez la méthode dans une boucle étroite, cela entraînera une surcharge d'instanciations de classe. Bien sûr, si vous appelez une telle méthode dans une boucle étroite, il peut être naturel et même très facile de réutiliser une instance de l'objet FooSettings.

Avantages

Comme je l'ai mentionné dans le commentaire de l'exemple, je pense que ce modèle convient parfaitement aux API publiques. L'ajout de nouvelles propriétés à une classe est une modification ABI ininterrompue. Vous pouvez donc ajouter de nouveaux paramètres facultatifs sans changer la signature de votre méthode à l'aide de ce modèle, ce qui donne davantage de code compilé récemment, tout en maintenant les anciens codes compilés sans travail supplémentaire. .

De plus, étant donné que les paramètres de méthode par défaut intégrés de C # sont traités comme des constantes de compilation et qu’ils sont intégrés au site d’appel, les paramètres par défaut ne seront utilisés par le code qu’une fois celui-ci recompilé. En instanciant un objet de paramètres, l'appelant charge dynamiquement les valeurs par défaut lorsqu'il appelle votre méthode. Cela signifie que vous pouvez mettre à jour les valeurs par défaut en modifiant simplement votre classe de paramètres. Ainsi, ce modèle vous permet de modifier les valeurs par défaut sans avoir à recompiler les appelants pour afficher les nouvelles valeurs, si cela est souhaité.

1
binki

Ma suggestion:

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

    //...
}

BTW TimeSpan.FromSeconds(2.0) n'est pas égal à new TimeSpan(2000) - le constructeur prend des ticks.

1
tymtam