Глава 8Обработка на Bugs и Error
Debugging е два пъти по-силно, отколкото самото писане на код. Ето защо ако напишете код, колкото и хитър да е по дефиниция, той не е достатъчно умен за да може сам да си отстранява грешките.”
Ян-ма написал малка програма, в която използвал много глобални променливи и калпави комбинации. Четейки я, един студент попитал: „Ти ни предупреди за тези техники, но аз ги намирам във вашата програма. Как е възможно това?” А майсторът казал:”Не е нужно да се носи вода, когато къщата не гори.””
Програмата кристализира мисълта. Понякога тези мисли са объркани. Друг път се правят грешки при превръщането на мисълта в код. Така или иначе, резултата е грешна програма.
Пропуски в програмата обикновено се наричат грешки ("bugs"). Bugs може да бъде грешка на програмиста или проблеми в други системи, с които програмата си взаимодейства. Някои грешки са очевидни, а други едва доловими и може да останат скрити в една система в продължение на години.
Често проблемите на повърхността са само, когато една програма се сблъска със ситуация, за която програмиста първоначално не е помислил. Понякога такива ситуации са неизбежни. Например, когато потребителя иска да въведе възраст и пол - портокал, това поставя програмата в трудна позиция. Ситуацията трябва да се очаква и обработва по някакъв начин.
Грешки на програмиста
Когато става дума за грешки на програмиста, нашата цел е ясна. Ние искаме да ги намерим и поправим. Такива грешки могат да варират от прости правописни грешки, които са причина компютъра да се оплаче веднага, до невидими за очите фини грешки, при които програмата работи, създавайки неправилни резултати само в определени случаи. При грешки от втория вид може да отнеме седмици, за да се диагностицират.
Степента, с която езиците ви помагат да намерите подобни грешки варира. Не е изненадващо, че JavaScript - “помага за всичко” е в края на тази скала. Някои езици искат да знаят типа на всички променливи и изрази преди изпълнение на програмата и ще ви кажат веднага, ако даден тип се използва по грешен начин. JavaScript отчита типовете при действителното изпълнение на програмата, а дори и тогава позволява да се правят някои очевидно безсмислени неща без да се оплаква, като x = true * "monkey"
.
Има някои неща, от които JavaScript се оплаква, все пак. Писане на програма, която не е синтактично валидна, веднага ще предизвика грешка. Друго нещо е, като извикате нещо, което не е функция или разглежда свойство на неопределена стойност, ще предизвика грешка без да се докладва, но когато се изпълнява програмата се натъкваме на безсмислени действия.
Често, безсмислените изчисления, ще произведат просто NaN
(не число) или неопределена стойност. И програмата щастливо продължава, убедена че прави нещо смислено. Грешката се появява едва по-късно, след като фалшивата стойност е минала през няколко функции. Тя не може да предизвика грешка във всички, но безшумно определя изхода на програмата да бъде грешен. Намирането на източника на тези проблеми може да бъде много трудно.
Процесът за намиране на грешки - бъгове в програмите се нарича debugging.
Strict mode
JavaScript може да се направи малко по-стриктен, като се използва strict mode. Това се прави чрез поставяне на string "use strict"
в горната част на файла или тялото на функцията. Ето един пример:
edit & run code by clicking itfunction canYouSpotTheProblem() { "use strict"; for (counter = 0; counter < 10; counter++) console.log("Happy happy"); } canYouSpotTheProblem(); // → ReferenceError: counter is not defined
Обикновено, когато сте пропуснали да поставите var
пред вашата променлива, както примера с counter
, JavaScirpt тихо създава глобална променлива и я използва. В строг режим обаче, грешката се съобщава. Това е много полезно. Трябва да се отбележи обаче, че това не работи, когато въпросната променлива вече съществува, като глобална променлива, а само при определянето и.
Друга промяна в строг режим е, че this
обвързването съдържа стойност undefined
във функции, които не са извикани, като методи. При извършване на такова извикване извън строг режим, this
се отнася до глобалния обхват на обекта. Така че, ако случайно се извика метод или конструктор неправилно в строг режим, JavaScript ще съобщи за грешка, веднага след като се опита да прочете нещо от this
, а не радостно да работи с глобалния обект, да създава и чете глобални променливи.
Например, нека разгледаме следния код, който призовава конструктор без ключовата дума new
, така че this
няма да се отнася до ново-изградения обект.
function Person(name) { this.name = name; } var ferdinand = Person("Ferdinand"); // oops console.log(name); // → Ferdinand
Така фалшивото извикване към Person
успя, но се връща недефинирана стойност и се създава глобалната променлива name
. В строг режим, резултата ще е по-различен
"use strict"; function Person(name) { this.name = name; } // Oops, forgot 'new' var ferdinand = Person("Ferdinand"); // → TypeError: Cannot set property 'name' of undefined
Тука веднага каза, че нещо не е наред. Това е от полза.
Строг режим прави още няколко неща. Той забранява подаването на множество параметри със същото име към функция и премахва някои проблемни изикови функции изцяло (подобно на with
което толкова е заблудено, че не се разглежда в тази книга).
С една дума, поставянето на "use strict"
в горната част на вашата програма рядко боли и може да ви помогне да забележите някакъв проблем.
Тестване
Ако езикът не може да направи много, за да ви помогне в откриването на грешки ще трябва да ги търсите по трудния начин: чрез стартиране на програмата и гледане дали тя прави правилното нещо.
Правейки това на ръка, отново и отново, е сигурен начин да ви докара до лудост. За щастие, често е възможно да се напише програма, която автоматизира повторното тестване на програмата.
Като пример, отново ще използваме типа Vector
.
function Vector(x, y) { this.x = x; this.y = y; } Vector.prototype.plus = function(other) { return new Vector(this.x + other.x, this.y + other.y); };
Ще напишем програма, която да провери нашето изпълнение на Vector
, дали работи по предназначение. След това, всеки път, когато се промени изпълнението, ще използваме програмата за тестване, така че да бъдем сигурни, че не сме счупили нещо. Когато добавяме допълнителна функционалност (например нов метод) към типа Vector
, ние също така ще добавим и тестове за новата функция.
function testVector() { var p1 = new Vector(10, 20); var p2 = new Vector(-10, 5); var p3 = p1.plus(p2); if (p1.x !== 10) return "fail: x property"; if (p1.y !== 20) return "fail: y property"; if (p2.x !== -10) return "fail: negative x property"; if (p3.x !== 0) return "fail: x from plus"; if (p3.y !== 25) return "fail: y from plus"; return "everything ok"; } console.log(testVector()); // → everything ok
Писането на тестове, като този, имат тенденцията да правят по-скоро неудобен повтарящ се код. За щастие съществува софтуер, който да ни помогне да изградим и пуснем колекции от тестове (тестов комплект), чрез предоставяне на език (под формата на функции и методи) подходящ за изразяване на тестове и чрез извеждане на информация, когато теста се провали. Те се наричат testing frameworks (тестови рамки).
Debugging
След като забележите, че нещо не е наред с вашата програма, защото тя не е с нормално поведение или произвежда грешки, следващата стъпка е да разберете какъв е проблемът.
Понякога е очевидно. Съобщаването на грешка посочва конкретен ред от вашата програма и ако се вгледате в описанието на този ред от кода, често може да видите проблема.
Но не винаги. Понякога реда, който е задействал проблема е просто първото място, където фалшивата стойност произведена другаде ще проработи по неправилен начин. Понякога не е съобщение за грешка, а просто невалиден резултат. Ако сте решавали упражненията в предните глави, вероятно вече сте имали такива ситуации.
В следният пример, програмата се опитва да превърне цяло число в string на някаква база (десетична, двоична и т.н.), като на няколко пъти взема последната цифра и след това я разделя на броя за да се отърве от тази цифра. Но на изхода произвежда някакъв ненормален резултат и се предполага, че това е грешка.
function numberToString(n, base) { var result = "", sign = ""; if (n < 0) { sign = "-"; n = -n; } do { result = String(n % base) + result; n /= base; } while (n > 0); return sign + result; } console.log(numberToString(13, 10)); // → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e-3181.3…
Дори и да виждате проблема, се престорете за момент, че не го виждате. Ние знаем, че нашата програма е неизправна и искаме да разберем защо.
Това е мястото, където трябва да устоим на изкушението да започнем да правим произволни промени в кода. Вместо това, мислете. Анализирайте случващото се и излезте с една теория, защо това се случва. След това направете допълнителни наблюдения, за да тествате тази теория или ако още не разполагате с теория, направете допълнителни наблюдения, които да ви помогнат да излезете с една.
Поставянето на няколко стратегически console.log
извиквания в програмата е добър начин да получите допълнителна информация относно това, което програмата прави. В този случай ние искаме n
да вземе стойностите 13
, 1
, а след това 0
. Нека да напишем стойността в началото на цикъла.
13 1.3 0.13 0.013 … 1.5e-323
Правилно. Разделянето на 13 от 10 не произвежда цяло число. Вместо n /= base
, всъщност искаме n = Math.floor(n / base)
, така че числото да бъде правилно променено от дясно.
Една алтернатива на използването на console.log
е да се използва debugger на вашия браузър. Съвременните браузъри разполагат с възможност за определяне на точка на прекъсване на определен ред от вашия код. Това ще доведе до пауза при изпълнението на вашата програма, всеки път, когато се достигне реда с точката за прекъсване "breakpoint" и ще ви позволи да инспектирате стойностите на променливите до този момент. Аз няма да влизам в подробности тука, тъй като дебъгерите се различават в различните браузъри, но ги разгледайте в инструментите за разработчици на браузъра си и потърсете в Интернет повече информация. Друг начин да се определи точка на прекъсване е да включите debugger
изявление във вашата програма (състоящо се просто, от ключовата дума). Ако инструментите за разработчици в браузъра са активни програмата ще направи пауза, когато стигне това изявление и ще бъдете в състояние да инспектирате съдържанието.
Разпространяване на Error
Не всички проблеми могат да бъдат предотвратени от програмиста, за съжаление. Ако вашата програма комуникира с външния свят по някакъв начин, има шанс данните на входа, които получава да са невалидни или други системи с които се опитва да се свърже са счупени или недостъпни.
Прости програми или програми, които работят само под вашето ръководство, могат да си позволят просто да се откажат, когато възникне такъв проблем. Ще разгледате проблема и ще опитате отново. Реалните приложения, от друга страна, не се очаква просто да се сринат. Понякога правилното нещо, което може да направите е да вземете грешното въвеждане и да продължите работа. В други случаи е по-добре да се докладва на потребителя, какво се е объркало и след това да се откажете. Но и в двете ситуации, програмата може да направи нещо активно в отговор на проблема.
Да речем, че функцията promptInteger
пита потребителя за цяло число и го връща. Какво трябва да върне, ако потребителя въведе на входа “портокал”?
Единият вариант е да върне специална стойност. Избора за тази стойност е null
или undefined
.
function promptNumber(question) { var result = Number(prompt(question, "")); if (isNaN(result)) return null; else return result; } console.log(promptNumber("How many trees do you see?"));
Това е ефективна стратегия. Сега всеки код, който извиква promptNumber
трябва да провери дали действително чете цифра и ако не, трябва по някакъв начин да се възстанови, може би като попита отново или чрез попълване на стойност по подразбиране. Или може да върне специална стойност към повикващия за да покаже, че той не е направил това, което е бил помолен.
В много ситуации, най-вече, когато грешките са често срещани и на повикващия трябва изрично да се каже да ги вземе под внимание, връщайки специална стойност е перфектен начин да се покаже грешката. Той обаче има своите недостатъци. Първо, какво ще стане ако функцията е готова да върне всяка възможна стойност? За такава функция е трудно да се намери специална стойност, която може да бъде разграничена от валиден резултат.
Вторият проблем с връщането на специални стойности е, че те могат да доведат до някой много затрупан код. Ако част от кода извиква promptNumber
10 пъти, трябва да се провери 10 пъти дали null
e върнатата стойност. И ако отговора за намирането на null
е просто да върне себе си на повикващия, той от своя страна трябва да провери за него и т.н.
Изключения (Exceptions)
Когато дадена функция не може да продължи нормално, това което бихме искали да направи е просто да спре и веднага да скочи обратно към място, за което знае как да се справи с проблема. Това се нарича обработка на изключения.
Изключенията са механизъм, който позволява на кода при проблем да хвърли грешка, което е просто една стойност. Хвърлянето на грешка донякъде прилича на супер - връщане от функция: то изкача не само от текущата функция, но и от нейните извикващи, чак до първото извикване, с което започва текущото изпълнение. Това се нарича развиване на стека. Може би си спомняте за стека за извикване на функции в Глава 3. Изключенията влизат надолу в този стек, изхвърляйки всички контексти на операциите, с които се сблъсква.
Ако изключенията винаги стигат чак до дъното на стека, те не биха били от голяма полза. Те могат да предоставят нов начин да взривите вашата програма. Тяхната сила се крие във факта, че можете да зададете “препятствия” по протежение на стека, за да направят изключение, ако влезе по на долу. След това може да направите нещо с него, след което програмата продължава да работи до мястото, където е уловила това изключение.
function promptDirection(question) { var result = prompt(question, ""); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new Error("Invalid direction: " + result); } function look() { if (promptDirection("Which way?") == "L") return "a house"; else return "two angry bears"; } try { console.log("You see", look()); } catch (error) { console.log("Something went wrong: " + error); }
Ключовата дума throw
се използва за предизвикване на изключение. Улавянето се извършва чрез обвиване на част от кода в try
блок, следвано от ключовата дума catch
. Когато кодът try
предизвика изключение catch
блока го прихваща и оценява. Името на променливата (в скобите) след catch
ще бъде обвързана със стойността на изключението. След като catch
блока приключи или ако try
блока премине без проблем, контрола продължава след try/catch
изявлението.
В такъв случай ние използваме Error
конструктора за създаване на нашата стойност за изключение. Той е стандартен JavaScript конструктор, който създава обект със свойство message
. В съвременните условия на JavaScript, инстанции на този конструктор също събират информация за стека на извикване, когато изключението се е създало, така наречената стек следа. Тази информация се съхранява в stack
свойство и може да бъде полезна, когато се опитваме да определим един проблем: тя ни казва точната функция, където е възникнал проблема и какави други функции са довели до създаването на провала.
Имайте в предвид, че функцията look
напълно пренебрегва възможността, че promptDirection
може да се обърка. Това е най-голямото предимство на кода за обработка на изключения, за който е необходима само точката, където се появява грешката и точката, където се обработва. Функциите между тях могат да забравят за него.
Почистване след изключения
Нека разгледаме следната ситуация: функцията withContext
иска да се увери, че по време на нейното изпълнение, променливата на най-високо ниво context
притежава специфична context
стойност. След това завършва и възстановява на тази променлива старата и стойност.
var context = null; function withContext(newContext, body) { var oldContext = context; context = newContext; var result = body(); context = oldContext; return result; }
Какво става ако body
предизвика изключение? В този случай извикването на withContext
ще бъде изхвърлено от стека заедно с изключението и context
никога няма да се върне към старата си стойност.
Има още една особеност, която try
изявленията имат. Те могат да бъдат последвани от finally
блок, вместо или в допълнение към catch
блока. Finally
блока казва ”Няма значение какво се случва, изпълни този код, след като мине през try
блока”. Ако една функция трябва да почисти нещо, кода за почистване обикновено се поставя в finally
блока.
function withContext(newContext, body) { var oldContext = context; context = newContext; try { return body(); } finally { context = oldContext; } }
Имайте в предвид, че вече не се налага да съхраняваме резултата на body
(който искаме да се върне) в променлива. Дори да се върне директно от try
блока, finally
блока пак ще се изпълни. Сега можем да направим това и да бъде безопасно.
try { withContext(5, function() { if (context < 10) throw new Error("Not enough context!"); }); } catch (e) { console.log("Ignoring: " + e); } // → Ignoring: Error: Not enough context! console.log(context); // → null
Въпреки, че функцията извикана от withContext
гръмна, самата withContext
правилно ще почисти context
променливата.
Подбиращо прихващане
Когато едно изключение премине през целия път до дъното на стека, без да е хванато, то се обработва от заобикалящата среда. Какво означава това, се различава между средите. В браузъри описанието за грешка обикновено се получава написано в конзолата на JavaScript (достъпна чрез Tools на браузъра с F12 или меню Developer).
За програмист грешки или проблеми, с които програмата не може да се справи, просто се разрешават да преминат и често е наред. Едно необработено изключение е един разумен начин да се сигнализира за счупена програма и конзолата на JavaScript в съвременните браузъри ви предоставя някаква информация за извикванията на функцията в стека, когато е възникнал проблема.
За проблеми, които се очаква да се случат по време на рутинна употреба, сблъскване с необработено изключение не е много приятелски отговор.
Невалидна употреба на езика, като се позовава на несъществуваща променлива, разглеждане на свойство null
или се извика нещо, което не е функция, също ще доведе до изключение. Такива изключения могат да бъдат прихванати, точно като вашите собствени изключения.
Когато catch
тялото е въведено, всичко което знаем е, че нещо в try
тялото ще причини изключение. Но ние незнаем, какво или кое изключение го е причинило.
JavaScript (има един доста фрапантен пропуск) не предоставя пряка подкрепа за избирателно прихващане на изключения: да го хване или да не го хване. Това прави много лесно предположението, че изключението, което получавате е това, за което се мисли, когато е написано в catch
блока.
Но може и да не бъде. Някое друго предположение може да бъде нарушено или може да сте въвели грешка някъде и това да е причина за изключение. Ето един пример, който се опитва да запази извикването към promptDirection
, докато стане валиден отговор:
for (;;) { try { var dir = promtDirection("Where?"); // ← typo! console.log("You chose ", dir); break; } catch (e) { console.log("Not a valid direction. Try again."); } }
Конструкцията for (;;)
е начин за умишлено създаване на цикъл, който не спира да работи. Можем да се измъкнем от цикъла, когато дадем валидна посока. Но и името на променливата написано, като promptDirection
е грешно, което ще доведе до “undefined variable” (неидентифицирана променлива). Тъй като
catch
блока напълно игнорира стойноста на своето изключение ((e)
) и ако приемем, че знае какъв е проблема, той неправилно третира грешката, като посочва грешно въвеждане. Не само, че причинява един безкраен цикъл, но и също така “затваря” полезното съобщение за грешка за неправилно изписана променлива.
Общо правило е, да не правим blanket-catch
за изключения, освен ако нямаме намерение да ги пращаме някъде, например, по мрежата за да кажем на друга система, че програмата ни е счупена. Дори и тогава помислете внимателно, как може да скриете информация.
Така че, ние искаме да прихванем специфичен вид изключение. Можем да направим това чрез проверка в catch
блока дали изключението, което получаваме е това, което ни интересува и го прехвърляме в друго. Но как да разпознаем това изключение?
Разбира се, бихме могли да съчетаем неговото свойство message
със съобщението за грешката, което очакваме. Но това е по-нестабилен начин за писане на код - така се лишаваме от използването на информацията предназначена за усвояване от човека (съобщението), за да направим програмно решение. Веднага, след като се промени (или се преведе) съобщението, кодът ще спре да работи.
По- скоро, нека да дефинираме нов тип грешка и да използваме instanceof
, за да го идентифицира.
function InputError(message) { this.message = message; this.stack = (new Error()).stack; } InputError.prototype = Object.create(Error.prototype); InputError.prototype.name = "InputError";
Прототипът е направен да извлича от Error.prototype
, така че instanceof Error
също ще върне true за InputError
обекти. Той също дава свойството name
, тъй като стандартните типове грешки, като Error
, SyntaxError
, ReferenceError
и т.н.) също имат това свойство.
Присвояването към stack
свойството се опитва да даде на този обект малко по-полезна следа в стека, за платформи, които го поддържат, чрез създаване на обект за нормална грешка и след това използвате този обект на stack
, като свой собствен.
Сега promptDirection
може да хвърли такава грешка.
function promptDirection(question) { var result = prompt(question, ""); if (result.toLowerCase() == "left") return "L"; if (result.toLowerCase() == "right") return "R"; throw new InputError("Invalid direction: " + result); }
И цикълът може да я прихване по-прецизно:
for (;;) { try { var dir = promptDirection("Where?"); console.log("You chose ", dir); break; } catch (e) { if (e instanceof InputError) console.log("Not a valid direction. Try again."); else throw e; } }
Това ще хване само случаи на InputError
и несвързани изключения. Ако се въведе отново печатна грешка или неопределена променлива, грешката ще бъде надлежно съобщена.
Твърдения
Твърденията са инструмент за основни разсъждения за проверка за грешки на програмиста. Помислете за помощната функция assert
:
function AssertionFailed(message) { this.message = message; } AssertionFailed.prototype = Object.create(Error.prototype); function assert(test, message) { if (!test) throw new AssertionFailed(message); } function lastElement(array) { assert(array.length > 0, "empty array in lastElement"); return array[array.length - 1]; }
Това осигурява компактен начин да се наложат очаквания - услужливо гръмване на програмата, ако посоченото условие не изпълнено. Например, функцията lastElement
, която извлича последния елемент от масив, ще върне undefined
за празени масиви, ако твърдението е пропуснато. Извличането на последния елемент от празен масив няма много смисъл, така че почти е сигурно, че грешката е на програмиста.
Твърденията са начин да се уверите, че грешките причиняват повреди на мястото на грешката, а не мълчаливо произвеждащи глупости, които могат да продължат да предизвикват проблеми в нямаща нищо общо част на системата.
Резюме
Грешки и лош вход са факти от живота. Грешките в програмите трябва да бъдат намерени и фиксирани. Те могат станат по-лесно забележими с наличието на автоматизирани тестове и добавяне на твърдения във вашите програми.
Проблеми причинени от фактори, които са извън контрола на програмата обикновено трябва да се обработват грациозно. По някога, когато проблемът е на местно ниво, специалните стойности за връщане са един нормален начин да ги проследят. В противен случай, изключенията са за предпочитане.
Хвърляне на изключение създава призив към стека да се развие до следващия обхващащ try/catch
блок или до дъното на стека. Стойността на изключението, ще бъде дадено на catch
блока, който го прихваща и се уверява, че това всъщност е очаквания вид изключение, а след това прави нещо с него. За да се справят с непредвидим контрол причинен от изключения, блоковете на finally
могат да бъдат използвани за да се гарантира, че част от кода винаги ще се стартира.
Упражнения
Опитайте отново
Да речем, че функцията primitiveMultiply
в 50% от случаите, умножава две числа, а в останалите 50%, извиква изключение от типа MultiplicatorUnitFailure
. Напишете функция, която завършва тази неудобна функция и продължава да опитва докато операцията стане успешна, след което връща резултата.
Уверете се, че сте разгледали само изключенията, които се опитват да се справят.
function MultiplicatorUnitFailure() {} function primitiveMultiply(a, b) { if (Math.random() < 0.5) return a * b; else throw new MultiplicatorUnitFailure(); } function reliableMultiply(a, b) { // Your code here. } console.log(reliableMultiply(8, 8)); // → 64
Извикването на primitiveMultiply
очевидно трябва да се случи в try
блок. Съответният catch
блок трябва да rethrow изключение, когато не е инстанция на MultiplicatorUnitFailure
и повикването да се повтори, когато това е гарантирано.
За да направите повторен опит, може да използвате един цикъл, който breaks само, когато повикването е успяло - както в примера по-рано в тази глава - или използвате рекурсия и надявам се да не получите поредица от неуспехи, толкова дълго, че да препълните стека (което е доста безопасен залог).
Заключената кутия
Да разгледаме следния (по скоро измислен) обект:
var box = { locked: true, unlock: function() { this.locked = false; }, lock: function() { this.locked = true; }, _content: [], get content() { if (this.locked) throw new Error("Locked!"); return this._content; } };
Това е кутия с ключалка. Вътре има масив, който може да получите само, когато кутията е отключена. Директен достъп до свойството _content
не е позволен.
Напишете функция, наречена withBoxUnlocked
, която взема стойността на функцията, като аргумент, отключва кутията, стартира функцията и след това гарантира, че кутията отново е заключена преди да се върне, независимо от това дали функцията аргумент се е върнала нормално или хвърля изключение.
function withBoxUnlocked(body) { // Your code here. } 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
За допълнителни точки, се уверете, че ако извикате withBoxUnlocked
, когато кутията е вече отключена тя остава отключена.
Това упражнение изисква finally
блок, както може би сте предположили. Вашата функция трябва първо да отключи кутията и след това да извика функцията с аргумент за вътрешноста на try
тялото. Накрая finally
блока трябва да заключи кутията отново.
За да сме сигурни, че не заключваме кутията, когато тя вече е била заключена, проверете заключването в началото на функцията и отключете и заключете, само когато тя е започнала заключена.