C # 4.0: Czy mogę użyć TimeSpan jako opcjonalnego parametru z wartością domyślną?

125

Oba generują błąd, który mówi, że muszą być stałą czasu kompilacji:

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

Przede wszystkim, czy ktoś może wyjaśnić, dlaczego tych wartości nie można określić w czasie kompilacji? Czy istnieje sposób na określenie wartości domyślnej dla opcjonalnego obiektu TimeSpan?

Mike Pateras
źródło
11
Nie ma związku z tym, o co pytasz, ale pamiętaj, że new TimeSpan(2000)nie oznacza to 2000 milisekund, ale 2000 „taktów”, co oznacza 0,2 milisekundy lub jedną 10.000 sekund.
Jeppe Stig Nielsen

Odpowiedzi:

172

Możesz bardzo łatwo obejść ten problem, zmieniając swój podpis.

void Foo(TimeSpan? span = null) {

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

   ...

}

Powinienem to wyjaśnić - powodem, dla którego te wyrażenia w twoim przykładzie nie są stałe czasu kompilacji, jest to, że w czasie kompilacji kompilator nie może po prostu wykonać TimeSpan.FromSeconds (2.0) i umieścić bajtów wyniku w skompilowanym kodzie.

Jako przykład rozważ, czy zamiast tego próbowałeś użyć DateTime.Now. Wartość DateTime.Now zmienia się za każdym razem, gdy jest wykonywana. Albo załóżmy, że TimeSpan.FromSeconds wziął pod uwagę grawitację. To absurdalny przykład, ale reguły stałych czasu kompilacji nie tworzą specjalnych przypadków tylko dlatego, że wiemy, że TimeSpan.FromSeconds jest deterministyczny.

Josh
źródło
15
Teraz udokumentuj wartość domyślną w <param>, ponieważ nie jest ona widoczna w podpisie.
Panika pułkownika
3
Nie mogę tego zrobić, używam specjalnej wartości null do czegoś innego.
Panika pułkownika
4
@MattHickford - Następnie musisz podać przeciążoną metodę lub przyjąć milisekundy jako parametr.
Josh
19
Można również użyć span = span ?? TimeSpan.FromSeconds(2.0);z typem dopuszczającym wartość null w treści metody. Lub var realSpan = span ?? TimeSpan.FromSeconds(2.0);aby uzyskać zmienną lokalną, która nie dopuszcza wartości null.
Jeppe Stig Nielsen
5
Nie podoba mi się to, że sugeruje to użytkownikowi funkcji, że ta funkcja „działa” z zerowym zakresem. Ale to nieprawda! Null nie jest prawidłową wartością zakresu, jeśli chodzi o rzeczywistą logikę funkcji. Żałuję, że nie ma lepszego sposobu, który nie wyglądałby jak zapach kodu ...
JoeCool,
31

Moje dziedzictwo VB6 niepokoi mnie pomysł traktowania „wartości zerowej” i „wartości brakującej” jako równoważnych. W większości przypadków prawdopodobnie jest w porządku, ale możesz mieć niezamierzony efekt uboczny lub możesz połknąć wyjątkowy warunek (na przykład, jeśli źródłem spanjest właściwość lub zmienna, która nie powinna mieć wartości null, ale jest).

Dlatego przeładowałbym metodę:

void Foo()
{
    Foo(TimeSpan.FromSeconds(2.0));
}
void Foo(TimeSpan span)
{
    //...
}
phoog
źródło
1
+1 za tę wspaniałą technikę. Parametry domyślne powinny być używane tylko z typami const. W przeciwnym razie jest zawodne.
Lazlo,
2
Jest to podejście `` uświęcone czasem '', które zastąpiono wartościami domyślnymi, i myślę, że w tej sytuacji jest to najmniej brzydka odpowiedź;) Samo w sobie niekoniecznie jednak działa tak dobrze w przypadku interfejsów, ponieważ naprawdę chcesz mieć wartość domyślną w jedno miejsce. W tym przypadku przydatnym narzędziem okazały się metody rozszerzające: interfejs ma jedną metodę ze wszystkimi parametrami, a następnie szereg metod rozszerzających zadeklarowanych w klasie statycznej wraz z interfejsem implementuje wartości domyślne w różnych przeciążeniach.
OlduwanSteve
23

To działa dobrze:

void Foo(TimeSpan span = default(TimeSpan))

Elena Lavrinenko
źródło
4
Witamy w Stack Overflow. Twoja odpowiedź wydaje się być taka, że możesz podać domyślną wartość parametru, o ile jest to bardzo konkretna wartość, na którą zezwala kompilator. Czy dobrze to zrozumiałem? (Możesz edytować swoją odpowiedź, aby wyjaśnić.) Byłaby to lepsza odpowiedź, gdyby pokazywała, jak wykorzystać to, co pozwala kompilatorowi dotrzeć do tego, czego pierwotnie szukało, czyli mieć inne dowolne TimeSpanwartości, takie jak podane przez new TimeSpan(2000).
Rob Kennedy,
2
Alternatywą, która używa określonej wartości domyślnej, byłoby użycie prywatnego statycznego tylko do odczytu TimeSpan defaultTimespan = Timespan.FromSeconds (2) w połączeniu z domyślnym konstruktorem i konstruktorem przyjmującym przedział czasu. public Foo (): this (defaultTimespan) and public Foo (Timespan ts)
johan mårtensson
15

