Czy należy deklarować metody przy użyciu przeciążeń lub parametrów opcjonalnych w języku C # 4.0?

94

Oglądałem rozmowę Andersa o C # 4.0 i zapowiedź C # 5.0 i pomyślałem o tym, kiedy parametry opcjonalne są dostępne w C #, jaki będzie zalecany sposób deklarowania metod, które nie wymagają określenia wszystkich parametrów?

Na przykład coś takiego jak FileStreamklasa ma około piętnastu różnych konstruktorów, które można podzielić na logiczne „rodziny”, np. Te poniżej ze stringa, te z an IntPtri te z a SafeFileHandle.

FileStream(string,FileMode);
FileStream(string,FileMode,FileAccess);
FileStream(string,FileMode,FileAccess,FileShare);
FileStream(string,FileMode,FileAccess,FileShare,int);
FileStream(string,FileMode,FileAccess,FileShare,int,bool);

Wydaje mi się, że ten typ wzorca można by uprościć, mając zamiast tego trzy konstruktory i używając opcjonalnych parametrów dla tych, które mogą być domyślne, co uczyniłoby różne rodziny konstruktorów bardziej odrębnymi [uwaga: wiem, że ta zmiana nie będzie wykonane w BCL, mówię hipotetycznie o tego typu sytuacjach].

Co myślisz? Czy od C # 4.0 bardziej sensowne będzie uczynienie blisko spokrewnionych grup konstruktorów i metod jedną metodą z opcjonalnymi parametrami, czy też jest dobry powód, aby trzymać się tradycyjnego mechanizmu wielokrotnego przeciążenia?

Greg Beech
źródło

Odpowiedzi:

122

Rozważyłbym następujące kwestie:

  • Czy chcesz, aby Twój kod był używany w językach, które nie obsługują parametrów opcjonalnych? Jeśli tak, rozważ uwzględnienie przeciążeń.
  • Czy masz w swoim zespole jakichś członków, którzy gwałtownie sprzeciwiają się opcjonalnym parametrom? (Czasami łatwiej jest żyć z decyzją, której nie lubisz, niż argumentować.)
  • Czy jesteś pewien, że twoje wartości domyślne nie zmienią się między kompilacjami twojego kodu, a jeśli tak, to czy Twoi rozmówcy będą z tym w porządku?

Nie sprawdzałem, jak będą działać wartości domyślne, ale zakładam, że wartości domyślne zostaną wypieczone w kodzie wywołującym, podobnie jak odwołania do constpól. Zwykle jest w porządku - zmiany wartości domyślnej są i tak dość znaczące - ale to są rzeczy, które należy wziąć pod uwagę.

Jon Skeet
źródło
21
+1 za mądrość dotyczącą pragmatyzmu: czasami łatwiej jest żyć z decyzją, której nie lubisz, niż argumentować.
legends2k
13
@romkyns: Nie, efekt przeciążeń nie jest taki sam jak w punkcie 3. W przypadku przeciążeń zapewniających wartości domyślne, wartości domyślne znajdują się w kodzie biblioteki - więc jeśli zmienisz wartość domyślną i udostępnisz nową wersję biblioteki, wywołujący będą zobacz nowe domyślne bez ponownej kompilacji. Podczas gdy w przypadku parametrów opcjonalnych trzeba by ponownie skompilować, aby „zobaczyć” nowe ustawienia domyślne. Przez większość czasu nie jest to ważne rozróżnienie, ale jest to rozróżnienie.
Jon Skeet
cześć @JonSkeet, chciałbym wiedzieć, czy używamy zarówno funkcji ie z opcjonalnym parametrem, jak i innych z przeciążeniem, która metoda zostanie wywołana? np. Add (int a, int b) i Add (int a, int b, int c = 0) i wywołanie funkcji powiedz: Add (5,10); która metoda będzie nazywana funkcją przeciążoną lub opcjonalną funkcją parametru? dzięki :)
SHEKHAR SHETE
@Shekshar: Czy próbowałeś tego? Przeczytaj specyfikację, aby uzyskać szczegółowe informacje, ale w zasadzie w przypadku rozstrzygania remisów wygrywa metoda, w której kompilator nie musiał wypełniać żadnych opcjonalnych parametrów.
Jon Skeet
@JonSkeet właśnie teraz próbowałem z powyższym ... przeciążenie funkcji wygrywa nad opcjonalnym parametrem :)
SHEKHAR SHETE
19

