Posts tagged ‘parameters’

Czytelne wywołania funkcji

2011-05-23 21:22

Ktokolwiek, kto programował dłużej w Windows API zna bardzo dobrze klasyczną sekwencję instrukcji, składającą się na zarejestrowanie nowej klasy okna i jego utworzenie. Jej częścią jest między innymi wywołanie funkcji CreateWindow lub CreateWindowEx, które przyjmują odpowiednio 11 lub 12 parametrów. Mimo że nie są one rekordzistkami pod tym względem (bije je chociażby CreateFont z 14 argumentami), to i tak mogą się “poszczycić” dużym potencjałem w zaciemniania kodu i czynienia go trudnym w zrozumieniu lub modyfikacji.
Niestety, takie lub nieco mniej drastyczne przypadki można spotkać w wielu językach, platformach i technologiach. Odchodzą one daleko od rozsądnego zalecenia, aby liczba parametrów funkcji nie przekraczała dwóch lub trzech, z ewentualnym uwzględnieniem this/self/Me. Jak sobie z nimi radzić, aby wynikowy kod zawierający tak rozrośnięte wywołania był jeszcze w jakikolwiek sposób czytelny?…

Otóż należy postarać się, aby każdy z wielu argumentów był identyfikowalny czymś więcej niż tylko pozycją w ciągu oddzielonym przecinkami. Dobrze tutaj sprawdza się feature niektórych języków programowania zwany argumentami słownikowymi. Umożliwia on “przypisywanie” w wywołaniu wartości parametrów do ich nazw. Pozwala to na zmianę ich kolejności, ale przede wszystkim dodaje czytelną etykietę dla każdego argumentu. Przykład takiego słownikowego wywołania w Pythonie widać poniżej:

  1. # posortowanie listy łańcuchów od najdłuższego
  2. sorted(strings, key = len, reverse = True)

Teoretycznie podobny efekt można osiągnąć także w językach nieposiadających wspomnianej opcji. Podejrzewam zresztą, że sposób ten jest pierwszym, jaki większości przyszedł do głowy. Chodzi tu o zwyczajne opatrzenie każdego argumentu odpowiednim komentarzem. Wiele przykładów tak właśnie traktuje argumenty wspomnianej funkcji CreateWindow(Ex):

  1. hWindow = CreateWindowEx(NULL,                   // rozszerzony styl
  2.                           windowClass.c_str(), // klasa okna
  3.                           "My Window",       // tekst na p. tytułu
  4.                           WS_OVERLAPPEDWINDOW,   // styl okna
  5.                           20,         // współrzędna X
  6.                           20,         // współrzędna Y
  7.                           600,         // szerokość
  8.                           500,         // wysokość
  9.                           NULL,                  // okno nadrzędne
  10.                           NULL,                  // menu
  11.                           hInstance,             // instancja aplikacji
  12.                           NULL);                 // dodatkowe dane

Ale rzeczywisty kod to nie przykład z tutoriala, a nadmiar kolorowych komentarzy niekoniecznie musi dobrze wpływać na przejrzystość całej instrukcji. W dodatku wciąż jesteśmy skazani na domyślną kolejność parametrów, a wszelkie rozbieżności między argumentami a ich opisem (bardzo mylące!) nie są wykrywane przez kompilator…

Co można zatem zrobić? Odpowiedź jest prosta: należy napisać kod, który sam się dokumentuje ;-) A rozwijając tę myśl do czegoś bardziej konkretnego: powinniśmy zauważyć, że absolutnie każdy język posiada możliwość opisywania nie tylko parametrów funkcji, ale ogóle jakichkolwiek wyrażeń. Nazywa się to… dokładnie tak – deklaracją zmiennych:

  1. const char* windowTitle = "My Window";
  2. POINT pos = { 20, 20 };
  3. SIZE size = { 600, 500 };
  4. DWORD style = WS_OVERLAPPEDWINDOW;
  5. hWindow = CreateWindowEx(NULL, windowClass.c_str(), windowTitle,
  6.        style, pos.x, pos.y, size.cx, size.cy,
  7.        NULL, NULL, hInstance, NULL);

