Pull to refresh

Мои любимые ошибки в программировании

Reading time 13 min
Views 8K
Original author: Paul Tero
За мою карьеру программиста я сделал огромное количество ошибок в нескольких различных языках. На самом деле, если я пишу 10 или больше строчек кода, которые работают с первого раза, я становлюсь подозрительным и принимаюсь тестировать его более тщательно, чем обычно, предполагая найти ошибку в синтаксисе, или неверную ссылку на массив, или неправильно записанную переменную, или что-то ещё.

Мне нравится подразделять эти ошибки на три большие группы: провалы, погрешности и недочеты. Провал – это когда ты сидишь тупо смотришь на экран и тихо говоришь «ой»; вещи вроде удаления базы данных или целого сайта, записи чего-либо поверх результата трехдневной работы, или случайной отсылки письма 20 тысячам человек.

Погрешности могут быть различными: от простых синтаксических ошибок (например, забыть поставить } ) до критических ошибок и ошибок в вычислениях.

Когда ошибка настолько неочевидна и неуловима, что это почти прекрасно, я зову это недочетом. Такое случается, когда кусок кода сталкивается с совершенно непредсказуемыми и весьма маловероятными обстоятельствами. Вы откидываетесь на спинку стула и думаете «Ого!», словно увидев яркую радугу или падающую звезду.

Невыключенный режим отладки

Первые две ошибки, о которых я упомяну в этой статье, были полноценными провалами.

Когда я впервые занялся фрилансом, я написал ряд PHP-библиотек для обработки запросов баз данных, форм и шаблонов страниц. Режим отладки был встроен в библиотеки на довольно глубоком уровне, который зависел от глобальной переменной $DEBUG.

Также я сохранял локальную копию каждого крупного сайта, над которым я работал, для разработки, отладки и тестирования. Таким образом, когда возникала проблема, я всегда мог установить $DEBUG=1; который сообщал мне различные вещи, например, все выполняемые операторы базы данных. Я редко использовал этот метод поиска ошибок для онлайн-сайтов, он был предназначен для локального использования.

Но однажды я работал поздно ночью, исправляя небольшую проблему на одном популярном сайте, занимающемся электронной коммерцией. Я поставил $DEBUG=1; вверху нескольких страниц и переключался между ними. От усталости в голове все смешалось, и в конце концов я каким-то образом добавил переменную отладки к самой важной странице сайта, которую пользователи видят сразу после нажатия кнопки «Оплатить сейчас», и в такой виде загрузил на рабочую версию сайта.

На следующее утро я рано ушел из дому, и, вернувшись в 9 вечера, обнаружил на автоответчике 12 сообщений, одно другого раздраженнее, и гораздо больше и-мейлов. В течение примерно 20 часов пользователи, нажимавшие на «Оплатить сейчас», видели приблизительно следующее:
image
Я потратил всего 10 секунд на исправление ошибки, но куда больше времени ушло на извинения перед клиентом за целый день упущенных заказов.

Усвоенные уроки

Я поразмыслил над этим случаем и установил, что следует:
1. Избегать работать поздно ночью
2. Проводить полное тестирование каждый раз, когда я делаю даже незначительные изменения в обработке заказа
3. Убедиться, что отчеты об отладке никогда не появятся на работающем сайте
4. Снабдить клиента контактными данными для чрезвычайных обстоятельств.

Внимательная отладка

В связи с третьим пунктом я написал несколько функций, чтобы показ сообщений о дебаггинге выводился только для меня:

function CanDebug() {
 global $DEBUG;
 $allowed = array ('127.0.0.1', '81.1.1.1');
 if (in_array ($_SERVER['REMOTE_ADDR'], $allowed)) return $DEBUG;
 else return 0;
}
function Debug ($message) {
  if (!CanDebug()) return;
  echo '<div style="background:yellow; color:black; border: 1px solid black;';
  echo 'padding: 5px; margin: 5px; white-space: pre;">';
  if (is_string ($message)) echo $message;
  else var_dump ($message);
  echo '</div>';
}

Массив $allowed содержит мой IP адрес для локального тестирования (127.0.0.1) и внешний IP.
Теперь я могу выводить вещи вроде:

$DEBUG = 1;
Debug ("The total is now $total"); //about a debugging message
Debug ($somevariable); //output a variable
Debug ("About to run: $query"); //before running any database query
mysql_query ($query);

