1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-eloquent-js-3e-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
8.md 31 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 29.11.2024 02:25 369bafd

Баги и ошибки

Перевод Brian Kernighan и P.J. Plauger из книги «Элементы стиля программирования»

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

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

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

Язык

Компьютер может автоматически указать нам на многие ошибки, если он достаточно понимает, что мы пытаемся сделать. Здесь JavaScript становится препятствием. Его концепции привязки и атрибутов довольно расплывчаты, поэтому редко обнаруживаются орфографические ошибки до фактического запуска программы. Тем не менее, он позволяет вам делать некоторые бессмысленные вещи, например, вычислять true *'monkey' без ошибок.

JavaScript выдаёт некоторые ошибки. Написание программы, которая не соответствует синтаксису языка, немедленно приводит к ошибке компьютера. Другие вещи, такие как вызов чего-то, что не является функцией, или поиск атрибута в неопределённом значении, приводят к ошибкам при попытке выполнения операции.

Однако при обработке бессмысленных вычислений JavaScript просто возвращает результат вроде NaN (не число) или undefined. Программа будет считать, что её исполняемый код работает нормально, и продолжит работу, но проблемы возникнут позже, когда уже многие функции использовали это бессмысленное значение. Во время выполнения программы также могут не возникнуть ошибки, а только появиться некорректный вывод программы. Поиск источника таких ошибок очень сложен.

Процесс поиска ошибок или багов в программе называется отладкой.

Строгий режим

После включения строгого режима (strict mode) JavaScript становится более строгим при выполнении кода. Мы можем включить строгий режим, поместив строку "use strict" в начало файла или тела функции. Вот пример кода:

function canYouSpotTheProblem() {
  "use strict";
  for (counter = 0; counter < 10; counter++) {
    console.log("Happy happy");
  }
}

canYouSpotTheProblem();
// → ReferenceError: counter is not defined

Обычно, когда вы забываете поставить let перед привязкой, как в примере с counter, JavaScript молча создаёт глобальную привязку и использует её. В строгом режиме он сообщит об ошибке. Это очень полезно. Однако следует отметить, что это не сработает, если привязка уже существует как глобальная привязка. В этом случае цикл всё равно будет тихо перезаписывать значение привязки.

Ещё одно изменение в строгом режиме заключается в том, что внутри функций, вызываемых как методы, привязка this имеет значение undefined. При таком вызове вне строгого режима this ссылается на объект глобальной области видимости, свойства которого являются глобальными привязками. Поэтому, если вы по ошибке вызываете метод или конструктор в строгом режиме, JavaScript выдаст ошибку при попытке чтения содержимого из this, вместо того чтобы радостно записать его в глобальную область видимости.

Например, рассмотрим следующий код, который вызывает конструктор Person без ключевого слова new, чтобы this не ссылался на новый объект:

function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand"); // oops
console.log(name);
// → Ferdinand

Хотя мы неправильно вызвали Person, код всё ещё может выполняться успешно, но вернёт неопределённое значение и создаст глобальную привязку с именем name. В строгом режиме результат будет другим.

"use strict";
function Person(name) { this.name = name; }
let ferdinand = Person("Ferdinand");
// → TypeError: Cannot set property 'name' of undefined

Здесь JavaScript сразу сообщает нам о наличии ошибки в коде. Эта функция очень полезна.

К счастью, использование конструкторов, созданных с помощью символа class, всегда будет выдавать ошибку, даже если не используется new, и не вызовет проблем вне строгого режима.

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

Короче говоря, включение "use strict" вверху программы редко вызывает проблемы и может помочь вам обнаружить проблемы.

Типы

Некоторые языки хотят знать тип всех привязок и выражений ещё до запуска программы. Когда они используются несовместимым образом, они сразу же сообщают вам об этом. JavaScript рассматривает типы только во время фактического выполнения программы, хотя он часто пытается неявно преобразовать значения в ожидаемые типы, так что это мало помогает.

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

Вы можете добавить комментарий, подобный этому, над функцией goalOrientedRobot в предыдущей главе, чтобы описать её тип.

// (WorldState, Array) → {direction: string, memory: Array}
function goalOrientedRobot(state, memory) {
  // ...
}

Существует множество различных соглашений для аннотирования типов в JavaScript-программах.

Что касается типов, они требуют введения собственной сложности, чтобы быть достаточно полезными для описания достаточно полезного кода. Как вы думаете, какой тип у функции randomPick, которая возвращает случайный элемент из массива? Вам нужно ввести привязку типа T, которая может представлять любой тип, чтобы вы могли дать randomPick тип вроде ([T])->T (от T до T массива функций).