Zestaw wartości, które mogą być używane jako wartość domyślna, jest taki sam, jak może być używany dla argumentu atrybutu. Powodem jest to, że wartości domyślne są kodowane w metadanych wewnątrz DefaultParameterValueAttribute.

Dlaczego nie można tego określić w czasie kompilacji. Zestaw wartości i wyrażeń przekraczających takie wartości dozwolone w czasie kompilacji jest wymieniony w oficjalnej specyfikacji języka C # :

C # 6.0 - typy parametrów atrybutów :

Typy parametrów pozycyjnych i nazwanych dla klasy atrybutów są ograniczone do typów parametrów atrybutów , którymi są:

  • Jeden z następujących typów: bool, byte, char, double, float, int, long, sbyte, short, string, uint, ulong, ushort.
  • Typ object.
  • Typ System.Type.
  • Typ wyliczenia.
    (pod warunkiem, że ma dostępność publiczną, a typy, w których jest zagnieżdżony (jeśli istnieją), również mają dostępność publiczną)
  • Tablice jednowymiarowe powyższych typów.

Typ TimeSpannie pasuje do żadnej z tych list i dlatego nie może być używany jako stała.

JaredPar
źródło
2
Nieznaczny wybór: wywołanie metody statycznej nie pasuje do żadnej z listy. TimeSpanpasuje do ostatniego na tej liście default(TimeSpan)jest ważny.
CodesInChaos
12
void Foo(TimeSpan span = default(TimeSpan))
{
    if (span == default(TimeSpan)) 
        span = TimeSpan.FromSeconds(2); 
}

podana default(TimeSpan)nie jest prawidłową wartością funkcji.

Lub

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

podana new TimeSpan()nie jest prawidłową wartością.

Lub

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

Powinno to być lepsze, biorąc pod uwagę, że szanse, że nullwartość będzie prawidłową wartością dla funkcji, są rzadkie.

nawfal
źródło
4

TimeSpanjest specjalnym przypadkiem dla DefaultValueAttributei jest określany przy użyciu dowolnego łańcucha, który można przeanalizować za pomocą TimeSpan.Parsemetody.

[DefaultValue("0:10:0")]
public TimeSpan Duration { get; set; }
dahall
źródło
3

Moja sugestia:

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

    //...
}

BTW TimeSpan.FromSeconds(2.0)nie równa się new TimeSpan(2000)- konstruktor bierze tiki.

tymtam
źródło
2

Inne odpowiedzi dały świetne wyjaśnienia, dlaczego opcjonalny parametr nie może być wyrażeniem dynamicznym. Ale żeby przypomnieć, parametry domyślne zachowują się jak stałe czasu kompilacji. Oznacza to, że kompilator musi być w stanie je ocenić i udzielić odpowiedzi. Są ludzie, którzy chcą, aby C # dodawał obsługę kompilatora oceniającego dynamiczne wyrażenia podczas napotykania stałych deklaracji - ten rodzaj funkcji byłby powiązany z oznaczaniem metod jako „czystych”, ale to nie jest obecnie rzeczywistością i może nigdy nie być.

Jedną z alternatyw dla użycia domyślnego parametru C # dla takiej metody byłoby użycie wzorca, którego przykładem jest XmlReaderSettings. W tym wzorcu zdefiniuj klasę z konstruktorem bez parametrów i publicznie zapisywalnymi właściwościami. Następnie zamień wszystkie opcje na wartości domyślne w swojej metodzie na obiekt tego typu. Nawet ustaw ten obiekt jako opcjonalny, określając nulldla niego wartość domyślną . Na przykład:

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

Aby wywołać, użyj tej dziwnej składni do tworzenia wystąpienia i przypisywania właściwości w jednym wyrażeniu:

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

Wady

To naprawdę ciężkie podejście do rozwiązania tego problemu. Jeśli piszesz szybki i brudny interfejsu wewnętrznego i dokonywania TimeSpanzerowalne i leczenia NULL jak żądanej wartości domyślnej będzie działać dobrze, że zamiast robić.

Ponadto, jeśli masz dużą liczbę parametrów lub wywołujesz metodę w ścisłej pętli, będzie to miało narzut związany z tworzeniem instancji klas. Oczywiście wywołanie takiej metody w ciasnej pętli może być naturalne, a nawet bardzo łatwe do ponownego wykorzystania instancji FooSettingsobiektu.

Korzyści

Jak wspomniałem w komentarzu w przykładzie, myślę, że ten wzorzec jest świetny dla publicznych interfejsów API. Dodanie nowych właściwości do klasy jest niezepsutą zmianą ABI, więc możesz dodawać nowe parametry opcjonalne bez zmiany podpisu metody przy użyciu tego wzorca - dając ostatnio skompilowanemu kodowi więcej opcji, jednocześnie kontynuując obsługę starego skompilowanego kodu bez dodatkowej pracy .

Ponadto, ponieważ wbudowane w C # domyślne parametry metod są traktowane jako stałe kompilacji i umieszczane na stronie wywołania, parametry domyślne będą używane przez kod dopiero po ponownej kompilacji. Tworząc wystąpienie obiektu ustawień, obiekt wywołujący dynamicznie ładuje wartości domyślne podczas wywoływania metody. Oznacza to, że możesz zaktualizować ustawienia domyślne, po prostu zmieniając klasę ustawień. Dlatego ten wzorzec umożliwia zmianę wartości domyślnych bez konieczności ponownej kompilacji wywołań w celu wyświetlenia nowych wartości, jeśli jest to wymagane.

binki
źródło