Procedury i funkcje

Adam Boduch

Procedura/funkcja to wydzielony blok kodu realizujący określone zadanie. Funkcje od procedury odróżnia to, że ta pierwsza zwraca rezultat działania

1 Programowanie proceduralne
     1.1 Procedury
     1.2 Funkcje
2 Parametry procedur i funkcji
     2.3 Kilka parametrów procedur lub funkcji
     2.4 Tablica jako parametr procedury
     2.5 Tablice zwracane przez funkcje
     2.6 Łańcuchy jako parametr procedury
     2.7 Parametry domyślne
          2.7.1 Parametry tego samego typu a wartości domyślne
               2.7.1.1 Kolejność wartości domyślnych
               2.7.1.2 Przekazywanie parametrów przez wartość
               2.7.1.3 Przekazywanie parametrów przez stałą
               2.7.1.4 Przekazywanie parametrów przez referencję
     2.8 Parametry bez typu
     2.9 Tablice otwarte
3 Procedury zagnieżdżone
4 Wplatanie funkcji i procedur
5 Przeciążanie funkcji i procedur
6 Dyrektywa forward
7 Wywołanie zewnętrzne

Programowanie proceduralne

Idea programowania proceduralnego zaczęła się pojawiać wraz z bardziej zaawansowanymi aplikacjami. Tradycyjny moduł projektowania nie sprawdzał się dobrze, gdy programy zaczęły być bardziej skomplikowane; wówczas ich konserwacja i naprawianie błędów były niezwykle trudne.
Ktoś mądry wymyślił wtedy, że można by było dzielić program na mniejsze części, tzw. procedury. Przykładowo, jeżeli napisano kod, który wyświetla pewien komunikat i kończy działanie programu, a ów fragment jest używany wiele razy w tym programie, to należałoby go dublować wiele razy. Powoduje to nie tylko zwiększenie objętości kodu, ale również potęguje podatność na błędy. Bo co się stanie, jeżeli właśnie w tym małym, wielokrotnie powtórzonym w aplikacji fragmencie, wystąpi błąd? Należałoby wówczas przeszukać cały kod i w każdym miejscu poprawiać usterkę.
Teraz, właściwie w każdym języku programowania można pewien fragment kodu umieścić w procedurze i za każdym razem, kiedy zajdzie potrzeba jego wykonania wywołać procedurę!

Procedury

Proste wywołanie procedury Messagebox zostało przedstawione na poniższym listingu.

program p13;

uses
  Windows;

begin
  MessageBox(0, 'Jestem oknem informacyjnym', 'Witaj!', 0);
end.

Jeżeli skompilujemy i uruchomimy program, zauważymy brak okna konsoli. Zamiast tego pojawi się okno informacyjne z tekstem: Jestem oknem informacyjnym. Po kliknięciu przycisku OK okno zostanie zamknięte, a działanie programu zakończone.

W tym przypadku procedurą jest Messagebox, która realizuje wyświetlenie okna informacyjnego. Musimy podać jedynie parametry, nie interesuje nas wnętrze procedury, a jedynie sposób jej działania: wyświetlanie okno informacyjnego.

Wyobraźmy sobie, że w danym programie trzeba często wykonywać ten sam blok kodu, np.wyświetlanie kilku tekstów w konsoli. W takim przypadku za każdym razem należałoby wpisywać po kolei instrukcje Writeln. Dzięki zastosowaniu procedur możesz wpisać te instrukcje tylko raz, później pozostaje już tylko umieszczenie w odpowiednim miejscu nazwy procedury, aby wykonać kod w niej zawarty. Przykładowa deklaracja procedury wygląda następująco:

procedure Nazwa_Procedury;
begin
    { kod procedury }
end;

Jak widać, procedurę deklaruje się z użyciem słowa kluczowego Procedure, po którym następuje jej nazwa. Nazwa procedury musi być unikalna dla każdego modułu/programu, nie może się powtarzać (jest to drobne uproszczenie, ponieważ tak w rzeczywistości możemy mieć kilka procedur o tych samych nazwach, lecz różnych parametrach (mowa wtedy o tzw.przeładowywaniu procedur)).