И быть увереным, что никто кроме меня не увидит никаких сообщений об отладке. При условии, что вышеупомянутые переменные указаны, код будет выглядеть так:
image
Для большей безопасности я также мог перенести сообщения об ошибках внутрь HTML-комментариев, но тогда мне бы пришлось долго копаться в коде, чтобы найти нужный кусок.
У меня есть другой полезный фрагмент кода, который можно поставить в верхней части страницы или конфигурационного файла, чтобы все PHP уведомления, предупреждения и ошибки были видны только мне. Ошибки и предупреждения будут записаны в логах, но не показаны на экране.

if (CanDebug()) {ini_set ('display_errors', 1); error_reporting (E_ALL);}
else {ini_set ('display_errors', 0); error_reporting (E_ALL & ~E_NOTICE);}

Дебаггеры

Подобный метод удобен для быстрого нахождения ошибок в строго определенных фрагментах кода. Также существуют различные инструменты для отладки, такие как FirePHP and Xdebug, способные дать огромное количество информации о коде. Они также могут действовать невидимо, выводя список всех вызовов функций в лог-файл, без показа пользователю. Xdebug может быть использован так:

ini_set ('xdebug.collect_params', 1);
xdebug_start_trace ('/tmp/mytrace');
echo substr ("This will be traced", 0, 10);
xdebug_stop_trace();

Этот код регистрирует все вызовы функций и их параметры в файле /tmp/mytrace.xt, который выглядит следующим образом:
image
Xdebug также показывает гораздо больше информации о любом PHP предупреждении или ошибке. Однако его необходимо устанавливать на сервере, так что это скорее всего неосуществимо для большинства хостингов.
FirePHP, с другой стороны, работает как PHP-библиотека, взаимодействующая с аддоном Firebug. Вы можете выводить информацию о дебаггинге из PHP прямо в консоль Firebug  — опять-таки невидимо для пользователя.
В обоих методах функция типа вышеуказанной CanDebug все так же полезна для того, чтобы трассировки стека и создание лог-файлов не были доступны каждому обладателю Firebug.

Выключение режима отладки

Отладка скриптов электронной почты более запутанная. Проверить, посылает ли скрипт письмо правильным образом, определенно сложно без реальной отсылки письма. Что я однажды и сделал по ошибке.

Несколько лет назад меня попросили создать массовый скрипт для электронной почты для рассылки ежедневных e-мейлов более чем 20 тысячам подписчиков. Во время разработки я использовал что-то похожее на функцию CanDebug, чтобы иметь возможность протестировать скрипт без отсылки письма. Функция отсылки и-мейла выглядела примерно так:

function SendEmail ($to, $from, $subject, $message) {
  if (CanDebug() >= 10) Debug ("Would have emailed $to:\n$message");
  else {
    if (CanDebug()) {$subject = "Test to $to: $subject"; $to = "test@test.com";}
    mail ($to, $subject, $message, "From: $from");
  }
}

Если я ставил $DEBUG=1, скрипт посылал е-мейлы (все 20 тысяч) на тестовый адрес, который я мог проверить. Если я ставил $DEBUG=10, он сообщал мне, что пытается отослать е-мейл, но на самом деле ничего не отсылал. Вскоре после запуска со скриптом начались проблемы. Я думаю, у него закончилась память из-за малопроизводительной обработки информации 20000 раз подряд. В какой-то момент я углубился в исправление какой-то ошибки, забыл о своей переменной $DEBUG (или же мой внешний IP не вовремя изменился) и случайно отправил письма 20 тысячам человек.

Я принес свои извинения агентству, на которое работал, но к счастью никаких особенных последствий не было. Наверное, многие письма были заблокированы спам-фильтром. Или, возможно, получатели были просто довольны, что письмо было пустым.

Усвоенные уроки

Я очень порадовался тому, что просто оставил слово “test” в качестве темы и содержания письма, а не какое-нибудь высказывание, отражающее мое недовольство возникшим багом. Я усвоил, что надо:
1. Быть особенно осторожным, когда тестируешь массовые скрипты для электронной почты, — проверять, работает ли режим отладки.
2. Посылать тестовые и-мейлы как можно меньшему количеству людей.
3. Всегда писать что-либо вежливое в тексте письма, например, «Пожалуйста, проигнорируйте это тестовое сообщение». Нежелательно писать что-то вроде «Мой клиент — дурень» — мало ли это прочитают 20 тысяч ничего не подозревающих инвесторов.

Пустая страница PHP

