Давать советы — неблагодарное занятие. Советы никогда никто не слушает.
Возможно, некоторые посчитают их очевидными. Однако, рискну. Меня
несколько удивляет, как много задач программисты решают «в лоб», не
заботясь ни о простоте решения, ни о скорости его выполнения, ни о стиле.
Постараюсь дать несколько советов, как новичкам, так и тем, кто поопытнее.
Совет первый (и главный). Все гениальное — просто.
Обычно самое простое решение задачи — самое правильное. Простое решение
чаще всего найти труднее, но его всегда легче понять, а поняв —
реализовать. Но на этом дело не останавливается. Программы надо еще и
отлаживать, поддерживать и изменять в угоду требованиям пользователей.
Простое решение обычно более эффективно и меньше в объеме. В конце концов,
оно доставляет удовольствие. Что-то щелкает в голове, и ты понимаешь — да,
это ОНО. Ради этого стоит программировать.
Расчеты/таблицы/логика
Предположим, нам нужно написать код, решающий такую задачу:
• если входной аргумент x равен 1, на выходе y:=25;
• если x равен 2, y:=30;
• если x равен 3, y:=35.
Можно выбрать один из трех подходов:
Расчет:
y:=x*5+20;
Таблицы:
const
tab:array[1..3] of integer=(25,30,35)
...
y:=tab[x];
Логика:
case x of
1:y:=25;
2:y:=30;
3:y:=35;
end;
Для данной задачи вычисление результата — самое простое решение. Однако
оно не самое быстрое.
Самое быстрое решение — табличное. Очень часто вычисление синусов и
косинусов заменяется выборкой из таблицы для достижения скорости. И во
многих случаях использовать таблицу проще, чем придумать формулу для
вычисления результата, с помощью которой этот результат может быть
вычислен. И стоит задаче чуть-чуть измениться — например, в первом
варианте вместо 25 нужно получить 26, — нужно придумывать формулу заново
(если это вообще возможно; в задаче случай тривиален, и подобрать формулу
легко). При использовании таблиц нужно всего лишь поменять в таблице одно
число.
Что касается варианта логики, оно обычно самое медленное и очень быстро
разрастается при увеличении количества вариантов.
Минимизация условных операторов
Совет второй. Выбирайте один из трех подходов к решению задачи в таком
порядке:
• расчеты (кроме случаев, когда нужна скорость)
• таблицы
• логика
Использование условных операторов усложняет ваш код. Причем, он имеет
тенденцию очень быстро разрастаться в размерах. Иногда очень сложно бывает
разобраться во всех многократных вложениях if’ов, else и then. Кроме того,
условные операторы замедляют выполнение программы.
Конечно, избежать условных операторов почти невозможно, но максима
программиста такова: по каждому if’у задать себе вопрос: «Что я делаю не
так?»
Совет третий. Не проверяйте то, что уже проверяли.
Возьмем пример, данный в начале статьи, и запишем его операторами if:
if x=1 then y:=25;
if x=2 then y:=30;
if x=3 then y:=35;
Что-то здесь не то. В каждом случае выполняются все три проверки. Если
x=1, зачем проверять варианты 2 и 3? Вместо этого нужно сделать вложенные
проверки:
if x=1 then y:=25
else if x=2 then y:=30
else if x=3 then y:=35;
Совет четвертый. Объединяйте условия вместе.
Многие вложенные структуры if ... then упрощаются с помощью объединения
условий с помощью логических операторов. Например:
if есть_деньги_на_счету then
if последний_день_месяца then
получить_зарплату;
Вместо использования двух if’ов скомбинируем условия с помощью оператора
and:
if есть_деньги_на_счету and последний_день_месяца then
получить_зарплату;
Это ближе к естественному языку и проще, особенно если условий много.
Однако из любого правила есть исключение. Если проверка наличия денег на
вашем счету в банке занимает много времени, лучше сначала проверить, какой
сегодня день, и записать так:
if последний_день_месяца then
if есть_деньги_на_счету then
получить_зарплату;
Зачем проверять, есть ли на счету деньги, если день получения зарплаты еще
не наступил? Смотрите также пятый совет.
Примечание: многие современные компиляторы позволяют генерировать код,
оптимизирующий выполнение объединенных условий. Например, если условие
есть_деньги_на_счету дает в результате ложь, то каким бы ни был результат
условия последний_день_месяца, в результате получим ложь, и вычислять
второе условие не нужно будет.
Совет пятый. Если у вас есть условия с разным весом, вкладывайте условные
операторы так, чтобы первым был тот, который выполнится с большей
вероятностью или является самым легким в вычислении.
Например, мы знаем, что в исходных данных для первой задачи x=3 в
большинстве случаев. Тогда лучше проверить этот вариант первым (смотрите
первый совет). Точно так же следует компоновать условия (то же касается
предыдущего совета), если у вас «умный» компилятор.
Совет шестой. Используйте функции min и max вместо условных операторов.
Очень часто в программах встречаются ситуации, когда необходимо, чтобы
значение некоторой переменной было не меньше 0 (или не больше некоторого
значения). Обычно записывают так:
if x<0 then x:=0;
Гораздо проще использовать функцию max:
x:=max(x,0);
Казалось, мы ничего не выигрываем. Зато
if x<0 then x:=0;
if x>100 then x:=100;
можно записать так:
x:=min(max(x,0),100);
Совет седьмой. Используйте деление вместо условных операторов.
Предположим, у вас есть переменная, которая постоянно увеличивается на 1
и, достигая максимального значения, сбрасывается в 0. Многие делают это
так:
if x<MAX then x:=x+1 else x:=0;
От этого оператора if можно избавиться:
x:=(x+1) mod max;
где mod — взятие остатка от деления. А зачем, спросите вы. Но допустим, вы
не знаете, на какое значение будет увеличиваться x. Может, даже и на
отрицательное, то есть x будет уменьшаться. Как записать это условными
операторами?
x:=x+increment;
if x>=max then x:=increment-1 else
if x<0 then x:=max+increment+1;
Попробуйте-ка разобраться! С вычислением остатка все гораздо проще:
x:=(x+increment+max) mod max;
Вообще, задача не такая уж и простая. А если учесть, что значение
приращения может быть и больше максимального значения x, тут уже можно и
голову сломать.
Совет восьмой. Не используйте флаги, сразу устанавливайте данные.
Если вы устанавливаете флаг только для того, чтобы позже выбрать одно из
двух чисел, то лучше сразу установить само такое число. Это делает
ненужной повторную проверку флага. Зачем проверять что-то еще раз, если мы
уже это делали?
К примеру, у вас есть две сетевые карты. Единственное, чем они отличаются,
это номера портов ввода/вывода. Естественно записать такой код:
var
device:integer; {1 — первая карта, 2 — вторая}
procedure byteout(b:byte);
var
port:integer;
begin
if device=1 then port:=$2658 else port:=$3254;
out(port,b);
end;
...
device:=1; // первая карта
byteout($34);
...
device:=2; // вторая карта
byteout($ff);
...
Переменная device содержит номер карты, с которой мы в данный момент
работаем. Недостаток такого подхода в том, что при посылке каждого байта
производится проверка номера карты. Почему прямо не записать в переменную
номер соответствующего порта?
var
device:integer; {номер порта}
procedure byteout(b:byte);
begin
out(device,b);
end;
...
device:=$2658; // первая карта
byteout($34);
...
device:=$3254; // вторая карта
byteout($ff);
...
Однако неудобно использовать номера портов каждый раз, поэтому объявим
константы:
const card1=$2658;
const card2=$3254;
...
device:=card1; // первая карта
byteout($34);
...
device:=card2; // вторая карта
byteout($ff);
...
Совет девятый. Не используйте флаги, сразу устанавливайте функцию.
Такой прием называется векторизацией. Этот совет полностью аналогичен
предыдущему. Если вы устанавливаете флаг только для последующего выбора
одной из функций, лучше сразу записать адрес самой функции.
К примеру, код для печати символа на принтере отличается от того, который
выводит его на экран. В плохой программе можно было бы написать:
procedure writechar(c:char;device:integer{0-экран, 1-принтер});
begin
if device:=0 then begin
{вывод на экран}
...
end else begin
{печать на принтере}
...
end;
procedure writestr(s:string;device:integer);
var
i:integer;
begin
for i:=1 to length(s) do
writechar(s[i],device);
end;
Это плохо потому, что решение принимается каждый раз, когда печатается
символ. А при печати строки решение будет приниматься для каждого символа
в строке.
Предпочтительнее использовать векторизованное исполнение. Например:
type
tproc=procedure(c:char);
var
writechar:tproc;
procedure print(c:char); // печать на принтере
begin
{код для принтера}
...
end;
procedure scr(c:char); // вывод на экран
begin
{код для экрана}
...
end;
procedure printer;
begin
writechar:=print;
end;
procedure screen;
begin
writechar:=scr;
end;
procedure writestr(s:string);
var
i:integer;
begin
for i:=1 to length(s) do
writechar(s[i]);
end;
Использование такого кода:
screen;
writestr('Вывод на экран');
printer;
writestr('Печать на принтере');
Если количество необходимых к векторизации функций велико,
предпочтительнее таблица функций (см. далее).
Совет десятый. Не устраивайте лишние проверки.
Даже авторы журнала «Мой компьютер» иногда используют такой код:
if flag=true then ...
else ...;
Зачем делать проверку истинности флага лишний раз, если это делает сам
оператор if? Нужно записывать так:
if flag then ...
else ...;
А вместо
if flag=false then ...
else ...;
лучше
if not flag then ...
else ...;
Очень часто встречаешь и такое:
function xxx(a,b:integer):boolean;
begin
if a>b then result:=true
else result:=false;
end;
Налицо лишняя проверка. Надо записывать так:
function xxx(a,b:integer):boolean;
begin
result:=a>b;
end;
Использование таблиц решений.
Совет одиннадцатый. Используйте таблицы решений.
Таблица решений — это таблица, которая содержит данные ( таблица данных)
или адреса функций ( таблица функций). В самом простом случае таблица
решений имеет одно измерение, может иметь и больше. В последнем случае
таблица решений гораздо уместнее структуры управления.
Таблица данных с одним измерением.
В первом нашем примере рассмотрена простая одномерная таблицы данных:
const
tab:array[1..3] of integer=(25,30,35)
Мы просто выбираем из таблицы значение y по значению x, взятому в качестве
индекса в таблице:
y:=tab[x];
Еще пример. Нужно вычислить степени тройки. Решение «в лоб»:
y:=1;
for i:=1 to n do
x:=x*3;
Однако вместо того чтобы вычислять ответ при помощи умножения тройки на
саму себя n раз, можно все такие ответы вычислить заранее и записать в
таблицу:
const
tab:array[0..19] of integer=(1,3,9,27,81,......,1162261467);
{3 в двадцатой степени уже не помещается в 32-битную переменную}
y:=tab[n];
Такое решение гораздо быстрее. Еще раз повторюсь, что часто такие таблицы
применяются для вычисления синусов и косинусов.
Таблица функций с одним измерением.
Предположим, ваша программа должна что-то предпринять при нажатии
какой-либо клавиши. Обычно используют структуру управления типа:
if key=вверх then up else
if key=вниз then down else
.......
Это хорошо, если у вас обрабатываются три-четыре клавиши. Если их больше,
лучше использовать таблицу функций:
type
tproc=procedure;
const
tab:array[0..255] of tproc=(...,up,...,down,...,...);
Конечно, позиция процедур в таблице должна соответствовать коду клавиши.
Тогда вся предыдущая громоздкая структура управления пишется одной
строкой:
tab[key];
Таблица данных с двумя измерениями.
Возьмем такую задачу. У нас есть два флага: fr — принимает значение
истина, когда происходит чтение данных с диска, fw — принимает значение
истина, когда происходит запись данных на диск. Необходимо вывести текст
Чтение, если происходит чтение с диска, Запись, когда происходит запись,
Чтение/запись, если чтение и запись происходит одновременно. И наконец, не
нужно ничего выводить, если ничего не происходит.
Реализация с помощью структур управления:
if fr and fw then label.caption:='Чтение/запись' else
if fr and (not fw) then label.caption:='Чтение' else
if not(fr) and fw then label.caption:='Запись' else
label.caption:='';
Это решение блекнет перед следующим. Применим таблицу данных. Почему-то
мало кто знает, что в качестве индексов в таблице могут выступать и
логические значения:
const
tab:array[boolean,boolean] of string=
(('','Запись'),('Чтение','Чтение/запись'));
...
label.caption:=tab[fr,fw];
Итоги
Использование логики и условных операторов в программировании ведет к
сложному, трудно управляемому и неэффективному коду. Вместо любого
условного оператора можно использовать таблицу функций. То бишь вместо
procedure сделать_это;
begin
...
end;
procedure сделать_то;
begin
...
end;
...
if условие then сделать_это else сделать_то;
использовать
type tproc=procedure;
const
tab:array[boolean] of tproc=(сделать_то,сделать_это);
...
tab[условие]; |