Przy takim rozwiązaniu niepotrzebne są już żadne dodatkowe wyjaśnienia, bo wszystko widać tu doskonale. Wywołanie stało się czytelne, bo każdy z parametrów jest po prostu swoją nazwą lub nieistotnym NULL-em. Warto też zauważyć, że w typowym kodzie wiele z tych nazw byłoby już zdefiniowanych wcześniej, bo np. byłyby argumentami funkcji otaczającej to wszystko. Ilość dodatkowych deklaracji niekoniecznie musiałaby więc być zbliżona do długości listy parametrów wywołania.

Powyżej widać zatem, że nawet z wyjątkowo rozrośniętymi funkcjami można sobie całkiem nieźle poradzić. Nie traktujmy tego jednak jako zachęty do wydłużania list argumentów naszych własnych funkcji. Zdecydowanie lepiej jest użyć struktury (jak to robi się np. przy tworzeniu urządzenia DirevtX) czy nawet wzorca Builder bez jego abstrakcyjnej części.

Tags: , , ,
Author: Xion, posted under Programming » 6 comments

Jedna funkcja w wielu wersjach

2011-03-10 18:12

Wytykanie niedoskonałości i różnego rodzaju felerów języka C++ to jedno z ulubionych zajęć niektórych programistów, zwłaszcza tych którzy z jakichś powodów muszą z tego języka korzystać. Sam czasami to robię, ale dzisiaj chcę zwrócić uwagę na to, co C++ robi dobrze – albo przynajmniej lepiej niż w wielu innych, popularnych językach. A chodzi tu o swobodę w definiowaniu wielu wersji tej samej funkcji, różniących się listą parametrów. Nazywa się to zwykle przeciążaniem, chociaż nie jest to jedyny sposób na osiągnięcie takiego efektu.

Innym są bowiem parametry domyślne i istnieje przynajmniej jeden język, w którym jest to sposób jedyny. Tak jest bowiem w Pythonie i nawet czasami ma to sens, biorąc pod uwagę brak deklarowanych typów w tym języku. Jednak równie często wymaga to czegoś w stylu samodzielnego rozstrzygania przeciążania już wewnątrz funkcji:

  1. def some_func(string_or_list):
  2.     if isinstance(string_or_list, list): x = "".join(string_or_list)
  3.     elif isinstance(string_or_list, basestring): x = string_or_list
  4.  
  5.     # ...

Powyższe ify to nic innego jak boilerplate code: redundantne, powtarzalne fragmenty kodu, które w dodatku robią tutaj to, co w innych językach spoczywa na barkach kompilatora. Usprawiedliwieniem może być pewnie to, że przecież Python nie jest kompilowany ;P

Ale jak wspomniałem wcześniej, język spod znaku węża posiada możliwość definiowania argumentów domyślnych, za co należy mu oddać honor. Niestety nie da się tego powiedzieć o kilku innych, popularnych dziś językach, jak choćby C# i Javie. Jeśli chcemy mieć w nich dwie sygnatury tej samej metody, musimy ją przeciążyć. Otrzymujemy wtedy dwie wersje (lub więcej), z których jedna – ta posiadająca mniej argumentów – jest po prostu wywołaniem drugiej. Widuje się to często w konstruktorach, z których czasem układa się swego rodzaju sztafeta wywołań:

  1. public Foo() { this(0); }
  2. public Foo(int x) { this(x, null); }
  3. public Foo(int x, Object o) { this(x, o, ""); }
  4. public Foo(int x, Object, String s) {
  5.     // właściwy konstruktor
  6. }

Jak łatwo zauważyć, jest ona wybitnie rozwlekła, trudna w utrzymaniu i modyfikacji, a także mało czytelna, gdyż wartości domyślne argumentów są ukryte w wywołaniach zamiast w deklaracjach funkcji. Jej odpowiednik z argumentami domyślnymi byłby natomiast krótki, przejrzysty, zrozumiały i łatwy do ewentualnej zmiany.

