Давать советы – неблагодарное занятие. Советы никогда никто не слушает. Возможно, некоторые посчитают их очевидными. Однако, рискну. Меня несколько удивляет, как много задач программисты решают “в лоб”, не заботясь ни о простоте решения, ни о скорости его выполнения, ни о стиле. Здесь я постараюсь дать несколько советов новичкам и не только.
Программирование компьютеров может свести с ума. Другие профессии дают Вам прекрасные возможности наблюдать осязаемые результаты Ваших усилий. Часовщик может смотреть на свои зубчики и колесики, швея — на швы, ровно ложащиеся после каждого взмаха иглы. Но программист проектирует, строит и ремонтирует нечто воображаемое, призрачные механизмы, ускользающие от восприятия органами чувств. Наша работа происходит не в ОЗУ, не в программе-редакторе, а внутри нашей головы. Л.Броуди. Способ мышления – Форт.

Совет первый (и главный). Все гениальное — просто.

Обычно самое простое решение задачи – самое правильное. Простое решение чаще всего найти труднее, но его всегда легче понять, а поняв – реализовать. Но на этом дело не останавливается. Программы надо еще и отлаживать, поддерживать и изменять в угоду требованиям пользователей. Простое решение обычно более эффективно и меньше в объеме. В конце концов, оно доставляет удовольствие. Что-то щелкает в голове, и ты понимаешь – да, это ОНО. Ради этого стоит программировать.

Расчеты / таблицы / логика

Предположим, нам нужно написать код, решающий такую задачу: если входной аргумент x равен 1, на выходе y:=25 если x равен 2, y:=30 если x равен 3, y:=35 Можно выбрать один из трех подходов:

Расчет

1
y:=x*5+20;

Таблицы

1
2
3
4
const
tab:array[1..3] of integer=(25,30,35);
...
y:=tab[x];

Логика

1
2
3
4
5
case x of
    1:y:=25;
    2:y:=30;
    3:y:=35;
end;
Для данной задачи вычисление результата — самое простое решение. Однако оно не самое быстрое. Самое быстрое решение – табличное. Очень часто вычисление синусов и косинусов заменяется выборкой из таблицы для достижения скорости. И во многих случаях использовать таблицу проще, чем придумать формулу для вычисления результата, с помощью которой этот результат может быть вычислен. И стоит задаче чуть-чуть измениться, например, в первом варианте вместо 25 нужно получить 26, как нужно придумывать формулу заново (если это вообще возможно. В задаче случай тривиален, и подобрать формулу легко). При использовании таблиц нужно всего лишь поменять в таблице одно число. Что касается варианта логики, оно обычно самое медленное и очень быстро разрастается при увеличении количества вариантов.

Совет второй. Выбирайте один из трех подходов к решению задачи в таком порядке:

  1. расчеты (кроме случаев, когда нужна скорость)
  2. таблицы
  3. логика

Минимизация условных операторов

Зачем это делать? Использование условных операторов усложняет Ваш код. Причем он имеет тенденцию очень быстро разрастаться в размерах. Иногда очень сложно бывает разобраться во всех многократных вложениях if-ов, else и then. Кроме того, условные операторы замедляют выполнение программы. Конечно, избежать условных операторов почти невозможно, но Максима программиста такова: по каждому if-у задать себе вопрос: «Что я делаю не так?»

Совет третий. Не проверяйте то, что уже проверяли.

Возьмем пример, данный в начале статьи и запишем его операторами if:
1
2
3
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;[/cc]

Совет четвертый. Объединяйте условия вместе.

Многие вложенные структуры if … then упрощаются с помощью объединения условий с помощью логических операторов. Например:
1
2
3
if есть_деньги_на_счету then
    if последний_день_месяца then
        получить_зарплату;
Вместо использования двух if-ов скомбинируем условия с помощью оператора and:
1
2
if есть_деньги_на_счету and последний_день_месяца then
    получить_зарплату;
Это ближе к естественному языку и проще, особенно если условий много. Однако из любого правила имеется исключение. Если проверка наличия денег на Вашем счету в банке занимает много времени, лучше сначала проверить, какой сегодня день, и записать так:
1
2
3
if последний_день_месяца then
    if есть_деньги_на_счету then
        получить_зарплату;