Gdy przeciążenie metody normalnie wykonuje to samo z inną liczbą argumentów, zostaną użyte wartości domyślne.

Gdy przeciążenie metody wykonuje funkcję w inny sposób na podstawie swoich parametrów, będzie nadal używane.

Użyłem opcjonalnego w moich dniach VB6 i od tamtej pory go przegapiłem, zmniejszy to wiele duplikatów komentarzy XML w C #.

cfeduke
źródło
11

Od zawsze używam Delphi z opcjonalnymi parametrami. Zamiast tego przełączyłem się na używanie przeciążeń.

Ponieważ kiedy zamierzasz stworzyć więcej przeciążeń, niezmiennie będziesz w konflikcie z opcjonalnym formularzem parametru, a następnie i tak będziesz musiał przekonwertować je na nieopcjonalne.

Podoba mi się pogląd, że ogólnie istnieje jedna super metoda, a reszta to prostsze opakowania wokół niej.

Ian Boyd
źródło
1
Bardzo się z tym zgadzam, jednak istnieje zastrzeżenie, że jeśli masz metodę, która przyjmuje wiele (3+) parametrów, które są z natury wszystkie "opcjonalne" (można je zastąpić domyślnymi), możesz skończyć z wieloma permutacjami podpis metody, aby nie mieć więcej korzyści. Rozważenia Foo(A, B, C)wymaga Foo(A), Foo(B), Foo(C), Foo(A, B), Foo(A, C), Foo(B, C).
Dan Lugg
7

Na pewno będę korzystał z funkcji parametrów opcjonalnych wersji 4.0. Pozbywa się śmiesznych ...

public void M1( string foo, string bar )
{
   // do that thang
}

public void M1( string foo )
{
  M1( foo, "bar default" ); // I have always hated this line of code specifically
}

... i umieszcza wartości dokładnie tam, gdzie dzwoniący może je zobaczyć ...

public void M1( string foo, string bar = "bar default" )
{
   // do that thang
}

O wiele prostsze i mniej podatne na błędy. Właściwie widziałem to jako błąd w przypadku przeciążenia ...

public void M1( string foo )
{
   M2( foo, "bar default" );  // oops!  I meant M1!
}

Nie grałem jeszcze z Complierem 4.0, ale nie zdziwiłbym się, gdybym się dowiedział, że Complier po prostu emituje przeciążenia.

JP Alioto
źródło
6

Parametry opcjonalne są zasadniczo fragmentem metadanych, które kierują kompilator przetwarzający wywołanie metody do wstawiania odpowiednich wartości domyślnych w miejscu wywołania. Z kolei przeciążenia zapewniają środki, za pomocą których kompilator może wybrać jedną z wielu metod, z których niektóre mogą same dostarczać wartości domyślne. Zauważ, że jeśli ktoś spróbuje wywołać metodę, która określa parametry opcjonalne z kodu napisanego w języku, który ich nie obsługuje, kompilator będzie wymagał określenia parametrów „opcjonalnych”, ale ponieważ wywołanie metody bez określenia parametru opcjonalnego jest równoważne wywołaniu go z parametrem równym wartości domyślnej, nie ma przeszkód, aby takie języki wywoływały takie metody.

Istotną konsekwencją wiązania parametrów opcjonalnych w miejscu wywołania jest to, że zostaną im przypisane wartości w oparciu o wersję kodu docelowego, która jest dostępna dla kompilatora. Jeśli zestaw Fooma metodę Boo(int)z wartością domyślną 5, a zestaw Barzawiera wywołanie Foo.Boo(), kompilator przetworzy to jako plik Foo.Boo(5). Jeśli wartość domyślna zostanie zmieniona na 6, a zestaw Foozostanie ponownie skompilowany, Barbędzie nadal wywoływać, Foo.Boo(5)chyba że lub do momentu ponownego skompilowania przy użyciu tej nowej wersji Foo. Dlatego należy unikać stosowania parametrów opcjonalnych dla rzeczy, które mogą ulec zmianie.

