C# 4.0: Puis-je utiliser un TimeSpan comme paramètre optionnel avec une valeur par défaut?
tous deux génèrent une erreur en disant qu'ils doivent être une constante de temps 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 y a-t-il un moyen de spécifier une valeur par défaut pour un objet optionnel TimeSpan?
8 réponses
vous pouvez contourner cela très facilement en changeant 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 de temps de compilation est qu'au temps de compilation, le compilateur ne peut pas simplement exécuter TimeSpan.Fromsecondes (2.0) et coller les octets du résultat dans votre code compilé.
par exemple, pensez à essayer D'utiliser DateTime.Maintenant à la place. La valeur de DateTime.Maintenant change chaque temps il est exécuté. Ou supposez cette durée.Fromsecondes a pris en compte la gravité. C'est un exemple absurde mais les règles des constantes de temps de compilation ne font pas de cas spéciaux juste parce que nous connaissons cette durée.Fromsecondes est déterministe.
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 non intentionnel, ou vous pourriez avaler une condition exceptionnelle (par exemple, si la source de span
est une propriété ou une variable qui ne devrait pas être nulle, mais est).
Je surchargerais donc la méthode:
void Foo()
{
Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
//...
}
Cela fonctionne très bien:
void Foo(TimeSpan span = default(TimeSpan))
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 dans des métadonnées à l'intérieur du DefaultParameterValueAttribute
.
pourquoi il ne peut pas être déterminé au moment de la compilation. L'ensemble des valeurs et expressions sur ces valeurs autorisées au moment de la compilation est répertorié dans la liste officielle C# language spec :
C# 6.0-attribut parameter types :
les types de paramètres de position et nommés pour une classe d'attribut 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
.- de type enum.
(à condition qu'elle soit accessible au public et que les types dans lesquels elle est imbriquée (s'il y en a) soient également accessibles au public)- matrices unidimensionnelles des types ci-dessus.
le tapez TimeSpan
ne rentre dans aucune de ces listes et ne peut donc pas être utilisé comme une constante.
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 compte tenu des chances de null
valeur étant une valeur valide pour la fonction sont rares.
TimeSpan
est un cas particulier pour le DefaultValueAttribute
et est spécifié en utilisant n'importe quelle chaîne de caractères qui peut être parée via la méthode TimeSpan.Parse
.
[DefaultValue("0:10:0")]
public TimeSpan Duration { get; set; }
Autres réponses ont donné de grandes explications pourquoi une option de paramètre 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 en mesure de les évaluer et de trouver une réponse. Il y a des gens qui veulent que C# ajoute du support pour le compilateur évaluant les expressions dynamiques lors de la rencontre avec des déclarations constantes-ce genre de fonctionnalité serait lié aux méthodes de marquage "pure", mais ce n'est pas une réalité maintenant, et pourraient 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ètres et des propriétés accessibles au public. Ensuite, remplacez toutes les options par défaut dans votre méthode par un objet de ce type. Même rendre cet objet optionnel en spécifiant un défaut de null
pour il. 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 assigner des propriétés toutes dans une 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 vraiment lourde pour résoudre ce problème. Si vous écrivez une interface interne rapide et sale et rendant le TimeSpan
null et traitant null comme votre valeur par défaut désirée fonctionnerait très bien, faire que plutôt.
en outre, si vous avez un grand nombre de paramètres ou appelez la méthode dans une boucle serrée, cela aura le dessus des instanciations de classe. Bien sûr, si on appelle une telle méthode en boucle serrée, il peut être naturel et même très facile de réutiliser une instance de l'objet FooSettings
.
prestations
comme je l'ai mentionné dans le commentaire dans l'exemple, je pense que ce modèle est excellent pour les API publiques. L'ajout de nouveaux propriétés à une classe est un changement ABI non-cassant, de sorte que vous pouvez ajouter de nouveaux paramètres optionnels sans changer la signature de votre méthode en utilisant ce modèle-donnant plus de code récemment compilé plus d'options tout en continuant à soutenir l'ancien code compilé sans travail supplémentaire.
aussi, parce que les paramètres de la méthode par défaut de C#sont traités comme des constantes de temps compilées et cuits dans le callsite, les paramètres par défaut ne seront utilisés par le code qu'une fois recompilé. Par en instanciant un objet settings, 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 changeant simplement votre classe de paramètres. Ainsi, Ce modèle vous permet de changer les valeurs par défaut sans avoir à recompiler les appelants pour voir les nouvelles valeurs, si cela est désiré.
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 tiques.