5. Funkcje własne.

5.17. Przykład stosowania funkcji

Nasza epicka przygoda z funkcjami powoli zbliża się ku końcowi. Przyznaję się, że dla początkującego wędrowcy w krainie MQL4 temat ten może nie być łatwy. Pamiętajmy jednak, że każdy krok do przodu, każda pokonana przeszkoda przybliżają nas do celu. Proponuję wykonać ostatni krok, po czym tę część naszej wspólnej wędrówki można będzie uznać za zakończoną. Ale to nie koniec, będzie czekać na nas jeszcze wiele nowych przygód.

W celu utrwalenia nowej wiedzy, wspólnymi siłami napiszmy teraz skrypt, gdzie zastosujemy znane już nam możliwości funkcji własnych. Dla przykładu, rozwiążmy następujące zadanie. Pewna firma zdecydowała wybudować 2 wielkie pojemniki w kształcie walca do przechowywania 2 różnych cieczy. Biuro projektowe otrzymało zadanie obliczenia maksymalnej wagi przechowywanych tam cieczy, aby obliczyć ilość betonu potrzebnego do zalania fundamentów pod te pojemniki. Pracownik biura, któremu przydzielono to zadanie, postanowił wykorzystać swoją wiedzę MQL4 i napisać skrypt.

Dane wejściowe:

  • pojemnik-walec 1: promień = 2.3 m, wysokość = 5 m, gęstość cieczy = 1260.0 kg/m3.
  • pojemnik-walec 2: promień = 1.7 m, wysokość = 5 m, gęstość cieczy = 1190.0 kg/m3.

Przygotujemy 2 pliki: skrypt oraz bibliotekę, gdzie zapiszemy wszystkie potrzebne nam funkcje. Kod skryptu podzielimy na kilka sekcji, które po kolei będę otwierać i wyjaśniać ich przeznaczenie (kod 1).

Kod 1
#property strict
#property script_show_inputs

//--- Sekcja 1: podłączenie biblioteki
#include <Kontener\MyLibrary.mqh>

//--- Sekcja 2.
//--- Sekcja 2.1: inicjalizacja wejściowych parametrów
input double Radius_1  = 2.3;    // 1: Promień, m.
input double Height_1  = 5.0;    // 1: Wysokość, m.
input double Density_1 = 1260.0; // 1: Gęstość, kg/m3.
input double Radius_2  = 1.7;    // 2: Promień, m.
input double Height_2  = 5.0;    // 2: Wysokość, m.
input double Density_2 = 1190.0; // 2: Gęstość, kg/m3.

//--- Sekcja 2.2: inicjalizacja dodatkowych zmiennych
int Amount = 2; // Ilość walców

//--- Sekcja 3.
void OnStart()
  {
   //...
  }

Jak zwykle, kod zaczynamy od zapisu #property strict w celu zastosowania nowego kompilatora. Komenda #property script_show_inputs pozwoli użytkownikowi wybrać wartości zewnętrznych parametrów po uruchomieniu skryptu w terminale MT4. W sekcji 1 podłączamy bibliotekę o nazwie MyLibrary.mqh, która się znajduje w folderze Kontener, a on z kolei jest w folderze Include.

W sekcji 2.1 tworzymy 6 parametrów, aby użytkownik mógł wprowadzać dane do obliczeń. W tym celu użyliśmy modyfikator input , typ każdego parametru to double . Następnie idzie nazwa, przypisana domyślna wartość liczbowa, a dzięki komentarzowi zapisanemu po dwóch prawych ukośnikach // będzie łatwiej zrozumieć przeznaczenie każdego parametru (rys. 1).

Rys. 1. Okienko z zewnętrznymi parametrami.


W tym skrypcie będę również potrzebował zmiennej, która będzie przechowywać ilość pojemników, co też i uczyniłem w sekcji 2.2. Teraz przejdźmy do omówienia zawartości głównej funkcji skryptu OnStart().