Когда типы программы известны, компьютер может проверить их за вас, указывая на ошибки до запуска программы. Существует несколько вариантов добавления и проверки типов в JavaScript. Самый популярный называется TypeScript. Если вам интересно добавить больше строгости в вашу программу, я рекомендую попробовать его.

В этой книге мы продолжим использовать оригинальный, опасный, нетипизированный JavaScript-код.

Тестирование

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

Делать это вручную снова и снова — плохая идея. Это не только утомительно, но и часто неэффективно, поскольку каждый раз, когда вносятся изменения, требуется слишком много времени, чтобы тщательно протестировать все аспекты.

Компьютеры хороши в повторяющихся задачах, и тестирование — идеальная повторяющаяся задача. Автоматизация тестирования — это процесс написания программы для тестирования другой программы. Написание тестов требует больше работы, чем ручное тестирование, но как только вы закончите, вы получите сверхспособность: всего за несколько секунд вы сможете убедиться, что ваша программа работает нормально во всех ситуациях, для которых вы написали тесты. Когда вы что-то сломаете, вы сразу заметите это, а не столкнётесь с этим случайно позже.

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

function test(label, body) {
  if (!body()) console.log(`Failed: ${label}`);
}

test("convert Latin text to uppercase", () => {
  return "hello".toUpperCase() == "HELLO";
});
test("convert Greek text to uppercase", () => {
  return "Χαίρετε".toUpperCase() == "ΧΑΊΡΕΤΕ";
});
test("don't convert case-less characters", () => {
  return "مرحبا".toUpperCase() == "مرحبا";
});

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

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

Отладка

Отладка — это процесс поиска и исправления ошибок в компьютерной программе. Перевод текста на русский язык:

Функция transfer будет переводить деньги с одного указанного счёта на другой. При этом она запрашивает название другого счёта. Если указать некорректное название счёта, функция getAccount вызовет исключение.

Однако функция transfer сначала удаляет средства со счёта, затем вызывает функцию getAccount и после этого добавляет их на другой счёт. Если в этот момент произойдёт исключение, то деньги пропадут.

Эту часть кода можно было бы сделать более умной, например, вызывать функцию getAccount перед тем, как переводить деньги. Но такие проблемы часто возникают более тонким образом. Даже функции, которые не похожи на те, что могут вызвать исключение, могут это сделать при определённых условиях или если они содержат ошибки программиста.

Один из способов решить эту проблему — использовать меньше побочных эффектов. Также может помочь программирование в стиле, который создаёт новые значения вместо изменения существующих данных. Если код остановится при создании нового значения, никто не увидит это незаконченное значение, и проблем не будет.

Но это не всегда так. Поэтому у оператора try есть ещё одна особенность. За ним может следовать блок finally, а не блок catch, и не после него. Блок finally говорит: «Что бы ни произошло после попытки запустить код в блоке try, этот код обязательно запустится».

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

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

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

Выборочный перехват

Когда программа выдаёт исключение и оно не перехвачено, исключение сразу возвращается к вершине стека и обрабатывается средой JavaScript. Обработка зависит от конкретной среды. В браузере описание ошибки обычно записывается в консоль JavaScript (доступ к консоли можно получить с помощью инструментов браузера или меню разработчика). Мы обсудим это в главе 20, но среда JavaScript без браузера Node.js относится к повреждению данных более осторожно. Когда происходит необработанное исключение, процесс останавливается.

Для ошибок программиста обычно лучше всего позволить ошибкам проходить. Необработанные исключения — это разумный способ обозначить плохую программу, а в современных браузерах консоль JavaScript предоставляет некоторую информацию о том, какие функции были вызваны в стеке во время возникновения проблемы.

Сбой из-за непредвиденных проблем в повседневном использовании — плохая стратегия.

Явные злоупотребления языком, такие как обращение к несуществующей привязке, запрос атрибута в null или вызов объекта, который не является функцией, в конечном итоге вызовут исключение. Вы можете поймать эти исключения так же, как свои собственные.

При входе в блок catch мы знаем только то, что в теле try возникло исключение, но не знаем, какого рода или какое именно исключение.

JavaScript (очевидный недостаток) не обеспечивает хорошей поддержки для выборочного перехвата исключений, нужно либо перехватывать все исключения, либо ничего не перехватывать. Это позволяет легко предположить, что полученное вами исключение — это то, о чём вы думали, когда писали catch.

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

Мы можем использовать цикл for (;;), чтобы создать бесконечный цикл, который никогда не остановится. Мы выйдем из цикла, когда пользователь даст действительное направление. Но мы неправильно написали promptDirection, поэтому возникнет ошибка «неопределённое значение». Поскольку блок catch полностью игнорирует значение исключения, предполагается, что он знает проблему и связывает ошибочную информацию как ошибку ввода. Это не только вызовет бесконечный цикл, но и скроет фактическое сообщение об ошибке — неправильное написание привязки.