“Problem” z tworzeniem wielu wersji funkcji polega właśnie na tym, iż mimo istnienia dwóch sposobów na osiągnięcie tego samego efektu, bardzo często jeden jest wyraźnie lepszy niż drugi. Stąd każdy język nieposiadający wsparcia dla któregoś z nich niejako z automatu kreuje sytuacje, dla których rozwiązania trzeba szukać na około.
I tu pozytywnie wyróżnia się C++, który pozwala na stosowanie obu technik (przeciążania lub argumentów domyślnych) w zależności od potrzeb. Również razem, jeśli rezultat nie powoduje niejednoznaczności w wywołaniach. Z pewnością jest to cenna cecha tego języka, równoważąca znaczącą część jego znanych niedostatków :)

Tags: , , , , ,
Author: Xion, posted under Programming » 4 comments

Triki z PowerShellem #16 – Parametry

2010-08-22 14:44

Pisząc mniej lub bardziej regularnie o PowerShellu, zdołałem zaprezentować całkiem pokaźną kolekcję różnych skryptów. Część z nich do działania wymagała podania dodatkowych danych. Jeśli są one w miarę niezmienne, można je zapisać w samym kodzie jako pseudo-stałe. Jeżeli jednak każde użycie skryptu może potencjalnie potrzebować innych wartości, wtedy najlepiej pobawić się z jego parametrami.

W PowerShellu do specyfikowania argumentów służy słowo kluczowe param. Można je stosować zarówno do samych skryptów, ale także do zawartych w nich funkcji. Tak jest – może nie wspominałem o tym wcześniej, ale w PSh jak najbardziej można definiować własne funkcje na użytek lokalny skryptu:

  1. function Fib
  2. {
  3.     param ([int]$x)
  4.     if ($x -eq 0 -or $x -eq 1)  { return 1 }
  5.     return (Fib ($x - 1)) + (Fib ($x - 2))
  6. }

W obu przypadkach (skryptów i funkcji) instrukcja param powinna znaleźć się na samym początku i wystąpić co najwyżej raz.

Składnia deklaracji parametrów jest raczej prosta i wygląda następująco:

  1. param (..., [typ]$nazwa = wartośćDomyślna, ...)

Kolejne elementy oddzielamy w niej przecinkami. Dla każdego z nich wartośćDomyślna nie jest obowiązkowa, lecz niepodanie jej nie czyni wcale argumentu obowiązkowym. Zamiast tego otrzyma on wartość “neutralną” (np. $null dla obiektów), jeśli nie zostanie przypisany w wywołaniu funkcji/skryptu. Żeby uczynić argument rzeczywiście niezbędnym, należy po prostu… rzucić wyjątek, jeśli nie został on użyty:

  1. param ([string]$name = $(throw "Name required"))

Może to się wydawać dziwne, ale nie zapominajmy, że języki powłok takich jak PowerShell są interpretowane Dlatego błąd w postaci braku wymaganego parametru (który normalnie wykryty by został podczas kompilacji) może dać o sobie znać dopiero w czasie wykonania.

Każdemu parametrowi możemy też przypisać typ, którym może być klasa z .NET-owego frameworka lub któryś z typów wbudowanych. Oczywiście nie musimy tego robić. Wtedy jednak będziemy mieli do czynienia z nieokreślonym obiektem (klasy System.Object), którego rodzaj możemy ewentualnie sprawdzić później (np. metodą GetType).
Specjalnym typem parametru jest switch, czyli przełącznik. Tworzy on parametr, któremu nie podajemy wartości, a jedynie uwzględniamy w wywołaniu (albo i nie). Jest to więc flaga podobna do Recurse czy -Verbose ze standardowych komend PowerShella lub tysięcy podobnych flag w programach i poleceniach z innych shelli. Semantycznie taki przełącznik jest potem zmienną logiczną typu bool:

  1. param ([switch]$Debug)
  2. # ...
  3. if ($Debug) { "Debug mode enabled." | Out-Host }

To mniej więcej tyle, jeśli chodzi o definiowanie parametrów dla funkcji i skryptów. Jak teraz wygląda ich przekazywanie w trakcie wywołania?… No cóż, odpowiedź jest prosta: dokładnie tak samo, jak w przypadku wbudowanych komend PowerShella. Przede wszystkim możemy podawać ich nazwę poprzedzoną myślnikiem oraz wartość:

  1. Fib -x 12 | Out-Host