Зачем проверять, если на счету деньги, если день получения зарплаты еще не наступил? Смотрите также пятый совет.
Примечание: многие современные компиляторы позволяют генерировать код, оптимизирующий выполнение объединенных условий. Например, если условие есть_деньги_на_счету дает в результате “ложь”, то каким бы ни был результат условия последний_день_месяца, в результате получим “ложь”, и вычислять второе условие не нужно.

Совет пятый. Если у вас есть условия с разным весом, вкладывайте условные операторы так, чтобы первым был тот, который выполняется наиболее вероятно или является самым легким в вычислении.

Например, мы знаем, что в исходных данных для первой задачи x=3 в большинстве случаев. Тогда лучше проверить этот вариант первым (смотрите первый совет). Точно так же следует компоновать условия (как в предыдущем совете), если у вас “умный” компилятор.

Совет шестой. Используйте функции MIN и MAX вместо условных операторов.

Очень часто в программах встречаются ситуации, когда необходимо, чтобы значение некоторой переменной было не меньше 0 (или не больше некоторого значения). Обычно записывают так:
1
if x<0 then x:=0;
Гораздо проще использовать функцию max:
1
x:=max(x,0);
Казалось, мы ничего не выигрываем, но посмотрите на такое:
1
2
if x<0 then x:=0;
if x>100 then x:=100;
Можно записать так:
1
x:=min(max(x,0),100);

Совет седьмой. Используйте деление вместо условных операторов.

Предположим, у вас есть переменная, которая постоянно увеличивается на 1, и достигая максимального значения, сбрасывается в 0. Многие делают это так:
1
2
inc(x);
if x>=Max then x:=0;
От этого оператора if можно избавиться:
1
x:=(x+1) mod Max;
где mod – взятие остатка от деления. А зачем, спросите вы. Но допустим, вы не знаете, на какое значение будет увеличиваться x. Может, даже и на отрицательное, то есть x будет уменьшаться. Как записать это условными операторами?
1
2
3
x:=x+increment;
if x>=Max then x:=increment-1 else
if x<0 then x:=Max+increment+1;
Попробуйте-ка разобраться! С вычислением остатка все гораздо проще:
1
x:=(x+increment+Max) mod Max;
Вообще, задача не такая уж и простая. А если значение приращения может быть и больше максимального значения x, тут уже можно и голову сломать.

Совет восьмой. Не используйте флаги, сразу устанавливайте данные.

Если вы устанавливаете флаг только для того, чтобы позже выбрать одно из двух чисел, то лучше сразу установить само такое число. Это делает ненужной повторную проверку флага. Зачем проверять что-то еще раз, если мы уже это делали? К примеру, у Вас есть две сетевые карты. Единственное, чем они отличаются, это номера портов ввода/вывода. Естественно записать такой код:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 содержит номер карты, с которой мы в данный момент работаем. Недостаток такого подхода в том, что при посылке каждого байта производится проверка номера карты. Почему прямо не записать в переменную номер соответствующего порта?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var
device:integer; {номер порта}

procedure ByteOut(b:byte);
begin
    out(device,b);
end;
...
device:=$­­2658; // первая карта
ByteOut($­­34);
...
device:=$­­3254; // вторая карта
ByteOut($­­ff);
...
Однако, неудобно использовать номера портов каждый раз, поэтому объявим константы:
1
2
3
4
5
6
7
8
9
const Card1=$­­2658;
const Card2=$­­3254;
...
device:=Card1; // первая карта
ByteOut($­­34);
...
device:=Card2; // вторая карта
ByteOut($­­ff);
...

Совет девятый. Не используйте флаги, сразу устанавливайте функцию.

Такой прием называется векторизацией. Этот совет полностью аналогичен предыдущему. Если вы устанавливаете флага только для последующего выбора, какую из функций выбрать, лучше сразу записать адрес самой функции. К примеру, код для печати символа на принтере отличается от кода, который выводит ого на экран. В плохой программе можно было бы написать:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
procedure WriteChar(c:char;device:integer{0-экран, 1-принтер});
begin
    if device:=0 then begin
        {вывод на экран}
        ...
    end else begin
        {печать на принтере}
        ...
    end;
end;

procedure WriteStr(s:string;device:integer);
var
    i:integer;
begin
    for i:=1 to Length(s) do
        WriteChar(s[i],device);
end;
Это плохо потому, что решение принимается каждый раз, когда печатается символ. А при печати строки решение будет приниматься для каждого символа в строке. Предпочтительнее использовать векторизованное исполнение, например:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
Использование такого кода:
1
2
3
4
5
Screen;
WriteStr('Вывод на экран');