supercat
źródło
Re: "Dlatego należy unikać stosowania parametrów opcjonalnych dla rzeczy, które mogą się zmienić." Zgadzam się, że może to być problematyczne, jeśli zmiana pozostanie niezauważona przez kod klienta. Jednak ten sam problem występuje, gdy wartość domyślna jest ukryta wewnątrz przeciążenia metody: void Foo(int value) … void Foo() { Foo(42); }. Z zewnątrz dzwoniący nie wie, jaka wartość domyślna zostanie użyta, ani kiedy może się zmienić; należałoby w tym celu monitorować pisemną dokumentację. Domyślne wartości parametrów opcjonalnych można postrzegać jako takie: dokumentacja w kodzie jaka jest wartość domyślna.
stakx - nie publikuje już
@stakx: Jeśli przeciążenie bez parametrów prowadzi do przeciążenia z parametrem, zmiana „domyślnej” wartości tego parametru i ponowna kompilacja definicji przeciążenia zmieni wartość, której używa, nawet jeśli kod wywołujący nie zostanie ponownie skompilowany .
supercat
To prawda, ale to nie sprawia, że ​​jest to bardziej problematyczne niż alternatywa. W jednym przypadku (przeciążenie metody) wywołanie kodu nie ma nic do powiedzenia w wartości domyślnej. Może to być odpowiednie, jeśli kod wywołujący naprawdę nie dba o opcjonalny parametr i co on oznacza. W drugim przypadku (opcjonalny parametr z wartością domyślną), poprzednio skompilowany kod wywołujący nie ulegnie zmianie, gdy wartość domyślna ulegnie zmianie. Może to być również odpowiednie, gdy kod wywołujący faktycznie dba o parametr; Pominięcie go w kodzie źródłowym jest jak powiedzenie „aktualnie sugerowana wartość domyślna jest dla mnie OK”.
stakx - nie publikuje już
Chodzi mi o to, że chociaż oba podejścia mają konsekwencje (jak wskazałeś), nie są one z natury korzystne ani niekorzystne. Zależy to również od potrzeb i celów kodu wywołującego. Z tego punktu widzenia werdykt w ostatnim zdaniu Twojej odpowiedzi wydał mi się nieco zbyt sztywny.
stakx - nie publikuje już
@stakx: Powiedziałem raczej „unikaj używania” niż „nigdy nie używaj”. Jeśli zmiana X będzie oznaczać, że następna rekompilacja Y zmieni zachowanie Y, co będzie wymagało skonfigurowania systemu kompilacji w taki sposób, aby każda rekompilacja X również rekompilowała Y (spowolnienie), lub stworzy ryzyko, że programista zmieni X w sposób, który złamie Y przy następnej kompilacji i odkryje takie uszkodzenie dopiero później, kiedy Y zostanie zmienione z zupełnie niezwiązanego powodu. Parametry domyślne należy stosować tylko wtedy, gdy ich zalety przewyższają takie koszty.
supercat
4

Można się spierać, czy argumenty opcjonalne lub przeciążenia powinny być używane, czy nie, ale co najważniejsze, każdy z nich ma swój własny obszar, w którym są niezastąpione.

Argumenty opcjonalne, używane w połączeniu z nazwanymi argumentami, są niezwykle przydatne w połączeniu z niektórymi listami z długimi argumentami ze wszystkimi opcjami wywołań COM.

Przeciążenia są niezwykle przydatne, gdy metoda jest w stanie operować na wielu różnych typach argumentów (tylko jeden z przykładów) i na przykład wykonuje rzutowania wewnętrznie; po prostu zasilasz go dowolnym typem danych, który ma sens (który jest akceptowany przez jakieś istniejące przeciążenie). Nie można tego przebić opcjonalnymi argumentami.

pan b
źródło
3

Nie mogę się doczekać opcjonalnych parametrów, ponieważ zachowuje wartości domyślne bliższe metodzie. Zatem zamiast dziesiątek wierszy dla przeciążeń, które po prostu wywołują metodę „rozwiniętą”, wystarczy zdefiniować metodę raz i zobaczyć, jakie parametry opcjonalne są domyślnie ustawione w sygnaturze metody. Wolałbym spojrzeć na:

