Обычно начинающие программисты, переходя с языка Pascal на Delphi, перетаскивают свои старые функции и прочие участки часто встречающегося кода без изменений, не зная, сколько всего уже реализовано в VCL. Причина чаще всего в том, что при преподавании языка Pascal в учебных заведениях рассматриваются только модули CRT и Graph, содержащие базовые функции, а современные технические писатели, описывающие Delphi, рассматривают только малую часть всех возможностей данного языка. Вот и выходит, что код загромождается лишними, часто неоптимизированными функциями.
Так было и со мной, когда после года изучения языка Pascal в ВУЗе, я решил сам освоить Delphi. Купил несколько книг, разобрался с тем, как создавать формы, вводить и выводить данные, стал писать простые программы. Но чем больше я углублялся в Delphi, тем чаще стал замечать, что большая часть написанного мною кода уже реализована в стандартных модулях. Изучая исходный код VCL, я открыл для себя много замечательных функций и типов, которые здорово облегчили бы мне работу над многими программами. Сейчас, разрабатывая уже достаточно сложные программы, прежде чем написать какую-нибудь функцию, производящую более-менее стандартные действия, я проверяю, нет ли уже её реализации в VCL, и процентах в тридцати случаев оказывается, что есть.
Недавно я открыл для себя замечательный модуль ConvUtils и решил рассказать новичкам, стремящимся изучить замечательный язык программирования Delphi, о скрытых функциях и возможностях, которые почти нигде не описаны и сразу не бросаются в глаза.
Рефакторинг
Среда разработки предоставляет множество возможностей, облегчающих жизнь. Все знают про то, что после точки Delphi выводит набор свойств и методов, доступных в данном классе. Но про то, что если нажать Ctrl+Пробел, то среда предложит набор функций и констант, доступных во всех подключенных модулях, а также все переменные, свойства и методы данного модуля, знают немногие.
Многим не нравится то, что в Pascal (а значит, и в Delphi) переменные объявляются в специальной секции Var, и часто для того, чтобы объявить новую переменную, приходится прокручивать много кода. В последних версиях Delphi эта «проблема» решена: достаточно в любом месте кода написать var и нажать Tab. Среда создаст строку: LVar: Integer. Если теперь написать имя будущей переменной, нажать Tab, задать тип и нажать Tab еще раз, среда сама переместит переменную в секцию var. Так же этот сервис доступен через Ctrl+Пробел. Можно сделать иначе: написать имя будущей переменной в коде функции, навести на неё курсор и нажать Shist+Ctrl+V. Появится окно «Declare Variable». В поле Name будет имя новой переменной, которое нельзя изменить. В поле Type – тип будущей переменной, его можно изменить, если среда определила его неверно. Если включить флажок Array, среда создаст динамический массив указанного типа с глубиной вложенности, указанной в поле Dimensions. Если включить флажок Set Value, среда позволит указать значение переменной, присваиваемое ей при инициализации. Новую переменную можно сделать полем некоторого класса. Для этого надо нажать Shift+Ctrl+D. Появится окно «Declare New Field», содержащее те же поля, что и «Declare Variable».
Еще одна замечательная возможность среды – Extract Method. Предположим, что при написании какой-то функции оказывается, что часть кода будет использоваться где-то еще. Можно при написании другой функции скопировать необходимую часть кода, объявить те же переменные, присвоить им значения, в общем, подогнать код под другие условия. А можно использовать этот код как отдельную функцию. Но вырезка уже написанных строк, создание новой функции с необходимыми параметрами и встраивание в неё кода займет больше времени и труда, чем выделение написанного, и нажатие Shift+Ctrl+M. Появится новое окно «Extract Method». В поле «New method name» вводим имя нового метода, а в поле «Sample extracted code» будет представлен будущий код. При этом все переменные, которые используются только внутри выделенного участка, перенесутся в секцию Var нового метода, а все переменные, которые используются и в окружающем выделенный фрагмент коде, будут переданы в новый метод в качестве параметров. Нажмем ok, и выделенные строки перенесутся в новый метод, а на их месте появится его вызов. Конечно, не всегда новый метод создается безупречно, иногда приходится что-то править вручную, однако эта возможность действительно очень удобна.
Так же стоит обратить внимание на «Sync Edit Mode». Предположим, что в уже написанном и отлаженном модуле, содержащем приличное количество строк, нужно изменить имя глобальной переменной. Это несложно, но потом приходится тратить немало времени, переименовывая эту переменную везде, где она встречается. Пользоваться вслепую функцией «Замена» тоже удается не всегда, например, когда надо переименовать переменную имя которой содержится в именах других переменных и методов. Можно возложить работу на компилятор и попытаться скомпилировать модуль, а потом пройтись по всем ошибкам, исправляя имена переменных. Но самый быстрый и верный способ решения данной задачи заключается в выделении текста всего модуля комбинацией Ctrl+A, нажатии Shift+Ctrl+J и изменении имени переменной только там, где она объявлена. Среда сама переименует её везде, где она используется. Замечу, что так можно изменять не только имена переменных, но и имена методов и свойств классов, имена самих классов и имена функций и процедур. Также можно выделять текст не всего модуля, а некоторого фрагмента. Изменить имя именно переменной можно еще одним способом: навести курсор на переменную, которую надо переименовать, и нажать Shift+Ctrl+E. В появившемся окне «Rename variable» в поле «New name» задается новое имя. Если оставить галочку «View reference before refactoring», то среда перед переименованием сначала покажет все места, где встречается переменная, для подтверждения программистом.
Работа со строками
Поиск подстроки в строке
Обычно программист, перешедший с Pascal на Delphi, сам пишет функцию поиска подстроки в строке. Выглядит она примерно так:
Function FindSubStrInStr(Const Str,SubStr:string):Boolean;
Var
i,j:integer;
Begin
Result:=false;
For i:=1 to Length(str) do
begin
If (Str[i]=SubStr[1]) and (Length(substr)<=Length(Str)-(i-1)) then
begin
Result:=true;
For j:=2 to Length(SubStr) do
begin
If SubStr[j]<>Str[i+j-1] then
begin
Result:=false;
Break;
End;
End;
End;
If Result Then
Break;
End;
End;
Создание и отладка этой функции занимает от 3х минут, и то, если программист некогда уже писал что-то подобное и имел представление что ему нужно. Но в стандартном модуле System уже есть замечательная функция Pos. Программист, знающий о её существовании потратит на ту же задачу секунд десять.
Функция Pos: Параметры:
1. Подстрока, тип string.
2. Строка, тип string. Возвращаемое значение:
Тип integer. Функция возвращает индекс первого символа подстроки в строке. Если подстрока не найдена – 0.
Впрочем, описание функции Pos все-таки иногда встречается в книгах.
Подсчет вхождений подстроки в строку.
Вторая похожая задача: подсчитать количество вхождений подстроки в строку. Можно переписать имеющуюся функцию примерно так, потратив те же примерно 3 минуты:
Function CountSubStrInStr(Const Str, SubStr:string):Integer;
Var
i,j:integer;
find:Boolean;
Begin
Result:=0;
For i:=1 to length(str) do
begin
Find:=false;
If (Str[i]=SubStr[1]) and (Length(substr)<=Length(Str)-(i-1)) then
begin
Find:=true;
For j:=2 to Length(SubStr) do
begin
If SubStr[j]<>Str[i+j-1] then
begin
Find:=false;
Break;
End;
End;
End;
If Find Then
Inc(Result);
End;
End;
Но можно подключить к проекту модуль StrUtils в котором есть функция PosEx. Функция PosEx: Параметры:
1. Подстрока, тип string.
2. Строка, тип string.
3. Смещение (индекс символа, с которого начинается поиск), тип integer; Возвращаемое значение:
Тип integer. Функция возвращает индекс первого символа подстроки в строке. Если подстрока не найдена – 0.
Теперь функция CountSubStrInStr будет выглядеть так:
Function CountSubStrInStr(Const Str, SubStr:string):Integer;
var
i:integer;
begin
Result:=0; // Значение по умолчанию 0 (подстрок не найдено).
i:=1; // Поиск начинается с первого символа.
repeat
i:=PosEx(SubStr,Str,i); // Присвоение i результата работы PosEx.
if i<>0 then // Если подстрока найдена…
begin
Inc(Result); //…увеличение результата на единицу…
i:=i+Length(SubStr); //…и увеличение значения i на длину подстроки.
end;
until i=0; //Выход из цикла, если подстрок больше (вообще) не найдено.
end;
Смотрится функция в таком виде куда лучше, да и скорость выполнения значительно выше.
Разбиение предложения на слова
Студент-первокурсник потратит на решение этой задачи минут 40. Более-менее опытный программист - минут 10. Результат будет примерно слудующим:
Procedure DivideString(const Str:string; StrList:TStrings);
Type
Mnoj=set of char;
Var
i:integer;
subs:string;
Сonst
Mn:Mnoj=['А'..'Я', 'а'..'я', '0'..'9', '-', #39];
Begin
subs:='';
for i:=1 to length(str) do
begin
if not (str[i] in Mn) then
Continue;
subs:=subs+str[i];
if (not (str[i+1] in Mn)) or (i=length(str)) then
begin
StrList.Add(subs);
subs:='';
end;
end;
End;
Функция вполне работоспособна и отлично справляется со своими обязанностями, разбивая строку за один проход. Но вместо 10 минут можно потратить одну, использовав в коде функцию ExtractStrings. Функция ExtractStrings: Параметры:
1. Множество символов, используемых в качестве делителей, тип TSysCharSet.
2. Множество символов, которые будут игнорироваться только если они находятся в начале строки, тип TSysCharSet.
3. Строка, тип PChar.
4. Список строк, в который будут добавлены получившиеся в результате разбиения слова, тип TStrings. Возвращаемое значение:
Тип integer. Функция возвращает количество получившихся в результате разбиения строки слов.
Единственный минус этой функции в том, что нельзя обработать символ «’» в сочетании с русскими буквами. То есть если вводится строка «Computed solution d’Alembert’s force», функция возвращает вполне ожидаемые четыре слова. А если ввести «Найденное решение и есть д’Аламберова сила», то функция просто откажется разбивать строку после «’», вернув все оставшиеся слова как одно. То есть, получится пять слов вместо шести.
Математические операции
Возведение в степень
В коде неопытных программистов можно часто встретить следующее:
y:=x*x;
Иногда это полезно для наглядности, но ведь есть функция Sqr.
Функция Sqr: Параметры:
1. Число, тип Extended. Возвращаемое значение:
Тип Extended. Квадрат числа, переданного в функцию.
Согласитесь, код:
y:=Sqr(Sqr(x));
выглядит лучше, чем:
y:=x*x*x*x;
Но еще лучше выглядит такой код:
y:=IntPower(x,4);
Функция IntPower: Параметры:
1. Основание, тип Extended.
2. Степень, тип Integer. Возвращаемое значение:
Тип Extended. Основание, возведенное в степень.
Функция IntPower идеально подходит для возведения в целую степень, если же нужно возвести число в вещественную степень, подойдет функция Power.
Функция Power: Параметры:
1. Основание, тип Extended / Double / Single.
2. Степень, тип Extended / Double / Single. Возвращаемое значение:
Тип Extended / Double / Single. Основание, возведенное в степень.
Конечно, функцию Power можно использовать и для возведения в целую степень, но функция IntPower справится с этим гораздо быстрее.
А на Pascal в свое время приходилось писать примерно такие функции:
function Power (const Base, Exponent:Real):Real
begin
Power:=0;
If 0 < Base then
Power:=Exp(Exponent*Ln(Base));
end;
Работа с массивами
Массив – один из основных типов данных в Pascal и Delphi. Конечно, сейчас в основном используются разнообразные списки, а с появлением .Net 2.0 и Delphi 2009 стало достаточно легко добавлять новые, однако в самом начале знакомства с Delphi программисты в основном используют массивы.
Перебор значений массива
Обычно перебор элементов массива выглядит примерно так:
var
ms: array [0..99] of Integer;
I: Integer;
begin
for I := 0 to 99 do
ShowMessage(IntToStr(ms[I]));
end;
Но если вдруг количество элементов изменится, то придется переписать начальное и конечное значения I. Чтобы об этом не думать, можно использовать функции Low и High:
var
ms: array [0..99] of Integer;
I: Integer;
begin
for I := Low(ms) to High(ms) do
ShowMessage(IntToStr(ms[I]));
end;
«Книга, а в ней кукиш да фига»
Так гласит старая русская пословица, и она вполне справедлива. Не всем книгам надо слепо верить. Некоторые примеры из книг можно смело именовать «пример, как не надо писать код». Не будем углубляться во все тонкости правильного написания кода, об этом можно, и даже нужно, почитать у С. Макконнелла в книге «Совершенный код», а приведем некоторые некачественные примеры из книг.
Примеры с булевскими переменными
Очень часто в программе используются CheckBox’ы. Допустим, в настройках программы могут существовать такие настройки, которые можно изменять только при включенном CheckBox’е. В книгах встречается такой код:
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
if CheckBox1.Checked then
GroupBox1.Enabled:=True
else
GroupBox1.Enabled:=False;
end;
Наверное, эти четыре строчки кода лучше заменить одной:
procedure TForm1.CheckBox1Click(Sender: TObject);
begin
GroupBox1.Enabled:=CheckBox1.Checked;
end;
Встречается похожий пример: по нажатию кнопки становится видимой скрытая панель, а при повторном нажатии она прячется:
procedure TForm1.Button1Click(Sender: TObject);
begin
if Panel1.Visible then
Panel1.Visible:=False
else
Panel1.Visible:=True;
end;
Но куда быстрее и красивее написать так:
procedure TForm1.Button1Click(Sender: TObject);
begin
Panel1.Visible:=not Panel1.Visible;
end;
Бывает нужно, чтобы панель появлялась при нажатии на одну кнопку, а скрывалась при нажатии на другую. Обычно пишут две процедуры:
procedure TForm1.Button1Click(Sender: TObject);
begin
Panel1.Visible:=True;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Panel1.Visible:=False;
end;
Но Delphi – Объектно-Ориентированный Язык, так почему же не воспользоваться всеми прелестями ООП? Если посмотреть на заголовок процедуры, можно увидеть, что ей в качестве параметра передается Sender типа TObject. TObject – это базовый класс, от которого унаследованы все другие классы. Sender – объект, вызывающий процедуру. То есть, внутри процедуры можно определить, кто её вызвал. Проведем эксперимент. Разместим на форме две кнопки (Button1 и Button2) и для первой напишем такой код:
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage((Sender as TButton).Name); //Приведение Sender к классу
//TButton и вывод имени экземпляра, который вызвал процедуру.
end;
Скомпилируем проект и кликнем на кнопку. Появится сообщение с текстом: «Button1». Закроем окно и Event’у второй кнопки присвоим ту же процедуру Button1Click. Вновь скомпилируем проект. Теперь при нажатии на первую кнопку появляется все то же сообщение «Button1», а при нажатии на вторую – «Button2». Теперь понятно, как заменить две процедуры показа/скрытия панели одной:
procedure TForm1.Button1Click(Sender: TObject);
begin
Panel1.Visible:= (Sender as TButton).Name=’Button1’;
end;
Заключение
Здесь рассмотрена мизерная часть того, что хотелось. Поэтому ожидайте новых статей (нумерация разделов будет сохраняться), в которых будут рассматриваться все новые и новые «секреты» и интересные модули.