Od teraz za każdym razem, gdy gdzieś w kodzie wpiszemy słowo Nazwa_Procedury, zostanie wykonany kod umieszczony w jej wnętrzu. Przykładowy program z wykorzystaniem procedur przedstawiam poniżej.

program ProcApp;

{$APPTYPE CONSOLE}

  procedure Quit;
  begin
    Writeln('Wyjście z programu! Naciśnij Enter, aby zakończyć!');
    Readln;
  end;

var
  Key : Char;

begin

  Writeln('Naciśnij literę Q, aby zakończyć!');
  Readln(Key);

  if Key = 'Q' then 
    Quit 
  else
  begin
    Writeln('Fajnie, że jesteś z nami');
    Readln;
  end;
end.

Trzeba zapamiętać, że procedury deklaruje się poza blokiem begin..end! W powyższym programie zadeklarowałem procedurę Quit. Od tej pory za każdym razem, gdy w programie znajdzie się słowo Quit, nastąpi wyświetlenie informacji i po naciśnięciu klawisza Enter zakończenie działania procedury (w naszym przykładzie zakończenie działania aplikacji).

Ogólna postać modułu formularza z procedurą zdarzeniową kliknięcia przycisku:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TForm1=class(TForm)
    Button1: TButton;
    //deklaracja procedury
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//wywołanie procedury poprzez zdarzenie kliknięcia myszą na przycisku
procedure TForm1.Button1Click(Sender: TObject);
begin
  //okno z komunikatem
  ShowMessage('Okienko z komunikatem !');
end;

end.

Funkcje

Funkcje są w swoim działaniu bardzo podobne do procedur. Podstawowa różnica pomiędzy procedurami a funkcjami polega na tym, że te drugie zwracają jakąś wartość.

Deklaracja funkcji jest bardzo specyficzna, następuje bowiem poprzez użycie słowa kluczowego Function. Oprócz tego funkcja, jak już mówiłem, musi zwracać jakąś wartość, po znaku dwukropka należy wpisać typ zwracanej wartości:

function GetName : String;
begin

end;

Być może ktoś w dalszym ciągu nie rozumie, na czym polega różnica między funkcją a procedurą, najłatwiej napisać przykładowy program; spójrzmy na poniższy listing.

program FuncApp;

{$APPTYPE CONSOLE}

  function GetName : String;
  begin
    Result := 'Jasio Stasio';
  end;

begin

  Writeln('Nazywam się ' + GetName);
  Readln;

end.

Po uruchomieniu takiego programu na konsoli zostanie wyświetlony tekst Nazywam się Jasio Stasio. To wszystko stanie się za sprawą wiersza:

Result := 'Jasio Stasio';

Słowo Result oznacza "ukrytą" zmienną; po przypisaniu jej wartości zostanie ona zwrócona przez funkcję.

W [[Turbo Pascal|Turbo Pascalu]] nie istniała niejawna zmienna [[Delphi/Result]]; zwracanie wartości przez funkcję następowało poprzez wykonanie takiego fragmentu: `nazwa_funkcji := wartosc;` W miejsce Result należało wstawić nazwę funkcji, wówczas program się kompilował, a funkcja zwracała odpowiednią wartość.

Jeśliby spróbować to samo zrobić z procedurami - po prostu się to nie uda, ponieważ procedura nie może zwrócić wartości. Program nie zostanie zatem uruchomiony.

Tak naprawdę popełniłem pewną nieścisłość, mówiąc wcześniej o Messagebox jako o procedurze, gdyż jest to funkcja. Ona również zwraca jakiś rezultat (przycisk naciśnięty przez użytkownika), lecz wtedy ten rezultat nie był nam do niczego potrzebny. Nie chciałem jednak Czytelnikowi mieszać w głowie, a MessageBox wydawał się dobrym przykładem na wykorzystanie procedury czy funkcji.

Parametry procedur i funkcji