public Rectangle (Point start = Point.Zero, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

Zamiast tego:

public Rectangle (Point start, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

public Rectangle (int width, int height) :
    this (Point.Zero, width, height)
{
}

Oczywiście ten przykład jest naprawdę prosty, ale w przypadku OP z 5 przeciążeniami, rzeczy mogą być naprawdę szybko zatłoczone.

Mark A. Nicolosi
źródło
7
Słyszałem, że opcjonalne parametry powinny być ostatnie, prawda?
Ilya Ryzhenkov
Zależy od Twojego projektu. Być może argument „start” jest zwykle ważny, chyba że tak nie jest. Być może masz ten sam podpis gdzie indziej, co oznacza coś innego. Dla wymyślonego przykładu, public Rectangle (int width, int height, Point innerSquareStart, Point innerSquareEnd) {}
Robert P
13
Z tego, co powiedzieli w rozmowie, parametry opcjonalne muszą znajdować się po wymaganych parametrach.
Greg Beech
3

Jednym z moich ulubionych aspektów parametrów opcjonalnych jest to, że widzisz, co stanie się z parametrami, jeśli ich nie podasz, nawet bez przechodzenia do definicji metody. Program Visual Studio po prostu pokaże domyślną wartość parametru po wpisaniu nazwy metody. W przypadku metody przeciążenia utkniesz z czytaniem dokumentacji (jeśli jest nawet dostępna) lub z bezpośrednim przejściem do definicji metody (jeśli jest dostępna) i metody, którą otacza przeciążenie.

W szczególności: wysiłek związany z dokumentacją może szybko wzrosnąć wraz z ilością przeciążeń i prawdopodobnie skończy się to kopiowaniem już istniejących komentarzy z istniejących przeciążeń. Jest to dość denerwujące, ponieważ nie daje żadnej wartości i łamie zasadę DRY ). Z drugiej strony, z opcjonalnym parametrem jest dokładnie jedno miejsce, w którym wszystkie parametry są udokumentowane i podczas pisania można zobaczyć ich znaczenie, a także wartości domyślne .

Last but not least, jeśli jesteś konsumentem API, możesz nawet nie mieć możliwości sprawdzenia szczegółów implementacji (jeśli nie masz kodu źródłowego) i dlatego nie masz szansy sprawdzić, do której super metody są przeciążone zawijają się. Dlatego utkniesz z czytaniem dokumentu i masz nadzieję, że wszystkie wartości domyślne są tam wymienione, ale nie zawsze tak jest.

Oczywiście nie jest to odpowiedź, która dotyczy wszystkich aspektów, ale myślę, że dodaje taką, która nie została do tej pory omówiona.

HimBromBeere
źródło
1

Chociaż są to (podobno?) Dwa koncepcyjnie równoważne sposoby modelowania interfejsu API od zera, niestety mają one subtelną różnicę, gdy trzeba rozważyć wsteczną kompatybilność środowiska wykonawczego dla starych klientów w środowisku naturalnym. Mój kolega (dzięki Brent!) Wskazał mi ten wspaniały post: Problemy z wersjonowaniem z opcjonalnymi argumentami . Kilka cytatów z tego:

Powodem, dla którego opcjonalne parametry zostały wprowadzone do C # 4 w pierwszej kolejności, była obsługa współpracy COM. Otóż ​​to. A teraz dowiadujemy się o pełnych konsekwencjach tego faktu. Jeśli masz metodę z opcjonalnymi parametrami, nigdy nie możesz dodać przeciążenia z dodatkowymi opcjonalnymi parametrami ze strachu przed spowodowaniem zmiany powodującej przerwanie kompilacji. I nigdy nie można usunąć istniejącego przeciążenia, ponieważ zawsze była to przełomowa zmiana w środowisku wykonawczym. Prawie trzeba traktować to jak interfejs. Jedynym rozwiązaniem w tym przypadku jest napisanie nowej metody z nową nazwą. Dlatego pamiętaj o tym, jeśli planujesz używać opcjonalnych argumentów w swoich interfejsach API.

RayLuo
źródło
1

Jedynym zastrzeżeniem opcjonalnych parametrów jest wersjonowanie, w przypadku którego refaktor ma niezamierzone konsekwencje. Przykład:

Kod początkowy

public string HandleError(string message, bool silent=true, bool isCritical=true)
{
  ...
}

Załóżmy, że jest to jedna z wielu wywołań powyższej metody:

HandleError("Disk is full", false);

Tutaj wydarzenie nie jest ciche i traktowane jako krytyczne.

Powiedzmy teraz, że po refaktorze stwierdzamy, że wszystkie błędy i tak podpowiadają użytkownikowi, więc nie potrzebujemy już flagi milczenia. Więc go usuwamy.

Po refaktoryzacji

Poprzednie wywołanie nadal się kompiluje i powiedzmy, że przechodzi przez refaktor bez zmian:

public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
  ...
}

...

// Some other distant code file:
HandleError("Disk is full", false);

Teraz falsebędzie miało niezamierzony efekt, wydarzenie nie będzie już traktowane jako krytyczne.

