Object Pascal: продвинутый кодинг или чему не учат в школе (Часть 1. Любите ли вы const так как люблю его я?) |
20.05.2007 Золотов Алексей, http://zolotov.h14.ru |
В статье мы поближе познакомимся с ключевыми словами const, var и out. При объявлении процедуры/функции на Pascal есть 4 способа передачи аргументов (в некоторых компиляторах может быть меньше). Рассмотрим все четыре способа на примере процедуры принимающей один аргумент.
Передача по значению: procedure func(S: mytype)
. При передаче аргумента по значению происходит копирование, то есть создается (в стеке) новая переменная и происходит копирование (как при вызове оператора присваивания). Это значит, что если mytype это массив из 1000 элементов, все эти 1000 элементов будут скопированы, на что будет потрачено время. Если mytype это ShortString, то будет скопировано до 256 байт. AnsiString и WideString внутренне представляются как указатели и казалось бы при копировании будет копироваться только SizeOf(Pointer) байт – так оно и есть, но кроме копирования будет еще неявный вызов функции, которая увеличит счетчик ссылок строки, будет добавлен неявный блок try/finally для надежного декремента счетчика ссылок (см. ниже). Тоже самое относиться и к интерфейсам, с той лишь разницей, что подсчет ссылок ведет объект, реализующий интерфейс и скорость работы завит от реализации объекта.
Остальные три типа являются вариациями передачи по ссылке:
1. procedure func(var S: mytype); 2. procedure func(const S: mytype); 3. procedure func(out S: mytype);
При передаче по ссылке функции передается не само значение, а ссылка (то бишь указатель) на переменную, содержащую передаваемое значение – поэтому вызов будет происходить быстрее (для сложных типов: строк, интерфейсов, записей, статических и динамических массивов). В случае использования var – мы можем читать и изменять значение, указываемое S, в случае использования const – только читать, случае out – только записывать.
Выбор соответствующей вариации зависит от того, что вы собираетесь делать с передаваемым значением. Если вы будете только читать, то укажите const – и пусть вас (если вы раньше программировали на С++ и любили там const) не смущает запись типа:
procedure func(const S: IList); begin S.Add(‘hello world’); end;
Ибо в Паскале, нет деления на константные и неконстантные методы (по крайней мере, пока) – здесь ключевое слово const запрещает только непосредственную модификацию указываемого объекта. Естественно, нельзя передать const аргумент другой функции, принимающий var или out аргумент.
Если вам нужно только передать значение из вызываемой функции, то используйте out аргумент:
procedure func(out S: IList); begin Result := TList.Create; end;
Кроме того, что при передаче по ссылке не происходит копирования больших блоков памяти, компилятор так же не вставляет дополнительные неявные блоки try/finally, которые нужны для гарантированного декремента ссылок (а, следовательно, для освобождения памяти и ресурсов).
Даже такая простая функция:
procedure func(S: AnsiString); begin end;
Порождает пустой try/finally блок – взглянем на ассемблерный листинг:
SECTION .text ALIGN 16 GLOBAL P$PROGRAM_FUNC$ANSISTRING P$PROGRAM_FUNC$ANSISTRING: ; Temps allocated between ebp-52 and ebp-4 ; [12] begin push ebp mov ebp,esp sub esp,52 ; Var a located at ebp-4 mov dword [ebp-4],eax mov eax,dword [ebp-4] call NEAR FPC_ANSISTR_INCR_REF lea ecx,[ebp-24] lea edx,[ebp-48] mov eax,1 call NEAR FPC_PUSHEXCEPTADDR call NEAR FPC_SETJMP push eax test eax,eax jne NEAR ..@j11 ..@j11: call NEAR FPC_POPADDRSTACK ; [14] end; lea eax,[ebp-4] call NEAR FPC_ANSISTR_DECR_REF pop eax test eax,eax je NEAR ..@j12 call NEAR FPC_RERAISE ..@j12: leave ret
Жуть, не правда ли? Если переписать то же самое на псевдокоде то получиться следующее:
procedure func(S); var A; begin // создаем локальную копию A := S; // увеличиваем счетчик ссылок для локальной копии А FPC_ANSISTR_INCR_REF(A); try finally // уменьшаем счетчик ссылок для A, то бишь освобождаем A FPC_ANSISTR_DECR_REF(A); end; end;
Если мы передаем по ссылке, то копирования не происходит, следовательно, не вызываются FPC_ANSISTR_INCR_REF/FPC_ANSISTR_DECR_REF, следовательно, нет надобности в try/finally.
Компилятор FreePascal имеет специальную директиву {$IMPLICITEXCEPTIONS}, которая разрешает/запрещает компилятору вставлять неявные блоки try/finally. {$IMPLICITEXCEPTIONS ON} – разрешает вставлять неявную обработку исключений, а {$IMPLICITEXCEPTIONS OFF} – запрещает вставлять неявную обработку исключений. По-молчанию неявная обработка исключений разрешена и не стоит её запрещать, если вы точно не уверены в том, что делаете – грамотное использование передачи по ссылке избавит вас от неявной обработки исключений и от лишних операций копирования.