Kod 2
//...
//--- Sekcja 1: podłączenie biblioteki
//--- Sekcja 2.
//--- Sekcja 3.
void OnStart()
  {
//--- Sekcja 3.1: sprawdzenie poprawności wejściowych parametrów
   if(Radius_1 < 0.0 || Height_1 < 0.0 || Density_1 < 0.0 ||
      Radius_2 < 0.0 || Height_2 < 0.0 || Density_2 < 0.0)
     {
      Print("Zastosowano parametr, który ma wartość ujemną. Koniec działania skryptu.");
      return;
     }

//--- Sekcja 3.2: deklaracja tablic
//--- Sekcja 3.3: kopiowanie wartości parametrów do tablic
//--- Sekcja 3.4: obliczenie wagi zawartości pojemników za pomocą funkcji
//--- Sekcja 3.5: wyświetlenie wyniku
  }

Każdy porządny program komputerowy powinien zawierać kontrolę błędów. W naszym przypadku nie można wykluczyć, że użytkownik nie chcący może podać wartość ujemną, co doprowadzi do obliczenia błędnego wyniku. Dlatego za pomocą operatora warunkowego if najpierw sprawdzimy czy wartość któregoś z parametrów nie jest mniej niż 0 (sekcja 3.1). W nagłówku tego operatora zapisaliśmy 6 warunków (Radius_1 < 0.0 itd.) rozdzielone operatorami sumy logicznej || (lub) . Jeśli chociaż by jeden z warunków będzie prawdziwy, tj. wartość któregoś z parametrów będzie mniej niż 0, wtedy program przejdzie do realizacji działań zapisanych między {}. Tam funkcja standardowa Print() wyświetli stosowny komunikat w logach dziennika terminala, a operator return przerwie działanie całego skryptu.


W sekcji 3.2 (kod 3) utworzymy 3 tablice typu double i każda będzie składać się z 2 elementów. Nadamy im nazwy, dzięki którym łatwo będzie zidentyfikować ich przeznaczenie: radius - dla promieni podstaw walców, height - wysokości walców i density - gęstości cieczy.

Kod 3
//...
//--- Sekcja 1: podłączenie biblioteki
//--- Sekcja 2.
//--- Sekcja 3.
void OnStart()
  {
//--- Sekcja 3.1: sprawdzenie poprawności wejściowych parametrów
//--- Sekcja 3.2: deklaracja tablic
   double radius[2], height[2], density[2];

//--- Sekcja 3.3: kopiowanie wartości parametrów do tablic
   radius[0]  = Radius_1;  radius[1]  = Radius_2;
   height[0]  = Height_1;  height[1]  = Height_2;
   density[0] = Density_1; density[1] = Density_2;

//--- Sekcja 3.4: obliczenie wagi zawartości pojemników za pomocą funkcji
//--- Sekcja 3.5: wyświetlenie wyniku
  }

W sekcji 3.3 wartości zewnętrznych parametrów skopiujemy do tablic. Pamiętamy, że w tablicach indeksacja elementów zaczyna się od zera, dlatego 1-szy element ma indeks [0], a 2-gi ma indeks [1]. Może pojawić się pytanie: po co to robimy? Dalej w sekcji 3.4 do obliczeń wykorzystamy funkcję i można było by po prostu te 6 parametrów przekazać do funkcji. Oczywiście, można i tak postąpić. Ja jednak chciałem dane do funkcji przekazać w postaci tablic, o czym więcej opowiem niżej, kiedy dojdziemy do omówienia funkcji w bibliotece.


W sekcji 3.4 (kod 4) utworzymy zmienną o nazwie weight typu string i od razu przypiszemy jej wynik obliczenia z funkcji CylindersWeight(), a do nagłówka funkcji, tj. między (), przekażemy 3 tablice oraz zmienną Amount.

Kod 4
//...
//--- Sekcja 1: podłączenie biblioteki
//--- Sekcja 2.
//--- Sekcja 3.
void OnStart()
  {
//--- Sekcja 3.1: sprawdzenie poprawności wejściowych parametrów
//--- Sekcja 3.2: deklaracja tablic
//--- Sekcja 3.3: kopiowanie wartości parametrów do tablic

//--- Sekcja 3.4: obliczenie wagi zawartości pojemników za pomocą funkcji
   string weight = CylindersWeight(radius, height, density, Amount);

//--- Sekcja 3.5: wyświetlenie wyniku
   Print("Waga ", IntegerToString(Amount), " walców = ", weight," kg.");
  }