Теперь мы добрались из царства провалов до области малозаметных ошибок. Если вы хотите увидеть ошибку, от которой трудно избавиться, похороните нижеследующее где-то в глубинах своего кода:

function TestMe() {TestMe();}
TestMe();

В зависимости от браузера и версий Apache и PHP на сервере вы можете получить пустую страницу, “This Web page is not available,” критическую ошибку, связанную с недостатком памяти, или предложение «Сохранить» или «Открыть» страницу:
image
Это по существу вызывает бесконечную рекурсию, которая может стать причиной недостатка памяти и/или прекращения работы серверного потока. Если он прекращает работать, в логах ошибок может остаться небольшой след:
[Mon Jun 06 18:24:10 2011] [notice] child pid 7192
exit signal Segmentation fault (11)

Это дает нам указание на то, где и почему произошла ошибка. И все быстрые методы дебаггинга с добавлением строк вывода в разных местах не дадут особого результата, поскольку пока проблемный код приводится в исполнение, целая страница не будет работать. В основном, это происходит из-за того, что PHP посылает браузеру сгенерированный HTML лишь периодически. Поэтому добавление множества выражений flush(); по крайней мере покажет вам, что ваш скрипт делал непосредственно перед рекурсивной ошибкой.
Конечно, код, вызывающий эту ошибку, может быть куда изощренней, чем показанный выше. Он может включать методы, вызывающие классы в других классах, которые отсылают обратно к изначальным классам. И эта ошибка может происходит лишь в трудновоспроизводимых обстоятельствах, и только потому что вы изменили что-то еще где-либо еще.

Усвоенные уроки

1. Знать расположения логов ошибок на случай, если там что-нибудь будет записано.
2. В подобных ситуациях stack-tracing дебаггеры вроде Xdebug могут быть очень полезны.
3. В противном случае, приберегите кучу времени, чтобы пройти весь код, строку за строкой, и закомментировать ненужные части, пока он не заработает.

Неправильный тип переменной

Эта ошибка часто случается в базах данных. Если даны эти SQL операторы…

CREATE TABLE products (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(60),
  category VARCHAR(10),
  price DECIMAL(6,2)
);
INSERT INTO products VALUES (1, 'Great Expectations', 'book', 12.99);
INSERT INTO products VALUES (2, 'Meagre Expectations', 'cd', 2.50);
INSERT INTO products VALUES (3, 'Flared corduroys', 'retro clothing', 25);

… угадайте, что будет возвращаться, если вы запустите следующее?

SELECT * FROM products WHERE category='retro clothing';

Ответ – ничего, поскольку колонка категории длиной всего в 10 символов, так что категория последнего продукта урезана до retro clot. Неожиданное исчезновение недавно измененных продуктов или новых элементов меню может внести большую неразбериху. Но обычно это довольно легко исправить:

ALTER TABLE products MODIFY category VARCHAR(30);
UPDATE products SET category='retro clothing' WHERE category='retro clot';

image
Я сделал более серьезную ошибку при работе над своим первым крупным сайтом электронной коммерции. В конце процесса заказа сайт просил клиента ввести реквизиты кредитной карты и затем вызывал Java-программу, которая посылала запрос о платеже в систему Barclays ePDQ. Сумма исчислялась в пенсах. Я не был хорошо знаком с Явой, поэтому взял за основу найденный образец кода, который представлял сумму как
short total;
Java-программа вызывалась из командной строки. Если она не возвращала ничего, то транзакция считалась выполненной, клиент получал и-мейл и заказ был выполнен. Если при проверке кредитной карты возникала ошибка, программа возвращала сообщения типа «Карта не авторизована» или «Карта не прошла проверку на подлинность».
Короткие целые числа могут хранить в себе значения между -32768 и +32767. Эти числа казались огромными. Но я не обратил внимания на то, что сумма исчислялась в пенсах, а не фунтах, то есть, наибольшей возможной суммой было £327.67. И самое худшее, что если сумма заказа была больше этой, Java-программа просто прекращала работу и не возвращала ничего. Это выглядело точно так же, как и удачно завершенный заказ, и далее процесс покупки шел как обычно. Ошибка была замечена то ли бухгалтерией, то ли бдительным и честным покупателем только через несколько месяцев, после нескольких больших неоплаченных заказов.

Усвоенные уроки

1. Назначая тип колонке в базе данных или переменной, надо быть предусмотрительным.
2. Надо убедиться, что успешное завершение программы отличается от программы, перестающей работать.