Nie jest to jednak zawsze konieczne. Zamiast tego możemy zdać się na ich kolejność w deklaracji param, co pozwala ominąć podawanie ich nazw w wywołaniu:

  1. Fib 12

Pamiętajmy tylko, że skrypty (rzadziej funkcje) mogą mieć nierzadko i kilkanaście parametrów. Pomijanie ich nazw na pewno nie wpłynie wtedy pozytywnie na czytelność.

Tags: ,
Author: Xion, posted under Applications, Programming » 2 comments

Dwukierunkowe funkcje i obiekty proxy

2010-06-01 16:07

Jedną z pierwszych rzeczy poznawanych w trakcie nauki programowania jest sposób działania funkcji. Mam tu na myśli zupełnie elementarny fakt, iż funkcje przyjmują zero lub więcej argumentów na wejściu i produkują co najwyżej jeden rezultat na wyjściu. Niezachwiana pewność w tę własność funkcji może być potem mocno nadwątlona, jeśli poznamy języki o bardziej egzotycznym sposobie działania, jak na przykład Prolog; tam wszystkie parametry funkcji mogą przekazywać dane w obu kierunkach, dzięki czemu np. za dzielenie i łączenie ciągów lub list odpowiada jedna i ta sam funkcja.
Nie trzeba jednak uciekać tak daleko od starego dobrego programowania imperatywnego, żeby zaobserwować odstępstwa od reguły. Parametry mogą przecież służyć do zwracania wartości – dzieje się to przy pomocy słów kluczowych ref/out w C# lub zwykłych wskaźników/referencji w C++. Często zdarza się wtedy, że “normalny” rezultat zwracany przez funkcję służy jedynie przekazaniu informacji o ewentualnym błędzie.

Rzadsza sytuacja polega na tym, że wynik funkcji staje się jej argumentem wejściowym. Technicznie nie jest nawet możliwe, ale można tak traktować sytuacje, gdy rezultatem jest l-wartość (l-value), tj. obiekt, do którego można przypisywać:

  1. vector<int> v(10);
  2. v.at(5) = 25;

Typowo jest to referencja (tutaj int&), a powyższa konstrukcja – poprzez swojej podobieństwo do indeksowania zwykłej tablicy – nie należy do wielce zaskakujących. Co jednak można powiedzieć o tej:

  1. Dim S as String = "Ala ma kota"
  2. Mid(S, 4, 2) = "nie ma" ' Ala nie ma kota

poza widocznym od razu faktem, że pochodzi ona z języka, którego rozwlekłość składni przyprawia o niestrawność? :) Mianowicie tutaj nie ma już oczywistych przesłanek co do tego, czym jest lewa strona. Widać bowiem, że możemy jej przypisać dłuższy podciąg niż oryginalny i zostanie on mimo wszystko poprawnie zastąpiony – nie jest to więc żaden prosty “wyrób wskaźnikopodobny”. Może więc to po prostu feature akurat tego języka, którego nie da się zreplikować?…

Odpowiedź jest – jak można się domyślać, skoro o tym piszę – oczywiście negatywna :) Ażeby podobny efekt osiągnąć w C++, konieczne jest jednak zastosowanie techniki znanej jako obiekt pośredniczącyproxy. Powinien on zachowywać się jak zwykły rezultat funkcji, ale w razie potrzeby dawać również możliwość przypisywania do siebie. Naturalnie efekt takiego działania jest specyficzny dla konkretnej funkcji, która swoją drogą może być często zredukowana do samego tworzenia obiektu proxy:

  1. inline Mid_Proxy Mid(std::string& s, size_t off, size_t len)
  2.     { return Mid_Proxy(s, off, len); }

Minimum, jakie rzeczony obiekt musi zapewniać, to operator przypisania oraz jakiś operator rzutowania, który pozwoli na “wyciągnięcie” rezultatu funkcji, gdy jest ona użyta w zwykły sposób:

  1. class Mid_Proxy
  2. {
  3.     private:
  4.         std::string& s;
  5.         size_t off, len;
  6.     public:
  7.         Mid_Proxy(std::string& _s, size_t _off, size_t _len)
  8.             : s(_s), off(_off), len(_len) { }
  9.  
  10.         void operator = (const std::string& str)
  11.             { s.replace (off, len, str); }
  12.         operator std::string () const
  13.             { return s.substr(off, len); }
  14. };