Po obliczeniu wyniku w sekcji 3.5 za pomocą funkcji Print() zostanie wyświetlona stosowna informacja i to już będzie koniec programu. W poniższym kodzie 5 znajduje się pełny kod źródłowy omówionego skryptu.

Kod 5
#property strict
#property script_show_inputs

//--- Sekcja 1: podłączenie biblioteki
#include <Kontener\MyLibrary.mqh>

//--- Sekcja 2.
//--- Sekcja 2.1: inicjalizacja wejściowych parametrów
input double Radius_1  = 2.3;    // 1: Promień, m.
input double Height_1  = 5.0;    // 1: Wysokość, m.
input double Density_1 = 1260.0; // 1: Gęstość, kg/m3.
input double Radius_2  = 1.7;    // 2: Promień, m.
input double Height_2  = 5.0;    // 2: Wysokość, m.
input double Density_2 = 1190.0; // 2: Gęstość, kg/m3.

//--- Sekcja 2.2: inicjalizacja dodatkowych zmiennych
int Amount = 2; // Ilość walców

//--- Sekcja 3.
void OnStart()
  {
//--- Sekcja 3.1: sprawdzenie poprawności wejściowych parametrów
   if(Radius_1 < 0.0 || Height_1 < 0.0 || Density_1 < 0.0 ||
      Radius_2 < 0.0 || Height_2 < 0.0 || Density_2 < 0.0)
     {
      Print("Zastosowano parametr, który ma wartość ujemną. Koniec działania skryptu.");
      return;
     }

//--- Sekcja 3.2: deklaracja tablic
   double radius[2], height[2], density[2];

//--- Sekcja 3.3: kopiowanie wartości parametrów do tablic
   radius[0]  = Radius_1;  radius[1]  = Radius_2;
   height[0]  = Height_1;  height[1]  = Height_2;
   density[0] = Density_1; density[1] = Density_2;

//--- Sekcja 3.4: obliczenie wagi zawartości pojemników za pomocą funkcji
   string weight = CylindersWeight(radius, height, density, Amount);

//--- Sekcja 3.5: wyświetlenie wyniku
   Print("Waga ",IntegerToString(Amount)," walców = ",weight," kg.");
  }

Teraz przejdźmy do omówienia kodu biblioteki (kod 6). Zapiszmy tam 2 funkcje:

  • w funkcji CylindersWeight() obliczymy ogólną wagę cieczy we wszystkich pojemnikach-walcach,
  • funkcja CylinderVolume() będzie pomocna do obliczenia objętości pojedynczego pojemnika-walca.

Funkcja CylindersWeight() będzie miała typ string, a w jej nagłówku zapiszemy 4 parametry. Pierwsze 3 (f_rad, f_hei oraz f_den) są tablicami, za pomocą których dotrzemy do zawartości tablic ze skryptu, odpowiednio radius, height oraz density. Z rozdziału o przekazywaniu tablic do funkcji pamiętamy, że najpierw zapisujemy specyfikator const, potem typ (w tym przypadku double), potem wskaźnik & (ampersand), nazwę tablicy oraz kwadratowe nawiasy []. Ostatni czwarty parametr będzie miał typ int o nazwie f_a, któremu przypiszemy domyślną wartość 1. Do niego zostanie skopiowana wartość zmiennej Amount, która w skrypcie też ma typ int.

Od siebie dodam, że w nazwach parametrów oraz zmiennych stosowanych w funkcjach własnych, na początku wolę dopisywać f_ , dlatego że kiedy skaczę z jednego kodu do drugiego to wizualnie od razu łatwo zidentyfikować, że teraz pracuję z kodem biblioteki.

Kod 6
#property strict

//--- A: funkcja do obliczenia wagi zawartości walców
string CylindersWeight(const double &f_rad[], // tablica promieni
                       const double &f_hei[], // tablica wysokości
                       const double &f_den[], // tablica gęstości
                       int f_a = 1)           // ilość walców
  {
//--- A.1: inicjalizacja zmiennych
   double f_volume = 0.0, f_weight = 0.0;

//--- A.2: obliczenie ogólnej wagi walców
//--- A.3: zwrócenie wyniku
  }