Printer;
WriteStr('Печать на принтере');
Если количество необходимых к векторизации функций велико, предпочтительнее таблица функций (см. далее).

Совет десятый. Не устраивайте лишние проверки

Даже авторы журнала “Мой компьютер” иногда используют такой код:
1
if flag=true then ... else ...;
Зачем делать проверку истинности флага лишний раз, если это делает сам оператор if? Нужно записывать так:
1
if flag then ... else ...;
А вместо
1
if flag=false then ... else ...;
лучше
1
if not flag then ... else ...;
Очень часто встречаешь и такое:
1
2
3
4
5
function xxx(a,b:integer):boolean;
begin
    if a > b then result:=true
    else result:=false;
end;
Налицо лишняя проверка. Надо записывать так:
1
2
3
4
function xxx(a,b:integer):boolean;
begin
    result:=a > b;
end;

Использование таблиц решений.

Совет одиннадцатый. Используйте таблицы решений.

Таблица решений — это таблица, которая содержит данные («таблица данных») или адреса функций («таблица функций»). В самом простом случае таблица решений имеет одно измерение, может иметь и больше. Таблица решений для случая нескольких измерений гораздо лучше, чем структура управления.

Таблица данных с одним измерением.

В первом нашем примере рассмотрена простая одномерная таблицы данных.
1
const tab:array[1..3] of integer=(25,30,35);
Мы просто выбираем из таблицы значение y по значению x, взятому в качестве индекса в таблице:
1
y:=tab[x];
Еще пример. Нужно вычислить степени тройки. Решение “в лоб”:
1
2
3
x:=1;
for i:=1 to n do
    x:=x*3;
Однако, вместо вычисления ответа при помощи умножения тройки на саму себя «n» раз, можно все такие ответы вычислить заранее и записать в таблицу:
1
2
3
4
const tab:array[0..19] of integer=(1,3,9,27,81,......,1162261467);
{3 в двадцатой степени уже не помещается в 32-битную переменную}

y:=tab[n];
Такое решение гораздо быстрее. Еще раз повторюсь, что часто такие таблицы применяются для вычисления синусов и косинусов.

Таблица функций с одним измерением.

Предположим, Ваша программа должна что-то предпринять при нажатии какой-либо клавиши. Обычно используют структуру управления типа:
1
2
3
if key=вверх then Up else
if key=вниз then Down else
.......
Хорошо, если у вас обрабатываются три-четыре клавиши. Если их больше, лучше использовать таблицу функций:
1
2
3
type
TProc=procedure;
const tab:array[0..255] of Tproc=(...,Up,...,Down,...,...);
Конечно, позиция процедур в таблице должна соответствовать коду клавиши. Тогда вся предыдущая громоздкая структура управления пишется одной строкой:
1
tab[key];

Таблица данных с двумя измерениями.

Возьмем такую задачу. У нас есть два флага: fr – принимает значение “истина”, когда происходит чтение данных с диска, fw – принимает значение “истина”, когда происходит запись данных на диск. Необходимо вывести текст “Чтение”, если происходит чтение с диска, “Запись”, когда происходит запись, “Чтение/запись”, если чтение и запись происходит одновременно. И наконец, не нужно ничего выводить, если ничего не происходит. Реализация с помощью структур управления:
1
2
3
4
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:='';
Это решение блекнет перед следующим. Применим таблицу данных. Почему-то мало кто знает, что в качестве индексов в таблице могут выступать и логические значения:
1
2
3
4
const tab:array[boolean,boolean] of string=
(('','Запись'),('Чтение','Чтение/запись'));
...
Label.Caption:=tab[fr,fw];

ИТОГИ

Использование логики и условных операторов в программировании ведет к сложному, трудно управляемому и неэффективному коду. А вместо любого условного оператора можно использовать таблицу функций: вместо
1
2
3
4
5
6
7
8
9
10
procedure сделать_это;
begin
    ...
end;
procedure сделать_то;
begin
    ...
end;
...
if условие then сделать_это else сделать_то;
использовать
1
2
3
4
type Tproc=procedure;
const tab:array[boolean] of Tproc=(сделать_то,сделать_это);
...
tab[условие];
Интересно посмотреть на Вашу реакцию. Пишите письма!

Страница просмотрена 955 раз(а)