Wraz z wywołaniem danej procedury lub funkcji jest możliwe przekazanie do niej danych, tzw. parametrów.
Deklaracja procedury lub funkcji zawierającej parametry wygląda następująco:

procedure SetName(Name : String);
begin
   { kod procedury }
end;

Od tej pory, chcąc wywołać procedurę SetName, należy podać wartość parametru. Parametr ten musi być typu String.

Poniższy, przykładowy program wyświetli na konsoli za pomocą procedury SetName tekst z parametru Name:

program ProcParam;

{$APPTYPE CONSOLE}

  procedure SetName(Name : String);
  begin
    Writeln('Nazywam się ' + Name);
  end;

begin

  SetName('Adam Boduch');
  Readln;
end.

Procedura SetName odczytuje wartość przekazanego do niej parametru i wyświetla go wraz z pozostałym tekstem na konsoli.

Kilka parametrów procedur lub funkcji

Możliwe jest przekazanie do procedury lub funkcji kilku parametrów tego samego bądź innego typu. W takim przypadku wszystkie parametry należy oddzielić znakami średnika:

function SetName(FName : String; SName : String; Age : Byte) : String;

Zatem aby program został prawidłowo skompilowany, należy przekazać funkcji aż trzy parametry. Jeżeli jednak występuje kilka parametrów tego samego typu, można oddzielić je przecinkami w ten sposób:

function SetName(FName, SName : String; Age : Byte) : String;

Dzięki temu zapis funkcji jest nieco krótszy. Naturalnie z punktu widzenia kompilatora nie ma znaczenia, w jaki sposób jest zapisana deklaracja, jest więc możliwe zapisanie tego również w taki sposób:

  function SetName(FName,
                   SName : String;
                   Age : Byte
                   ) : String;
  begin

  end;

Tablica jako parametr procedury

Istnieje możliwość deklaracji parametru procedury jako tablicy dynamicznej (czyli takiej bez określonego z góry rozmiaru), wtedy jest mowa o tzw.open array:

procedure ArrayProc(A : array of String);
begin
  Writeln(Length(A));
end;

W takim przypadku procedura wyświetli w konsoli ilość elementów w tablicy. Tablica jest przekazywana do procedury w standardowy sposób:

var A : array[0..1] of String;
begin
  A[0] := 'Delphi';
  A[1] := '2005';
  ArrayProc(A); // wywołanie funkcji
  Readln;
end.

Nasuwa się jednak pytanie: czym zatem różni się deklaracja:

procedure First(A : array of Integer);

od:

type TIntegerArray = array of Integer;
procedure Second(A: TIntegerArray);

Jak wspomniane zostało wyżej, pierwsza deklaracja zawiera open array; znaczy to ni mniej ni więcej, ile to, że takie wywołanie:

First([1, 2, 3]);

Jest poprawne, podczas gdy:

Second([1, 2, 3]);

Już nie.

Tablice zwracane przez funkcje

Nie można w standardowy sposób zadeklarować funkcji, która zwracałaby tablicę jako rezultat operacji:

function ArrayFunc : array of String;
begin

end;

Można jednak zadeklarować nowy typ. Poniższy kod zostanie skompilowany bez żadnych problemów:

type
  TBar = array of String;

function Foo : TBar;
begin

end;

Łańcuchy jako parametr procedury

W parametrach procedur i funkcji nie można przekazywać łańcuchów z ograniczoną dlugością - np.:

procedure Foo(S : String[20]);
begin

end;

Jeżeli chcemy, aby max. długość łancucha S ograniczała się do 20 znaków, należy zadeklarować nowy typ:

type
  MyString = String[20];

procedure Foo(S : MyString);
begin

end;

Parametry domyślne

Delphi oferuje przydatną możliwość deklarowania parametrów domyślnych. Oznacza to, że podczas wywoływania procedury lub funkcji taki parametr może ale nie musi zostać wpisany; w takim przypadku Delphi zastosuje wartość domyślną, ustaloną przez programistę. Oto przykład deklaracji takiej procedury:

  procedure SetName(FName : String;
                    SName : String = 'Nieznany';
                    Age : Byte = 0
                   );
  begin

  end;