//--- B: funkcja do obliczenia objętości walca

W ciele funkcji na samym początku tworzymy 3 zmienne typu double, którym od razu przypisuje się domyślną wartość 0.0 (A.1).


Dalej (A.2) za pomocą operatora pętli for obliczymy ogólną wagę walców (kod 7).

Kod 7
//...
//--- A: funkcja do obliczenia wagi zawartości walców
//--- A.1: inicjalizacja zmiennych

//--- A.2: obliczenie ogólnej wagi walców
   for(int i = 0; i < f_a; i++)
     {
      //--- A.2.1: obliczenie objętości walca
      f_volume = CylinderVolume(f_rad[i], f_hei[i]);
      //--- A.2.2: waga zawartości walców
      f_weight = f_weight + (f_volume * f_den[i]);
     }

//--- A.3: zwrócenie wyniku
//--- B: funkcja do obliczenia objętości walca

Ta pętla powtórzy się tyle razy ile będzie równe f_a. W naszym przykładzie parametr ten będzie równy 2, ponieważ podczas wywołania funkcji w skrypcie (kod 4, sekcja 3.4) przekazano tam zmienną Amount, której przed tym przypisano wartość 2 (kod 1, sekcja 2.2). Licznik i zacznie się od zera (i = 0) i zakończy się na 1 z krokiem +1 (i++).


Iteracja 1 (i = 0)

Najpierw następuje wywołanie funkcji CylinderVolume(), do nagłówka której przekazuje się 2 wartości:

  • element tablicy f_rad z indeksem 0
  • element tablicy f_hei z indeksem 0 (A.2.1)

Potrzebna funkcja CylinderVolume() również znajduje się w bibliotece (kod 8, B).

Kod 8
//...
//--- A: funkcja do obliczenia wagi zawartości walców
//--- B: funkcja do obliczenia objętości walca
double CylinderVolume(double f_radius, double f_height)
  {
   return(M_PI * f_radius * f_radius * f_height);
  }

Użyliśmy tu funkcji, którą dobrze już znamy z rozdziału 5.2. Realizacja funkcji własnej (kod 2). Tutaj jednak nie tworzymy dodatkowej zmiennej, której przypisalibyśmy wynik obliczenia M_PI * f_radius * f_radius * f_height, a od razu całe to wyrażenie zapisaliśmy w nagłówku operatora return(). W MQL4 jest to możliwe. Korzyścią takiego podejścia jest to, że program nie będzie musiał rezerwować miejsca dla zmiennej w pamięci RAM, zapisywać tam wartość, a po zakończeniu działania funkcji usuwać ją z RAM. Przyspieszy to nieco działanie całego programu.

Funkcja CylinderVolume() zwróci obliczoną wartość, która zostanie przypisana zmiennej f_volume (A.2.1). Będzie to objętość pierwszego pojemnika-walca obliczona jako 3.14159265358979323846 * 2.3 * 2.3 * 5.0 = 83.09512568744990.

Następnie (kod 9, A.2.2) zostanie obliczona waga zawartości 1-ego pojemnika-walca. Do zmiennej f_weight zostanie dodany wynik mnożenia objętości 1-ego walca (f_volume, obliczonego na poprzednim etapie A.2.1) na gęstość 1-ej cieczy (f_den[i], przy i = 0 będzie to f_den[0]). Przed 1-ą iteracją pętli zmiennej f_weight została przypisana wartość 0.0, do niej zostanie dodana obliczona waga i ta suma zostanie przypisana f_weight.

Wyglądać to będzie w następujący sposób: f_weight = 0.0 + (83.09512568744990 * 1260.0) = 104699,8583661870. Koniec 1-ej iteracji.

Kod 9
//--- A.2.2: waga zawartości walców
f_weight = f_weight + (f_volume * f_den[i]);

Iteracja 2 (i = 1)