«Ошибки одного пенса»

Среди моих любимых ошибок можно назвать те, что вызывают расхождение всего лишь в 1 пенс (цент, эре или другую монету). Они мне нравятся тем, что их очень сложно отследить, и они часто сводятся к ошибке в округлении.
Несколько лет назад мне нужно было создать для одного сайта быструю функцию в JavaScript, выводящую количество денег. Я использовал это:

<script type="text/javascript">
function GetMoney (amount) {return Math.round (amount * 100) / 100;}
</script>

Однако вскоре выяснилось, что такие суммы как 1.20 выводились на экран в виде 1.2, что выглядело непрофессионально. Поэтому я поменял код таким образом:
<script type="text/javascript">
function GetMoney (amount) {
  var pounds = Math.floor (amount);
  var pence = Math.round (amount * 100) % 100;
  return pounds + '.' + (pence < 10 ? '0' : '') + pence;
}
</script>

Главное различие — лишний 0 в последней строке. Но так как теперь пенс вычисляется отдельно, оператор % нужен, чтобы получить остаток, когда количество делится на 100. Попробуйте найти такие маловероятные обстоятельства, при которых этот код вызовет ошибку.
Это случилось на сайте, где продавали бисер. Тогда я узнал, что бисер может продаваться в различных формах и количествах, включая изготовленные на заказ смеси, содержащие дробные значения. Однажды клиент купил 1.01 предмета, стоившего £4.95, и заплатил всего £4.00. Поскольку сумма была определена как 4.9995, программа округлила пенсы до 100, а % 100 оставило 0 пенсов. Таким образом, заплаченная сумма снизилась до 4 фунтов.
image
Простой недочет в округлении, где за 101 бусину, проданную по £4.95 за сотню, заплатили £4 вместо £5.
Я быстро поправил код:

<script type="text/javascript">
function GetMoney (amount) {
  var pounds = Math.floor (amount);
  var pence = Math.floor (amount * 100) % 100;
  return pounds + '.' + (pence < 10 ? '0' : '') + pence;
}
</script>

Впрочем, это не было удачным исправлением, поскольку оно округляло £4.9995 до £4.99, что мешало синхронизации с любыми соответствующими вычислениями со стороны сервера. Еще хуже было то, что в случае заказа 0.7 чего-либо, стоившего £1.00, сумма выходила 69 пенсов вместо 70! Это происходило потому что такие числа с плавающей запятой, как 0.7 представляются в бинарном коде скорее как 0.6999999999999999, что затем будет уменьшено до 69 пенсов вместо округления до 70.
Это настоящая «однопенсовая ошибка». Чтобы исправить ее, я добавил еще одно округление в начале:

<script type="text/javascript">
function GetMoney (amount) {
  var pence = Math.round (100 * amount);
  var pounds = Math.floor (pence / 100);
  pence %= 100;
  return pound + '.' + (pence < 10 ? '0' : '') + pence;
}
</script>

Теперь у меня было четыре строчки весьма сложного кода, чтобы сделать одну очень простую вещь. Сегодня, пока я писал эту статью, я обнаружил встроенную в Javascript функцию, которая справится со всем этим:

<script type="text/javascript">
function GetMoney (amount) {return amount.toFixed (2);}
alert (GetMoney (4.9995) + ' ' + GetMoney (0.1 * 0.7));
</script>


Предоставление скидки с PayPal

PayPal – это «однопенсовая ошибка», которая ждет своего времени. Многие сайты предлагают коды, которые дают скидку в некоторое количество процентов от суммы заказа. Она высчитывается в самом конце. Если вы заказали 2 предмета по 95 пенсов, общая сумма будет £1.90, и вы получите скидку в 19 пенсов, следовательно, заплатите £1.71.
Однако PayPal не поддерживает такой тип скидок. Если вы хотите, чтобы PayPal показывал предметы в вашей покупательской корзине, вам надо отдельно посчитать цену и количество каждого из них:

<input name="item_name_1" type="hidden" value="My Difficult Product" />
<input name="amount_1" type="hidden" value="0.99" />
<input name="quantity_1" type="hidden" value="1" />