Parametry domyślne wpisuje się po znaku równości. W przypadku powyższej procedury możliwe jest albo wywołanie jej w ten sposób:

SetName('Moje imię');

albo wpisanie wszystkich parametrów:

SetName('Moje imię', 'Moje nazwisko', 100);

Obydwa sposoby są prawidłowe. Jeżeli nie wpiszemy dwóch ostatnich parametrów, Delphi za domyślne wartości uzna te, które zostały wpisane w deklaracji procedury. Zatem dla parametru SName będzie to wartość Nieznany, a dla Age - 0.

Parametry tego samego typu a wartości domyślne

Niestety, nie jest możliwe deklarowanie kilku parametrów tego samego typu i jednoczesne przypisanie wartości jednemu z nich:

  procedure SetName(FName,
                    SName : String = 'Wartość';
                    Age : Byte = 0
                   );

Powyższa instrukcja spowoduje błąd; zawsze podczas określania wartości domyślnych należy wpisać typ parametru.

Kolejność wartości domyślnych

Należy jeszcze poczynić jedno istotne zastrzeżenie dotyczące deklaracji wartości domyślnych. Otóż błędna jest deklaracja wyglądająca tak:

  
function Funkcja(X : Integer = 0; Y : Integer) : Integer;
begin

end;

Próbujemy w ten sposób przypisać wartość domyślną pierwszemu parametrowi, a drugiemu nie. Delphi zaprotestuje i wyświetli błąd: Default value required for 'Y'..
Jedynym wyjściem jest umieszczenie domyślnych parametrów na samym końcu lub deklaracja wartości domyślnych dla każdego parametru. Taki kod będzie już jak najbardziej prawidłowy:

  
function Funkcja(Y : Integer; X : Integer = 0) : Integer;
begin

end;

Przekazywanie parametrów przez wartość

Przekazywanie parametrów przez wartość jest sposobem najprostszym z możliwych. Deklaracja procedury nie musi zawierać żadnych dodatkowych słów kluczowych - wystarczy poniższa konstrukcja nagłówka procedury bądź funkcji:

procedure Foo(S : String);

Przekazanie parametru przez wartość wiąże się utworzeniem jego kopii lokalnej do wykorzystania jedynie przez procedurę lub funkcję. Oryginalna wartość zmiennej przekazanej do procedury nie zostaje w żaden sposób naruszona.

Przekazywanie parametrów przez stałą

Idea przekazywania parametrów przez stałą pojawiła się już w Turbo Pascalu 7.0. Umieszczenie w deklaracji parametrów słowa kluczowego const spowoduje przekazywanie parametrów jako stałych.

procedure Show(const Message : String);
begin
  Writeln(Message);
end;

Co to oznacza? Procedura nie może w żaden sposób wpływać na zawartość parametru. Próba nadania przez procedurę jakiejś wartości spowoduje komunikat o błędzie: Left side cannot be assigned to. - nie można więc zapisać tego w ten sposób:

procedure Show(const Message : String);
begin
  Message := 'Nowa wartość';
  Writeln(Message);
end;

Przekazując parametry przez stałą, pozwalamy kompilatorowi na maksymalną optymalizację kodu.

Przekazywanie parametrów przez referencję

Przekazywanie parametrów przez referencję polega na umieszczeniu przed parametrami słowa kluczowego Var. Dzięki temu kod znajdujący się wewnątrz procedury może zmienić wartość parametru. Aby lepiej to zrozumieć, wykonajmy małe ćwiczenie. Spójrzmy na poniższy listing.

program VarParam;

{$APPTYPE CONSOLE}

procedure SetValue(Message : String);
begin
{ próba nadania parametrowi nowej wartości }
  Message := 'Hello there!';
end;

var
  S : String;

begin
  S := 'Hello World';
  SetValue(S);  // przekazanie zmiennej
  Writeln(S); // odczyt wartości zmiennej
  Readln;

end.

