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 FileStream
klasa 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 IntPtr
i 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?
źródło
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 #.
źródło
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.
źródło
Foo(A, B, C)
wymagaFoo(A)
,Foo(B)
,Foo(C)
,Foo(A, B)
,Foo(A, C)
,Foo(B, C)
.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.
źródło
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
Foo
ma metodęBoo(int)
z wartością domyślną 5, a zestawBar
zawiera wywołanieFoo.Boo()
, kompilator przetworzy to jako plikFoo.Boo(5)
. Jeśli wartość domyślna zostanie zmieniona na 6, a zestawFoo
zostanie ponownie skompilowany,Bar
będzie nadal wywoływać,Foo.Boo(5)
chyba że lub do momentu ponownego skompilowania przy użyciu tej nowej wersjiFoo
. Dlatego należy unikać stosowania parametrów opcjonalnych dla rzeczy, które mogą ulec zmianie.źródło
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.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.
źródło
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.
źródło
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.
źródło
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:
źródło
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
false
bę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.
źródło
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.
źródło
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
źródło
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);
źródło