Таким образом, вы должны получить скидку за каждый предмет отдельно. 10% скидки от 95 пенсов оставляет 85.5 пенсов. PayPal не оперирует дробными числами, поэтому вам надо округлить их до 86 пенсов, что дает общую сумму £1.72 в PayPal. Если округлять до 85p, общая сумма будет £1.70.
Чтобы решить эту проблему, мне также пришлось высчитывать скидку для каждого предмета в отдельности. Вместо обычного расчета 10% × £1.90, код аккумулирует скидку от предмета к предмету, каждый раз используя всю сумму пенсов. При условии, что $items это PHP массив предметов заказа:

$discount = 0; $discountpercent = 10;
foreach ($items as $item) {
 $mydiscount = floor ($item->price * $discountpercent) / 100;
 $item->priceforpaypal = $item->price - $mydiscount;
 $discount += $mydiscount * $item->quantity;
}

Усвоенные уроки

1. Не изобретайте колесо, даже очень маленькие колеса, которые выглядят просто.
2. Если у вас появляется расхождение в 1 пенс, проверьте, где и как цифры округляются.
3. Избегайте представления цен с помощью переменных формата float, если это возможно. Вместо этого для пенсов и центов используйте целые числа, а в базах данных используйте тип переменных с фиксированной запятой — DECIMAL.
Перевод часов

Я бы не назвал эту ошибку «погрешностью». Она появляется при особых редких обстоятельствах, поэтому с точки зрения программиста это скорее «недочёт». Она выходят за границы всего, что программист в состоянии предусмотреть.
Можете ли вы угадать, что не так с этой вроде бы безобидной строкой кода, которая выделяет заказы, выполненные более одной недели назад?

mysql_query ("SELECT * FROM orders WHERE completeddate < '" .
  date ('Y-m-d H:i:s', (time() - 7 * 86400 + 600)) . "'")

Я использовал похожую строку в системе для повторяющегося еженедельного заказа. Она выбирала заказы, выполненные на прошлой неделе, дублировала их и оформляла их для текущей недели. 86,400 – количество секунд в одном дне, так что time() — 7 * 86400 было ровно неделю назад, а +600 добавляет 10 минут запасного времени.
Это был малобюджетный метод реализации повторяющихся заказов. Если бы у меня было больше времени, я бы создал отдельную таблицу и/или покупательскую корзину для разграничения повторяющихся и неповторяющихся заказов. Получилось так, что этот код работал хорошо в течение нескольких месяцев и по загадочным причинам вышел из строя в конце марта.
Устранение недочета и его последствий заняло кучу времени, пришлось выполнять заказы вручную. Еще больше времени ушло на выявление причины, особенно из-за того, что я должен был заставить целый сайт считать, что на дворе другой день.
Я, в общем-то, уже проговорился о причине в названии раздела: я забыл взять в расчет перевод часов на летнее время, когда одна неделя меньше, чем 7*86400 seconds.
Сравните следующие три способа получения даты ровно недельной давности. Последний – самый элегантный. Я лишь недавно обнаружил его:

$time = strtotime ('28 March 2011 00:01');
echo date ('Y-m-d H:i:s', ($time - 7 * 86400)) . '<br/>';
echo date ('Y-m-d H:i:s', mktime (date ('H', $time), date ('i', $time), 0,
  date ('n', $time), date ('j', $time) - 7, date ('Y', $time)));
echo date ('Y-m-d H:i:s', (strtotime ('-1 week', $time))) . '<br/>';

Усвоенные уроки

Из подобной ошибки трудно сделать общие выводы, но определенный урок был усвоен:
1. На сайтах с чем-либо повторяющимся не забывайте принимать во внимание часовые пояса и перевод часов.
2. Опять-таки, не изобретайте колесо.

Заключение

Ошибки в программировании бывают разных форм и размеров. В данной статье они колеблются от вполне очевидных провалов до невероятно тонких просчетов. И, похоже, что все они подтверждают закон Мёрфи: если неприятность может случиться, она обязательно случится.

Однако, на каждую найденную, заявленную и исправленную ошибку приходится, возможно, несколько оставшихся в коде. Они могут быть не найдены (потому что необычные условия, которые ее вызывают, никогда не воспроизводились), о них может быть не сообщено (поскольку большинство пользователей не утруждаются отсылкой отчета об ошибке), или же они не исправлены из-за излишних денежных или временных затрат.

Кроме того, ошибки чаще встречаются на популярных сайтах – в основном потому, что они делаются множеством людей, но также потому, что исправление одной ошибки может повлечь за собой появление новых в других местах. А значит, следует думать наперед и внимательно исправлять ошибки.
Tags:
Hubs:
+54
Comments 108
Comments Comments 108

Articles