Teraz zagadka: jaką wartość będzie miała zmienna S? Uruchommy program i sprawdźmy! Na ekranie konsoli zostanie wyświetlony napis Hello World, co oznacza, że procedurze nie udało się zmienić wartości parametru. Wszystko dlatego, że w przypadku przekazywania parametru przez wartość, jest tworzona lokalna kopia zmiennej.

Zmieńmy teraz nieco deklarację procedury SetValue:

procedure SetValue(var Message : String);
begin
{ próba nadania nowej wartości dla parametru }
  Message := 'Hello there!';
end;

Dzięki dodaniu słowa Var i ponownemu uruchomieniu programu można przekonać się, że zmienna S będzie miała wartość Hello there. Oznacza to, że naszej procedurze udało się zmienić wartość tego parametru.

Sposób przekazywania danych przez referencję jest optymalny, gdyż nie jest tworzona kopia zmiennej. W związku z tym jest możliwe przekazywanie danych z procedury na zewnątrz (co pokazałem na poprzednim przykładzie).

Parametry bez typu

W Delphi dla Win32 można deklarować procedury i funkcje, które nie posiadają typu:

procedure Foo(const Src, Dst);
begin

end;

W takim wypadku, nazwy parametrów muszą być poprzedzone słowem kluczowym Var, Const lub Out. Dostęp do wartości takich parametrów musi odbywać się poprzez Rzutowanie.

Taka konwencja nie jest dozwolona w Delphi dla .NET!

Tablice otwarte

Rzadko spotykany sposób przekazywania parametrów do funkcji/procedury to tablice otwarte:

function Foo(const A : array of const) : String;

Mamy tutaj specyficzne wyrażenie: array of const. Dzięki temu możemy wywołać procedurę np. w taki sposób:

ShowMessage(Foo(['test', 10, ' ', 'mama', 1000]));

Budowa samej funkcji Foo może wyglądać np. tak:

function Foo(const A : array of const) : String;
var
  i : Integer;
begin
  for I := 0 to High(A) do
  begin
    case A[i].VType of
      vtInteger: Result := Result + IntToStr(A[i].VInteger);
      vtChar: Result := Result + A[i].VChar;
      vtString: Result := Result + A[i].VString^;
      vtAnsiString: Result := Result + String(A[i].VAnsiString);
      vtVariant: Result := Result + String(A[i].VVariant^);
      { itd. }
    end;
  end;
end;

Procedury zagnieżdżone

Nic nie stoi na przeszkodzie, aby daną procedurę lub funkcję umieścić w innej procedurze lub funkcji. Wygląda to mniej więcej tak:

procedure A;
  procedure B;
  begin

  end;
begin

end;

Z powyższego zapisu wynika, że procedura lub funkcja zagnieżdżona (w tym wypadku procedura B) musi zostać umieszczona przed blokiem Begin.

W takim przypadku nadal obowiązują zasady o zmiennych lokalnych. Oznacza to, że zmienna umieszczona w procedurze B nie będzie dostępna dla procedury A.

Przy zastosowaniu procedur zagnieżdżonych obowiązują również inne zasady. Procedura zewnętrzna do A nie ma dostępu do procedury B, procedura B ma dostęp do parametrów wywołania procedury A. Dodatkowo w procedurze A istnieje możliwość wielokrotnego zastosowania bloku deklaracji zmiennych:

function A():integer;
var
  x:integer;
  function B():integer;
  begin
    result:=x;
  end;
var
  y:integer;
begin
  result:=y+B();
end;

W powyższym przykładzie, zmienne x, y są dostępne w funkcji A, jednak rozbicie bloku deklaracji zmiennych daje możliwość określenia, że funkcja B będzie miała dostęp wyłącznie do zmiennej x, gdyż ta jest zdefiniowana przed funkcją B. Nie będzie miała jednak dostępu do zmiennej y.

Procedury i funkcje zagnieżdżone mogą okazać się przydatne, gdy do realizacji jednej funkcji przydatna lub wymagana jest inna procedura lub funkcja, która, z punktu widzenia innych procedur lub funkcji danego modułu, jest zbędna. Jednocześnie warto przypomnieć, że zagnieżdżenie pozwala na wywoływanie bez przekazywania parametrów z funkcji nadrzędnej.