Как правило, мы перехватываем все исключения только тогда, когда перемещаем выброшенное исключение в другое место для обработки. Например, через сетевое уведомление другие системы о сбое текущего приложения. Однако мы должны следить за тем, чтобы наш код не скрывал реальную информацию об ошибках.

Поэтому мы переходим к перехвату определённых типов исключений. Мы можем проверить в блоке catch, является ли перехваченное исключение тем, которое мы ожидаем обработать, и если нет, повторно выдать его. Как нам определить тип выброшенного исключения?

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

Лучше определить новый тип ошибки и использовать instanceof для идентификации исключения.

Новый класс ошибок расширяет Error. Он не определяет свой собственный конструктор, что означает, что он наследует конструктор Error, которому требуется строковый аргумент сообщения. Фактически, он вообще ничего не определяет — класс пуст. Поведение объектов InputError похоже на поведение объектов Error, разница только в их классе, и мы можем идентифицировать их по классу.

Теперь цикл может более тщательно их перехватить.

Здесь блок catch будет перехватывать только исключения типа InputError, а другие типы исключений обрабатываться здесь не будут. Если снова ввести неправильное значение, система точно сообщит пользователю об ошибке — «привязка не определена».

Утверждения

Утверждения (assertions) — это проверки внутри программы, предназначенные для подтверждения того, что что-то должно быть таким, каким оно должно быть. Они предназначены не для обработки ситуаций, которые могут возникнуть при нормальном функционировании, а для выявления ошибок программирования.

Например, если firstElement описывается как функция, которая никогда не вызывается для пустого массива, мы можем написать:

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

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

Заключение главы

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

Нам часто приходится изящно обрабатывать проблемы, выходящие за рамки контролируемого программой диапазона. Если проблему можно решить на месте, то возвращение специального значения для отслеживания ошибки — хорошее решение. Или исключение также может быть приемлемым. Выброс исключения вызовет разворачивание стека до тех пор, пока не встретится следующий закрытый блок try/catch или дно стека.

После того как блок catch перехватит исключение, он присвоит значение исключения блоку catch. Блок catch должен проверить, является ли исключение тем, которое действительно нужно обработать, и затем выполнить обработку.

Чтобы помочь разрешить непредсказуемый поток выполнения из-за исключения, можно использовать блок finally для обеспечения выполнения кода после блока try.

Задача: повторная попытка

Предположим, что есть функция primitiveMultiply, которая умножает два числа в 20% случаев, а в остальных 80% случаях вызывает исключение типа MultiplicatorUnitFailure. Напишите функцию, которая вызывает эту функцию с ошибками, пытается снова и снова, пока вызов не будет успешным и не вернёт результат.

Убедитесь, что вы обрабатываете только ожидаемые исключения.

class MultiplicatorUnitFailure extends Error {}

function primitiveMultiply(a, b) {
  if (Math.random() < 0.2) {
    return a * b;
  } else {
    throw new MultiplicatorUnitFailure();
  }
}

function reliableMultiply(a, b) {
  // Ваш код здесь.
}

console.log(reliableMultiply(8, 8));
// → 64

Задача: запертый ящик

Рассмотрим следующий готовый объект:

const box = {
  locked: true,
  unlock() { this.locked = false; },
  lock() { this.locked = true;  },
  _content: [],
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this._content;
  }
};

Это запертый ящик. Внутри него находится массив, но доступ к массиву возможен только тогда, когда ящик открыт. Непосредственное обращение к свойству _content запрещено.

Напишите функцию withBoxUnlocked, которая принимает параметр функции. Функция должна разблокировать ящик, выполнить переданную функцию и перед возвратом обязательно запереть ящик. Независимо от того, вернётся ли функция нормально или выбросит исключение, ящик должен быть заперт перед возвращением функции withBoxUnlocked.

const box = {
  locked: true,
  unlock() { this.locked = false; },
  lock() { this.locked = true;  },
  _content: [],
  get content() {
    if (this.locked) throw new Error("Locked!");
    return this._code;
  }
};

function withBoxUnlocked(body) {
  // Ваш код здесь.
}

withBoxUnlocked(function() {
  box.content.push("gold piece");
});

try {
  withBoxUnlocked(function() {
    throw new Error("Pirates on the horizon! Abort!");
  });
} catch (e) {
  console.log("Error raised:", e);
}
console.log(box.locked);
// → true

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-eloquent-js-3e-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-eloquent-js-3e-zh.git
oschina-mirror
wizardforcel-eloquent-js-3e-zh
wizardforcel-eloquent-js-3e-zh
master