Widzimy tutaj, że przy takim proxy nasza funkcja Mid użyta w zwykły sposób zachowuje się jak metoda substr. Gdy jednak umieścimy jej wywołanie po lewej stronie przypisania, zadziała metoda replace, służąca do zastępowania podciągu innym. W sumie więc poniższy kod:

  1. std::string s("Ala ma kota");
  2. Mid(s, 4, 2) = "nie ma";

będzie działał analogicznie do prezentowanego wyżej fragmentu w języku Visual Basic.

Opisana tu sztuczka nie jest oczywiście doskonała. Do pełni możliwości potrzebna jest jeszcze wersja read-only funkcji Mid, przyjmująca stałą referencję do stringa i wywołująca substr bezpośrednio, z pominięciem obiektu proxy. To jednak da się łatwo zauważyć.
Mniej widoczny jest natomiast fakt, że obiekt proxy, dodając kolejną warstwę niejawnych konwersji (tutaj: z siebie na string) może spowodować kłopoty tam, gdzie jedna niestandardowa konwersja jest już wykorzystywana. Dobry przykład to interakcja ze strumieniem:
std::cout << Mid(s, 0, 3) << std::endl;[/cpp] która nie powiedzie, bo operator strumienowy << nie posiada wersji dla lewego prawego argumentu będącego std::stringiem, a jedynie dla const char* (co swoją drogą jest dość dziwne). Rozwiązaniem jest napisanie takowego dla samego obiektu proxy.

Konstruktory bezparametrowe

2008-06-28 12:12

W C++ konstruktor domyślny jest generowany automatycznie, jeśli w klasie nie zostanie zdefiniowany żaden inny. W przeciwnym wypadku nie jst on tworzony, co może prowadzić do sytuacji, gdy klasa posiada jedynie takie konstruktory, które wymagają podania parametrów. Jest ona niekorzystna przynajmniej z dwóch powodów:

  1. Gdy dziedziczymy po takiej klasie, musimy jakoś zapewnić, że w klasie pochodnej zostanie wywołany konstruktor klasy bazowej. Zwykle robi się to za pomocą listy inicjalizacyjnej:
    1. Derived(int param1, int param2) : Base(param1, param2) { /*... */ }

    Kiedy jednak zmieni się postać konstruktora klasy bazowej, wówczas modyfikacja będzie musiała dotyczyć wszystkich bezpośrednich potomków tej klasy. W nowej wersji standardu C++ ma być aczkolwiek wprowadzone nowe zadanie dla słowa kluczowego using, które pozwoli na automatyczne utworzenie konstruktorów “przekaźnikowych” – takich, których jedyną rolą jest wywołanie konstuktorów bazowych z takimi samymi parametrami jak te otrzymane w konstruktorze pochodnym. Jeśli jednak przy okazji chcemy coś zmienić czy pominąć, to nie ma rady: trzeba całą tę “sztafetę” zakodować ręcznie.

  2. Jeszcze gorzej jest, kiedy klasa bez konstruktora domyślnego jest wirtualną klasą bazową. Wówczas o jej prawidłowej inicjalizacji muszą pamiętać nie tylko klasy bezpośrednio pochodne, ale w ogóle wszystkie potomne! Dokładnie tak: nawet w piątym czy dziesiątym pokoleniu hierarchii o korzeniu w wirtualnej klasie bazowej, klasy pochodne muszą w swoich konstruktorach wywoływać konstruktory owego korzenia. To oczywiście sprawia, że każda zmiana musi być rozpropagowana na całe to drzewo dziedziczenia (a właściwie graf, bo pewnie zawiera on cykle :>).

Dlatego też klasy będące przede wszystkim klasami bazowymi w dziedziczeniu dobrze jest wyposażać w konstruktory bezparametrowe. Może to aczkolwiek wymagać wyróżnienia w obiekcie stanów Zainicjowany-Niezainicjowany, które powinny być sprawdzane w jego metodach i w razie potrzeby odpowiednio sygnalizowane.

Tags: ,
Author: Xion, posted under Programming » 5 comments
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.