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

125

Les deux génèrent une erreur indiquant 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?

Mike Pateras
la source
11
Pas lié à ce que vous demandez, mais sachez que new TimeSpan(2000)cela ne signifie pas 2000 millisecondes, cela signifie 2000 "ticks", soit 0,2 millisecondes, soit un 10 000 ème de deux secondes.
Jeppe Stig Nielsen

Réponses:

173

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 élaborer - la raison pour laquelle ces expressions dans votre exemple ne sont pas des constantes au moment de 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é.

À titre d'exemple, considérez si vous avez essayé d'utiliser DateTime.Now à la place. La valeur de DateTime.Now change chaque fois qu'elle est exécutée. 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.

Josh
la source
15
Maintenant, documentez la valeur par défaut dans <param>, car elle n'est pas visible dans la signature.
Colonel Panic
3
Je ne peux pas faire cela, j'utilise la valeur spéciale null pour autre chose.
Colonel Panic
4
@MattHickford - Ensuite, vous devrez fournir une méthode surchargée ou prendre des millisecondes comme paramètre.
Josh
19
Peut également être utilisé span = span ?? TimeSpan.FromSeconds(2.0);avec le type Nullable, dans le corps de la méthode. Ou var realSpan = span ?? TimeSpan.FromSeconds(2.0);pour obtenir une variable locale qui n'est pas nullable.
Jeppe Stig Nielsen
5
Ce que je n'aime pas, c'est que cela implique à l'utilisateur de la fonction que cette fonction "fonctionne" avec une étendue nulle. Mais ce n'est pas vrai! Null n'est pas une valeur valide pour span en ce qui concerne la logique réelle de la fonction. J'aurais aimé qu'il y ait un meilleur moyen qui ne ressemblait pas à une odeur de code ...
JoeCool
31

Mon héritage VB6 me rend 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 pourriez avoir un effet secondaire involontaire, ou vous pourriez avaler une condition exceptionnelle (par exemple, si la source de spanest une propriété ou une variable qui ne devrait pas être nulle, mais qui l'est).

Je surchargerais donc la méthode:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}
phoog
la source
1
+1 pour cette excellente technique. Les paramètres par défaut ne doivent être utilisés qu'avec les types const, vraiment. Sinon, ce n'est pas fiable.
Lazlo le
2
C'est l'approche `` respectée par le temps '' que les valeurs par défaut ont remplacée, et pour cette situation, je pense que c'est la réponse la moins laide;) En soi, cela ne fonctionne pas nécessairement si bien pour les interfaces, car vous voulez vraiment la valeur par défaut dans une place. Dans ce cas, j'ai trouvé que les méthodes d'extension sont un outil utile: l'interface a une méthode avec tous les paramètres, puis une série de méthodes d'extension déclarées dans une classe statique à côté de l'interface implémentent les valeurs par défaut dans diverses surcharges.
OlduwanSteve
23

Cela fonctionne bien:

void Foo(TimeSpan span = default(TimeSpan))

Elena Lavrinenko
la source
4
Bienvenue dans Stack Overflow. Votre réponse semble être que vous pouvez fournir une valeur de paramètre par défaut, à condition que ce soit la seule valeur très spécifique autorisée par le compilateur. Ai-je bien compris? (Vous pouvez modifier votre réponse pour clarifier.) Ce serait une meilleure réponse si elle montrait comment tirer parti de ce que le compilateur permet pour obtenir ce que la question recherchait à l'origine, qui devait avoir d' autres TimeSpan valeurs arbitraires , comme celle donnée par new TimeSpan(2000).
Rob Kennedy
2
Une alternative qui utilise une valeur par défaut spécifique serait d'utiliser une lecture seule statique privée TimeSpan defaultTimespan = Timespan.FromSeconds (2) combiné avec le constructeur par défaut et le constructeur prenant une période. public Foo (): this (defaultTimespan) et public Foo (Timespan ts)
johan mårtensson
15

L'ensemble des valeurs qui peuvent être utilisées comme valeur par défaut sont les mêmes que celles qui peuvent être utilisées pour un argument d'attribut. La raison en est que les valeurs par défaut sont encodées en métadonnées à l'intérieur du DefaultParameterValueAttribute.

Quant à savoir pourquoi il ne peut pas être déterminé au moment de la compilation. L'ensemble de valeurs et d'expressions sur ces valeurs autorisées au moment de la compilation est répertorié dans les spécifications officielles du langage C # :

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

Les types de paramètres positionnels et nommés pour une classe d'attributs sont limités aux types de paramètres d'attribut , qui sont:

  • 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 ait une accessibilité publique et les types dans lesquels il est imbriqué (le cas échéant) ont également une accessibilité publique)
  • Tableaux unidimensionnels des types ci-dessus.

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

JaredPar
la source
2
Légère sélection: appeler une méthode statique ne rentre dans aucune de la liste. TimeSpanpeut contenir le dernier de cette liste default(TimeSpan)est valide.
CodesInChaos
12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

fourni default(TimeSpan)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); 
}

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

Ou

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

Cela devrait être mieux vu que les chances que la nullvaleur soit une valeur valide pour la fonction sont rares.

nawfal
la source
4

TimeSpanest un cas particulier pour le DefaultValueAttributeet est spécifié à l'aide de toute chaîne pouvant être analysée via la TimeSpan.Parseméthode.

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

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 graduations.

tymtam
la source
2

D'autres réponses ont donné de grandes explications sur les raisons pour lesquelles un paramètre facultatif ne peut pas être une expression dynamique. Mais, pour raconter, 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. Il y a des gens qui veulent que C # ajoute la prise en charge du compilateur évaluant les expressions dynamiques lorsqu'il rencontre des déclarations constantes - ce type de fonctionnalité serait lié au marquage des méthodes «pures», mais ce n'est pas une réalité pour le moment 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 publiquement accessibles en écriture. Remplacez ensuite toutes les options par les valeurs par défaut de votre méthode par un objet de ce type. Rendez même cet objet facultatif en spécifiant une valeur par défaut de nullfor it. 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 attribuer 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

Il s'agit d'une approche très lourde pour résoudre ce problème. Si vous écrivez une interface interne rapide et sale et que vous TimeSpanrendez la valeur Nullable et que vous la traitez comme la valeur par défaut souhaitée, cela fonctionnerait bien, faites-le à la place.

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

Avantages

Comme je l'ai mentionné dans le commentaire de l'exemple, je pense que ce modèle est idéal pour les API publiques. L'ajout de nouvelles propriétés à une classe est un changement ABI sans rupture, vous pouvez donc ajouter de nouveaux paramètres facultatifs sans changer la signature de votre méthode à l'aide de ce modèle - donnant plus d'options au code compilé plus récemment tout en continuant à prendre en charge l'ancien code compilé 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 intégrés au site d'appel, les paramètres par défaut ne seront utilisés par le code qu'une fois recompilé. En instanciant un objet de paramètres, l'appelant charge dynamiquement les valeurs par défaut lors de l'appel de 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 voir les nouvelles valeurs, si cela est souhaité.

binki
la source