Może to spowodować subtelną wadę, ponieważ nie wystąpi błąd kompilacji lub wykonania (w przeciwieństwie do innych zastrzeżeń dotyczących opcji, takich jak ta lub ta ).

Zauważ, że istnieje wiele form tego samego problemu. Przedstawiono tu jeszcze jedną formę .

Należy również zauważyć, że ściśle pomocą nazwanych parametrów przy wywołaniu metoda pozwoli uniknąć tego problemu, takich jak tak: HandleError("Disk is full", silent:false). Jednak założenie, że wszyscy inni programiści (lub użytkownicy publicznego interfejsu API) będą to robić, może nie być praktyczne.

Z tych powodów unikałbym używania parametrów opcjonalnych w publicznym API (lub nawet metody publicznej, jeśli mogłaby być szeroko stosowana), chyba że istnieją inne ważne względy.

Zach
źródło
0

Oba parametry opcjonalne, Przeciążenie metody, mają swoje zalety lub wady. To zależy od twoich preferencji wyboru między nimi.

Parametr opcjonalny: dostępny tylko w .Net 4.0. opcjonalny parametr zmniejsza rozmiar kodu. Nie możesz zdefiniować parametru out i ref

przeciążone metody: możesz zdefiniować parametry wyjściowe i ref. Rozmiar kodu wzrośnie, ale przeciążone metody są łatwe do zrozumienia.

sushil pandey
źródło
0

W wielu przypadkach do przełączania wykonania używane są parametry opcjonalne. Na przykład:

decimal GetPrice(string productName, decimal discountPercentage = 0)
{

    decimal basePrice = CalculateBasePrice(productName);

    if (discountPercentage > 0)
        return basePrice * (1 - discountPercentage / 100);
    else
        return basePrice;
}

Parametr rabatu jest tutaj używany do podawania instrukcji if-then-else. Istnieje polimorfizm, który nie został rozpoznany, a następnie został zaimplementowany jako instrukcja jeśli-to-inaczej. W takich przypadkach znacznie lepiej jest podzielić dwa przepływy sterowania na dwie niezależne metody:

decimal GetPrice(string productName)
{
    decimal basePrice = CalculateBasePrice(productName);
    return basePrice;
}

decimal GetPrice(string productName, decimal discountPercentage)
{

    if (discountPercentage <= 0)
        throw new ArgumentException();

    decimal basePrice = GetPrice(productName);

    decimal discountedPrice = basePrice * (1 - discountPercentage / 100);

    return discountedPrice;

}

W ten sposób zabezpieczyliśmy nawet klasę przed odebraniem połączenia z zerową zniżką. To wywołanie oznaczałoby, że dzwoniący uważa, że ​​istnieje zniżka, ale w rzeczywistości nie ma jej wcale. Takie nieporozumienie może łatwo spowodować błąd.

W takich przypadkach wolę nie mieć parametrów opcjonalnych, ale wymusić na dzwoniącym jawne wybranie scenariusza wykonania, który pasuje do jego bieżącej sytuacji.

Sytuacja jest bardzo podobna do sytuacji, gdy parametry mogą być zerowe. To równie zły pomysł, gdy implementacja sprowadza się do takich stwierdzeń jak if (x == null).

Szczegółową analizę można znaleźć na tych łączach: Unikanie parametrów opcjonalnych i Unikanie parametrów zerowych

Zoran Horvat
źródło
0

Aby dodać bez myślenia, kiedy używać przeciążenia zamiast opcji:

Jeśli masz kilka parametrów, które mają sens tylko razem, nie wprowadzaj do nich opcji.

Lub bardziej ogólnie, gdy sygnatury metod włączają wzorce użycia, które nie mają sensu, ogranicz liczbę permutacji możliwych wywołań. Na przykład, używając przeciążeń zamiast opcji (ta reguła obowiązuje również, gdy masz kilka parametrów tego samego typu danych, nawiasem mówiąc, tutaj mogą pomóc urządzenia takie jak metody fabryczne lub niestandardowe typy danych).

Przykład:

enum Match {
    Regex,
    Wildcard,
    ContainsString,
}

// Don't: This way, Enumerate() can be called in a way
//         which does not make sense:
IEnumerable<string> Enumerate(string searchPattern = null,
                              Match match = Match.Regex,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);

// Better: Provide only overloads which cannot be mis-used:
IEnumerable<string> Enumerate(SearchOption searchOption = SearchOption.TopDirectoryOnly);
IEnumerable<string> Enumerate(string searchPattern, Match match,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);
Sebastian Mach
źródło