Zagnieżdżenie może być wielokrotne (na przykład wiele funkcji lub procedur zagnieżdżonych w A) oraz wielopoziomowe (na przykład C zagnieżdżone w B, które jest zagnieżdżone w A). W takich sytuacjach blok deklaracji zmiennych może wystąpić wielokrotnie, co pokazuje poniższy przykład:

function A():integer;
//BLOK 1
var
  x:integer;
  function B1():integer;
    function C():integer;
    begin
      result:=x;
    end;
  begin
    result:=C();
  end;
//BLOK 2
var
  y:integer;
  function B2():integer;
    begin
    result:=y;
    end;
//BLOK 3
var
  z:integer;
begin
  result:=B1()+B2()+z;
end;

Wplatanie funkcji i procedur

Nowością w Delphi 2005 jest możliwość wplatania funkcji i procedur. Polega to na opatrzeniu funkcji/procedury słowem kluczowym Inline, np.:

procedure Foo; inline;
begin
  Console.WriteLine('Procedura wplatana...');
end;

begin
  Foo;
end.

W powyższym przykładzie procedura Foo nie jest wywoływana, jej kod jest kopiowany do miejsca wywołania. Można powiedzieć, że kompilator zastępuje powyższy kod na następujący:

begin
  Console.WriteLine('Procedura wplątana...');
end.

W niektórych przypadkach pozwala to na zwiększenie wydajności aplikacji, lecz zwiększa się przez to rozmiar aplikacji wykonywalnej. Dobrym rozwiązaniem będzie umieszczenie w kodzie programu dyrektywy kompilatora:

{$INLINE AUTO}

Dzięki temu kompilator sam oceni, czy należy wplatać daną procedurę/funkcję, czy też nie.

Przeciążanie funkcji i procedur

Ten temat został opisany w osobnym artykule: Przeciążanie funkcji i procedur.

Dyrektywa forward

Ten temat został opisany w osobnym artykule: Forward.

Wywołanie zewnętrzne

Ten temat został opiasny w osobnym artykule: External.

Zobacz także:

9 komentarzy

No.... powiedzmy - niedopowiedzenie. Poprawilem. :)

"W powyższym programie zadeklarowałem procedurę Quit. Od tej pory za każdym razem, gdy w programie znajdzie się słowo Quit, nastąpi wyświetlenie informacji i ? po naciśnięciu klawisza Enter ? zakończenie jego działania."

Błąd? Ta procedura tylko wypisuje tekst i czeka na naciśnięcie klawisza! Gdyby umieścić ją na początku programu to napis by się wyświetlił i czekałby na naciśnięcie klawisza ale po tym program działałby dalej.

LOL, jakiej procedury KeyPress? :D

przysałoby sie cos na temat procedury keypress!!

Jakoś nie podoba mi się definicja "procedura wplątana" :/ Chyba wolę stare, dobre "funkcje inline", ew. "rozwijane w miejscu wywołania" :) Po spisie treści i treści nagłówka w życiu bym się nie domyślił, że o inline chodzi.

Przekazując parametry przez stałą, pozwalamy kompilatorowi na maksymalną optymalizację kodu.

Mozna by napisac jeszcze dlaczego - bo wtedy smialo mozna przekazac wszystko przez referencje co daje korzysc przy duzych strukturach.

"Tablice zwracane przez funkcje" - to chyba jest do dokonczenia, prawda? :)

Nie wiem czy nie powinienem umiescic informacji na temat parametrow w osobnym artykule? Jak myslicie?

Wszelkie bledy, niedopowiedzenia czy przeklamania -- prosze o poprawienie :)

Programowanie proceduralne -> Programowanie strukturalne -> Programowanie obiektowe -> Programowanie zdarzeniowe

Programowanie proceduralne? A nie strukturalne się to nazywa raczej? A na ewpno częściej słyszałem o strukturalnym.