Następuje wywołanie funkcji CylinderVolume(), do nagłówka której przekazuje się 2 wartości:

  • element tablicy f_rad z indeksem 1
  • element tablicy f_hei z indeksem 1 (A.2.1)

Funkcja ta zwróci objętość 2-go pojemnika-walca obliczonego jako 3.14159265358979323846 * 1.7 * 1.7 * 5.0 = 45.39601384437250.

Dalej (A.2.2) zostanie obliczona waga zawartości 2-ego pojemnika-walca: f_volume * f_den [1] = 45.39601384437250 * 1190.0 = 54021.25647480320. Potem ten wynik zostanie dodany do f_weight, wartość której po porzedniej iteracji jest równa 104699.8583661870 i suma tych liczb zostanie zapisana w f_weight: f_weight = 104699.8583661870 + (54021.25647480320) = 158721.114840990. Koniec 2-ej iteracji i koniec pętli.


Wyżej obiecywałem napisać uzasadnienie, dlaczego w skrypcie wartości zewnętrznych parametrów skopiowałem do tablic radius, height i density (kod 3, sekcja 3.3), które przekazałem następnie do funkcji CylindersWeight() (kod 4, sekcja 3.4).

Popatrz, w tym momencie mamy uniwersalną funkcję do obliczenia wagi zawartości nieograniczonej ilości (w granicach rozsądku ) pojemników-walców. Gdyby zadanie się zmieniło i trzeba było by obliczyć dane dla 1000 pojemników, wystarczyło by teraz w skrypcie te dane skopiować do tablic, zmienną Amount poprawić odpowiednio na 1000 i gotowe. A funkcję CylindersWeight() zostawiamy bez zmian i nie trzeba było by za każdym razem przygotowywać nową do innej ilości pojemników. Dodatkowo z dokumentacji MQL4 wynika, że liczba parametrów przekazywanych do funkcji jest ograniczona i nie może przekroczyć 64.

Tym przykładem chcę pokazać, że przygotowując funkcję własną warto pomyśleć o tym, żeby uczynić ją trochę uniwersalną. Żeby nie była to funkcja ściśle dopasowana tylko do programu, nad którym obecnie pracujesz, lecz żeby miała ona szerszy potencjał zastosowania. Kto wie, może po jakimś czasie będziesz musiał przygotować inny program z nieco podobnym algorytmem, a gotowa funkcja już będzie w naszej magicznej bibliotece.


Wracamy do omówienia funkcji. Przedostatnim krokiem jest utworzenie zmiennej typu string o nazwie f_result, której od razu się przypisuje wynik działania funkcji standardowej DoubleToString() (kod 10, A.3). Wartość liczbowa typu double przekształcana jest na wartość tekstową typu string z dwoma cyframi po znaku dziesiętnym. Ostatni krok - operator return zwraca obliczony wynik w postaci tekstu.

Kod 10
//...
//--- A: funkcja do obliczenia wagi zawartości walców
string CylindersWeight(...)
  {
//--- A.1: inicjalizacja zmiennych
//--- A.2: obliczenie ogólnej wagi walców
//--- A.3: zwrócenie wyniku
   string f_result = DoubleToString(f_weight, 2);
   return(f_result);
  }

//--- B: funkcja do obliczenia objętości walca

Po zakończeniu działania funkcji (A i B), wszystkie utworzone przez nie zmienne są usuwane z pamięci RAM.


ZAKOŃCZENIE

Artykuł o funkcjach okazał się dość obszerny jak i sam temat. Jeśli przy opracowaniu małego programu, cały kod bez większych problemów można zapakować w głównej funkcji, to w większym programie może to być już problematyczne. W takiej sytuacji część działań można zapakować w funkcje i wynieść poza granice funkcji głównych. Poprawia to czytelność kodu, ułatwia poszukiwanie błędów i pozwala na wielokrotne wykorzystanie funkcji. Jeśli kiedyś bawiłeś się w budowanie konstrukcji z klocków Lego to sądzę, że pozytywnie ocenisz też i możliwość budowania różnorodnych programów z takich klocków-funkcji. W tym artykule postarałem się przedstawić podstawowe informacje nt. funkcji własnych, które powinny być wystarczające do zrozumienia mechanizmu ich działania.