Глава двадцать третья. Как заставить неправильный код выглядеть неправильно
11 мая 2005 года, среда
Давным-давно, в сентябре 1983-го я поступил на свою первую настоящую работу в Oranim, крупный израильский хлебозавод, каждую ночь выпекающий около 100 000 буханок хлеба в шести гигантских печах размером с авианосец.
Прежде всего в пекарне меня поразила грязь. Пожелтевшие печи, ржавые машины, всюду смазка.
Я спросил: «Здесь всегда так грязно?»
«О чем это ты? — сказал менеджер. — Уборка только что закончилась. Здесь уже много недель не было так чисто».
Боже милостивый.
Два месяца убирая пекарню по утрам, я понял, что они имели в виду. Чистота в пекарне означала, что на машинах нет теста. И что нет гниющего теста в мусорных бачках. И что тесто не валяется на полу.
Чистота не означала, что печи выкрашены свежей белой краской. Их красили, но раз в десять лет, а не каждый день. Чистота не означала, что нигде не видно смазки. Там было много машин, которые нужно было регулярно смазывать, и тонкий слой чистого масла обычно служил признаком того, что машину только что почистили.
Что такое чистота в пекарне, нужно было понять. Зайдя со стороны, невозможно решить, чисто в этом месте или нет. Постороннему в голову не придет осмотреть внутренние поверхности формовщика (машина, которая скатывает квадратные куски теста в круглые) и определить, хорошо ли
они отскоблены. Посторонний обратил бы внимание на выцветшие панели старой печи, потому что они огромны. Но пекаря меньше всего волнует, что краска на внешних панелях печи начала немного желтеть. На вкусе хлеба это не отражается.
После двух месяцев в пекарне вы бы узнали, что такое «чисто».
То же самое в программировании.
Если вы начинающий программист или пробуете читать код на новом языке, все выглядит одинаково непостижимо. Пока вы не освоите язык, вы не заметите даже очевидные синтаксические ошибки.
На первом этапе обучения вы уже начинаете обращать внимание на то, что принято называть «стилем кодирования». И вы замечаете, что в коде не соблюдаются стандарты отступов или применения заглавных букв в именах переменных.
В такой момент обычно говорят: «Лопни моя селезенка! Мы должны установить какие-то правила для единообразного кодирования!» — и тратят весь следующий день на составление правил написания кода для своей команды, остаток недели — на споры о Едином Истинном Стиле Расстановки Скобок, еще три недели — на переписывание старого кода в соответствии с Единым Истинным Стилем Расстановки Скобок, пока менеджер не засечет вас, сделав выговор за напрасную трату времени, не приносящую никакой прибыли, и вы решите, что вполне можно переформатировать код позже, в результате только половина вашего кода соответствует Истинному Стилю Расстановки Скобок, но скоро вы об этом забудете и увлечетесь чем-то еще, столь же бесполезным в смысле прибыли, например станете заменять один строковый класс на другой.
По мере совершенствования в написании кода в определенной среде разработки вы учитесь видеть и нечто другое. То, что совершенно законно и правильно с точки зрения правил кодирования, но вызывает беспокойство.
Например, в С:
char* dest, src;
Это абсолютно верный код; он может соответствовать вашему соглашению о кодировании и даже правильно делать то, для чего предназначен, но при наличии достаточного опыта работы на С вы заметите, что здесь dest объявляется как указатель на char, в то время как src объявляется просто как char, и возможно, это не то, чего вы хотели. Этот код выглядит неопрятно.
Еще более тонкий случай:
if (i != 0)
foo(i);
В этом случае код правилен на 100%, соответствует большинству соглашений о кодировании и в нем нет ничего плохого, но тот факт, что тело оператора if, содержащего один оператор, не помещено в фигурные скобки, может вызвать беспокойство, потому что возникает мысль, не угораздит ли кого-нибудь вставить туда еще одну строку кода:
if (i != 0)
bar(i);
foo(i);
и забыть добавить фигурные скобки, что ненароком заставит foo(i) выполняться безусловно! То есть при виде блоков кода, не заключенных в фигурные скобки, вас начинает беспокоить крошечное, микроскопическое, мизерное ощущение неопрятности.
Итак, пока я упомянул три уровня квалификации программиста:
1. Вы не отличаете чистое от грязного.
2. У вас есть поверхностное представление о чистоте, главным образом на уровне соответствия соглашениям о кодировании.
3. Вы начинаете улавливать тонкие намеки на скрытую неопрятность, и они раздражают вас достаточно сильно для того, чтобы вы исправили код.
Однако есть еще один, более высокий уровень, о котором я и хочу поговорить:
4. Вы специально строите код так, чтобы ваше чутье на неопрятность эффективнее помогало избавить код от ошибок.
Сделать код надежным, изобретая правила написания кода, чтобы ошибки бросались в глаза, — настоящее искусство.
Сейчас мы рассмотрим небольшой пример, затем я покажу общее правило, которое поможет вам изобретать эти самые соглашения для написания надежного кода, что в конце концов оправдает один из видов «венгерской нотации», не столь уж тошнотворный, и послужит критикой применения обработки исключений в определенных обстоятельствах, хотя, скорее всего, вы не назовете эти обстоятельства типичными.
Но если вы твердо уверены, что венгерская нотация — это плохо, а обработка исключений — лучшее изобретение человечества после шоколадномолочного коктейля, и других мнений даже слышать не хотите, то вместо
этого, к примеру, посмотрите классные комиксы (www.neopoleon.com/home/blogs/neo/archive/2005/04/29/15699. aspx) — может, вы ничего и не потеряете. Немного ниже я начну разбирать реальные примеры кода, от чего вы, скорее всего, заснете раньше, чем успеете возмутиться. Да. Думаю, вас нужно убаюкать, и пока вы дремлете и не станете активно сопротивляться, незаметно протолкнуть идею, что венгерская нотация = хорошо, а исключения = плохо.
Пример
Правильно. Начнем с примера. Предположим, что вы разрабатываете некое веб-приложение (потому что на них, кажется, все сегодня помешались).
Значит так. Существует такой вид уязвимости компьютерных систем, как межсайтовый скриптинг, или XSS. Не буду вдаваться в детали: достаточно знать, что в целях безопасности ваше веб-приложение никогда не должно передавать обратно строки, введенные пользователем в формах.
Например, если у вас есть веб-страница с полем ввода для ответа на вопрос «Как вас зовут?», а затем эта страница пересылает вас на другую страницу, где сказано «Привет, Элмер!» (если пользователь ввел имя Элмер), ну, в общем, это опасная уязвимость, потому что пользователь мог ввести везде вместо «Элмер» любой код HTML или JavaScript, и этот вредный код JavaScript может сделать какую-нибудь гадость, и теперь эти гадости, похоже, будут исходить от вас, например они могут прочитать ваши cookie и послать информацию из них на злой сайт доктора Зло.
Давайте запишем это в виде псевдокода. Представьте, что
s = Request("name")
читает введенные данные (аргумент POST) из HTML-формы. Если вы где-то поместите код:
Write "Hello, " & Request("name")
то ваш сайт становится уязвимым для XSS-атак. Этого достаточно. Вместо этого вы должны перекодировать строку, прежде чем отправить ее обратно в HTML. Перекодировка означает замену символов: " — на ", > — на > и так далее. Следующий код является совершенно безопасным:
Write "Hello, " & Encode(Request("name"))
Все строки, которые приходят от пользователя, опасны. Любую опасную строку не следует выводить без перекодирования.
Попробуем придумать соглашение о кодировании, гарантирующее, что если вы когда-либо сделаете эту ошибку, код сразу же станет выглядеть неправильно. А если код неправильно выглядит, то разработчик или тот, кто просматривает код, сможет отловить ошибку.
Решение № 1
Одно из решений состоит в том, чтобы кодировать любую строку сразу, как только она пришла от пользователя:
s = Encode(Request("name"))
Таким образом, наше соглашение говорит: если вы увидите Request, который не заключен в Encode, то код неправильный.
Начинайте тренировать зрение на поиск голых Request, потому что они нарушают соглашение.
Такой способ действует — в том смысле, что, следуя этому соглашению, вы избежите XSS-атак, но не факт, что такая архитектура — лучшая. Например, если вам нужно хранить эти пользовательские строки где-нибудь в базе данных, нет смысла записывать их в базу в HTML-кодировке, потому что они могут предназначаться не для HTML-страницы, а, например, для приложения, обрабатывающего кредитные карточки, которое собьется, если получит строку в HTML-кодировке. Большинство веб-приложений разрабатывают по принципу, чтобы не перекодировать все строки внутри до самого последнего момента перед их отправкой на HTML-страницу, и это, по всей видимости, верная архитектура.
Итак, нам нужно какое-то время хранить данные в опасном формате. Хорошо. Попробую еще раз.
Решение №2
А если условиться, что все строки должны кодироваться при выводе?
s = Request("name")
// намного дальше:
Write Encode(s)
Теперь всякий раз, видя голый Write без Encode, вы догадаетесь, что здесь что-то упущено.
Однако такой метод не всегда действует правильно. Иногда в коде встречаются фрагменты HTML, которые нельзя перекодировать:
If mode = "linebreak" Then prefix = "<br>"
// намного дальше:
Write prefix
Это не соответствует нашему соглашению, которое требует, чтобы мы перекодировали строки при выводе:
Write Encode(prefix)
Но теперь префикс "<br>", которым должна начаться новая строка, будет перекодирован в <br> и пользователь увидит символы < b r >. Опять неправильно.
Итак, иногда нельзя перекодировать строку при вводе, а иногда — при выводе, таким образом, ни одно из предложенных соглашений не годится. А без соглашения мы постоянно подвергаемся риску написать примерно такой код:
s = Request("name")
...через несколько страниц...
name = s
...через несколько страниц...
recordset("name") = name // сохранить имя в БД в поле "name"
...через несколько дней...
theName = recordset("name")
...через несколько страниц или месяцев...
Write theName
Мы не забыли перекодировать строку? Неясно, как можно заметить ошибку. Непонятно, где копать. Если такого кода много, уйму сил придется потратить на то, чтобы проследить происхождение каждой строки, которую нужно вывести, и удостовериться, что она была перекодирована.
Правильное решение
Теперь позвольте мне предложить соглашение о кодировании, которое работает. У нас будет только одно правило:
Все строки, которые приходят от пользователя, должны быть сохранены в переменных (или полях базы данных), имена которых начинаются с префикса us (Unsafe String — опасная строка). Все строки, перекодированные для HTML или полученные из надежного источника, должны быть сохранены в переменных, имена которых начинаются с префикса s (Safe -безопасная строка).
Позвольте мне переписать тот же самый код, изменяя только имена переменных, так чтобы это соответствовало нашему новому соглашению.
us = Request("name")
... через несколько страниц...
usName = us
... через несколько страниц...
recordset("name") = usName // сохранить имя в БД в поле "name"
... через несколько дней...
sName = Encode(recordset("name"))
...через несколько страниц или месяцев...
Write sName
Хочу обратить ваше внимание на то, что если вы, соблюдая новое соглашение, допустили ошибку при действиях с опасной строкой, это всегда можно обнаружить в одной-единственной строке кода:
s = Request("name")
Заведомо неправильно, потому что результат Request присваивается переменной, название которой начинается с s, а это противоречит правилам. Результат Request всегда опасен, поэтому он должен всегда присваиваться переменной, имя которой начинается us.
Всегда правильно:
us = Request("name")
Всегда правильно:
usName = us
Явно неправильно:
sName = us
Правильно:
sName = Encode(us)
Явно неправильно:
Write usName
Правильно:
Write sName
Тоже правильно:
Write Encode(usName)
Каждую строку кода можно просмотреть отдельно, и если каждая строка кода правильна, то весь код верен в полном объеме.
Пользуясь таким соглашением о кодировании, нужно натренироваться находить в коде Write usxxx, и вы сразу заметите ошибку, зная, как ее исправить. Сначала будет нелегко увидеть неправильный код, но после трех недель такой работы ваши глаза научатся это делать — так же, как рабочие хлебопекарни научились, бросив один взгляд, сразу замечать: «Так, никто не почистил формовщик внутри! Что за бездельники тут работали?»
На самом деле, можно немного развить правило и переименовать (или обернуть) функции Request и Encode так, чтобы они стали UsRequest и Sen-code. Иными словами, имена функций, возвращающих опасные или безопасные строки, будут начинаться с Us и S, точно так же, как и переменные. Теперь взгляните на код:
us = UsRequest("name") usName = us
recordset("usName") = usName sName = Sencode(recordset("usName"))
Write sName
Видите, что я сделал? Теперь сразу видно ошибку, когда кодовые строки слева и справа от знака присваивания начинаются с разных префиксов.
us = UsRequest(''name'') // порядок, слева и справа начинается с "us"
s = UsRequestC'name") // ошибка
usName = us // порядок
sName = us // явно неправильно
sName = SEncode(us) // явно правильно
Пожалуй, можно сделать еще один шаг и переименовать Write в WriteS, а SEncode — в SFromUs:
us = UsRequest("name'') usName = us
recordset("usName") = usName
sName = SfromUs(recordset("usName"))
WriteS sName
В результате ошибки станут еще заметнее. Ваши глаза научатся обнаруживать код «с душком», и это поможет вам находить неясные ошибки защиты в процессе обычного написания или чтения кода.
Сделать так, чтобы плохой код бросался в глаза, — прекрасный подход, но не для всех проблем безопасности такое решение — лучшее. Не все ошибки или недостатки можно так обнаружить, потому что трудно просмотреть каждую строку кода. Но это явно лучше, чем ничего, и я с удовольствием приму соглашение о кодировании, при соблюдении которого неправильный код, по крайней мере, выглядит неправильно. Оно приносит пользу каждый раз, когда программист пробегает глазами строку кода для проверки отсутствия некоторой конкретной ошибки.
Общее правило
Для успешного применения правил, благодаря которым неправильный код и выглядит неправильно, нужно, чтобы все необходимое на экране располагалось как можно ближе друг к другу. Когда я ищу ошибки в коде и вижу строку, то где бы она ни находилась, мне нужно знать, опасная она или нет. И я не хочу, чтобы выяснить это, лезть в другой файл или на другую страницу. Я хочу видеть это тут же на месте, а для этого требуется соглашение об именовании переменных.
Есть ряд примеров, когда можно улучшить код, помещая нужные вещи близко друг к другу. Большинство соглашений о кодировании содержит правила наподобие следующих:
Функции должны быть короткими.
Объявляйте переменные как можно ближе к тому месту, где вы будете их использовать.
Не создавайте персональные языки программирования с помощью макросов.
Не применяйте goto.
Закрывающая фигурная скобка должна быть на одном экране с соответствующей ей открывающей скобкой.
Общее в этих правилах то, что все они стремятся как можно компактнее разместить всю информацию, относящуюся к тому, чем занимается некая строка кода. Это повышает вероятность того, что ваши глаза смогут охватить все, что нужно для понимания.
Должен признаться, я слегка опасаюсь особенностей языка, позволяющих что-то скрыть. Когда вы видите код
i=j * 5;
то в языке С вы, по крайней мере, знаете, что j умножается на 5, и результаты сохраняются в i.
Но если вы видите тот же фрагмент в C++, то не знаете о нем ничего. Абсолютно ничего. В C++ единственный способ узнать, что здесь действительно происходит, это выяснить, какие типы имеют i и j, а они могут быть объявлены в совершенно другом месте. Дело в том, что в зависимости от типа j оператор *может оказаться перегруженным для выполнения какой-нибудь заумной операции вместо обычного умножения. Да и оператор = может оказаться перегруженным для типа i, а типы могут оказаться несовместимыми, и в результате может быть автоматически вызвана функция приведения типов. И единственный способ разобраться во всем этом -не просто проверять типы, но отыскивать код, реализующий именно эти типы, и вам остается только молиться, встретив наследование, потому что тогда вам придется тащиться вверх по иерархии классов и искать, где все-таки на самом деле находится нужный код, а если где-нибудь есть полиморфизм, то вы действительно попали в беду, потому что тогда недостаточно знать, с какими типами объявлены i и j, — надо точно знать, какие типы у них именно сейчас, а сколько кода придется для этого просмотреть, уже вовсе неизвестно, и нет уверенности, что вы просмотрели все, потому что существует «проблема остановки» (уф!).
Видя в C++ код i = j * 5, вы можете рассчитывать только на себя, ребята, и, по-моему, это уменьшает возможность обнаружить потенциальные проблемы, просто глядя на код.
Конечно, никто не собирался осложнять вам жизнь. Если вам позволяют делать всякие умные штуки вроде перегрузки оператора *, так это для того, чтобы помочь вам обеспечить хорошую надежную абстракцию. Черт возьми, j имеет тип Unicode String, а умножение строки Unicode на целое число — несомненно, прекрасная абстракция для преобразования Традиционного Китайского в Стандартный Китайский, не так ли?
Беда в том, что надежных абстракций не бывает. Я уже достаточно много говорил об этом в Законе Дырявых Абстракций в другой книге, поэтому не стану повторяться.
Скотт Мейерс (Scott Meyers) 13 сделал себе имя, демонстрируя разнообразные ловушки, подстерегающие вас в абстракциях, по крайней мере, в C++.
Так. Я теряю нить. Пожалуй, подытожу все сказанное выше:
Ищите соглашения о кодировании, заставляющие неправильный код выглядеть неправильно. Старайтесь делать так, чтобы вся нужная информация размещалась в одном и том же месте экрана, так, чтобы ваш код позволял ясно видеть определенные проблемы и сразу же их устранять.
Я — венгр
Итак, вернемся к пресловутой венгерской нотации.
Венгерская нотация была изобретена программистом Microsoft Чарльзом Симони (Charles Simonyi). Одним из крупных проектов, над которыми работал Симони в Microsoft, был Word; на самом деле, он руководил проектом по созданию первого в мире текстового редактора, работающего в режиме WYSIWYG (того, что в Xerox Parc называли Bravo).
При обработке текстов в режиме WYSIWYG окна прокручиваются, поэтому все координаты можно интерпретировать как относительно окна, так и относительно страницы, а это большая разница, и очень важно корректно работать с ними.
Это, как я предполагаю, и стало одной из веских причин, по которым Симони начал применять то, что впоследствии было названо «венгерской нотацией». Она была похожа на венгерский язык (Симони — венгр), отсюда и пошло название. В версии венгерской нотации, предложенной Симо-ни, имя каждой переменной содержало префикс из строчных букв, указывающий вид данных, которые содержит эта переменная.
Я нарочно использую здесь слово «вид» (kind), потому что Симони по ошибке использовал в своей статье слово «тип» (type), и целые поколения программистов не так поняли, что он имел в виду.
Если вы читали статью Симони внимательно, то знаете, что он предложил соглашение об именах переменных, похожее на использованное мной в приведенном выше примере, где мы решили, что us обозначает «опасную строку», а s — «безопасную строку». Обе эти переменные имеют тип string. Компилятор не помешает вам присвоить одной из них значение
Между прочим, вышло третье издание книги Скотта «Эффективный C++» («Effective C++»), полностью переработанное.
другой, и IntelliSense ничего не подскажет. Но семантически они различаются; они должны интерпретироваться и рассматриваться по-разному, и если вы присваиваете значение переменной одного типа переменной другого типа, то нужно вызывать функцию для конвертации, иначе возникнет ошибка времени выполнения (runtime bug). Если вам повезет.
Оригинальную концепцию нотации Симони в Microsoft назвали Apps Hungarian («венгерской для приложений»), потому что применялась в отделе приложений (Applications Division), который, в частности, разрабатывал Word и Excel. В исходном тексте Excel вы встретите много rw и col и, видя их, сразу поймете, что они относятся к строкам и столбцам. Да, это целые числа, но бессмысленно присваивать значение строки столбцу, и наоборот. В Word, как мне говорили, есть многочисленные xl и xw, где xl означает «горизонтальная координата относительно листа», а xw — «горизонтальная координата относительно окна». Обе переменные целые. Не взаимозаменяемые. В обоих приложениях часто встречается cb, что означает «счетчик байтов». Да, это тоже целое, но чтобы узнать это, достаточно посмотреть на имя переменной. Это счетчик байтов — размер буфера. И если вы видите xl = cb, то это сигнал: очевидно, что код здесь — неправильный, потому что даже при том, что и xl, и cb являются целыми, полный идиотизм присваивать горизонтальной координате размер буфера в пикселах.
В нотации Apps Hungarian префиксы используются не только для переменных, но и для функций. Честно говоря, я никогда не видел исходного текста Word, но готов держать пари на доллар против цента, что в нем есть функция по имени YlFromYw, преобразующая вертикальные координаты относительно окна в вертикальные координаты относительно листа. Нотация Apps Hungarian требует вместо более привычного TypeToType использовать TypeFromType, чтобы имя каждой функции начиналось с типа данных, которые она возвращает, — точно так же, как это сделал я в своем примере, переименовав функцию Encode в SFromUs. По правилам Apps Hungarian вы обязаны дать функции Encode название SFromUs. Apps Hungarian не оставляет вам выбора в названии функции. И это хорошо, потому что меньше нужно запоминать и не придется задаться вопросом, какую кодировку осуществляет Encode, — у вас есть нечто гораздо более точное.
Нотация Apps Hungarian была чрезвычайно ценна, особенно в эпоху программирования на С, когда компилятор предоставлял не слишком удобную систему типов.
Но затем случилась неприятность.
Силы зла завладели венгерской нотацией.
Никто в точности не знает, но похоже, что это авторы документации из команды Windows неосторожно изобрели то, что стали называть Systems Hungarian (системная венгерская).
Кто-то где-то прочел статью Симони, в которой говорилось о «типе», и решил, что автор имел в виду тип как класс, как в языке с системой типов, как проверку типов, выполняемую компилятором. Автор имел в виду не это. Он тщательно и точно объяснил, что подразумевал под словом «тип», но это не помогло. Вред был нанесен.
В Apps Hungarian были определены очень полезные и содержательные префиксы, такие как ix — для обозначения индекса в массиве, c — для счетчиков, d — для разности между двумя числами (например, префикс dx означал «ширину») и так далее.
В Systems Hungarian были гораздо менее полезные префиксы, такие как I — для длинного целого (long), ul — для беззнакового длинного целого (unsigned long) и dw — для двойного слова (double word), которое, фактически, э-э, является беззнаковым длинным целым. В Systems Hungarian префикс сообщал вам лишь одно, а именно: фактический тип данной переменной.
Это было скрытым, но совершенно ошибочным пониманием намерений и практики Симони, и должно служить примером того, что если написать сложный и непонятный научный текст, то никто его не поймет, ваши идеи будут извращены, а затем эти извращенные идеи будут высмеяны, несмотря на то что они никогда не были вашими. Так, в Systems Hungarian появлялись всякие dwFoo, имена которых говорили вам, черт возьми, только то, что эта foo занимает двойное слово, что, в общем-то, не сообщает вам ничего полезного. Неудивительно, что у Systems Hungarian появились противники.
Нотацию Systems Hungarian распространили; она стала стандартом во всей документации по программированию Windows; ее широко рекламировали такие книги, как «Программирование в Windows» Чарльза Пет-цольда («Programming Windows», Charles Petzold), настоящая библия для изучающих программирование в Windows, и она быстро стала доминирующим видом венгерской нотации даже в Microsoft, где только очень немногие программисты помимо разработчиков Word и Excel поняли, какая ошибка была сделана.
А затем пришел Великий Бунт. В конечном счете, программисты, которые никогда не понимали венгерскую нотацию правильно, первые заметили, что то неправильно понятое подмножество, которое они использовали, было Раздражающим и Почти Бесполезным, и восстали против него. Однако у Systems Hungarian все-таки есть хорошие качества, помогающие видеть ошибки. По крайней мере, применяя Systems Hungarian, можно сразу узнать тип переменной там, где вы ее используете. Но это несравнимо с достоинствами Apps Hungarian.
Пик Великого Бунта совпал с первым выпуском .NET. Microsoft наконец начала говорить, что пользоваться венгерской нотацией не рекомендуется. По этому поводу было много веселья. По-моему, они даже не потрудились объяснить причин. Просто открыли раздел руководства, посвященный правилам именования, и написали «Не используйте венгерскую нотацию» во всех статьях. К этому времени венгерская нотация была уже так непопулярна, что никто не стал жаловался, и все, кроме разработчиков Excel и Word, только облегченно вздохнули, избавившись от неуклюжего соглашения об именах, в котором, как они считали, не было нужды в эпоху строгой проверки типов и IntelliSense.
Но в нотации Apps Hungarian все-таки очень много полезного, поскольку она увеличивает сорасположение в коде, что облегчает чтение, написание, отладку и сопровождение кода, а также, что важнее всего, она заставляет неправильный код выглядеть неправильно.
Прежде чем двинуться дальше, я должен сделать то, что пообещал, -еще раз пнуть обработку исключений (exceptions). В предыдущий раз у меня были большие неприятности. В замечании, небрежно брошенном на главной странице «Joel on Software», я написал, что не люблю исключения, потому что фактически за ними кроется goto, а это, как я рассуждал, хуже явного goto. Разумеется, мне в глотку вцепились миллионы. Единственным из всех, кто вступился за меня, был, конечно, Реймонд Чен (Raymond Chen), между прочим, лучший в мире программист — это кое-что значит, не правда ли?
Вот пример с обработкой исключений в контексте данной статьи. Глаз учится замечать неправильные вещи, когда они выделяются, и это предотвращает ошибки. Для того чтобы при просмотре кода выявлять ошибки, делая код действительно надежным, нужно применять соглашения о кодировании, поддерживающие правильное расположение информации. Другими словами, чем больше информации о том, что делает код, расположено прямо перед вашими глазами, тем эффективнее окажется поиск ошибок. Допустим, есть код:
dosomething();
cleanup();
Замечаете, что здесь что-то не так? «Мы всегда делаем уборку!» Но если dosomething вызовет исключение, то до cleanup дело может не дойти. Это легко исправить с помощью fi nally или чего-то подобного, но я не об этом; я о том, что единственный способ узнать, что cleanup точно вызовется, это исследовать все дерево вызовов dosomething и разобраться, нет ли где-нибудь чего-нибудь такого, что может вызвать исключения, и это правильно, и есть такие вещи, как проверяемые исключения, которые облегчают жизнь, но я хочу указать на то, что исключения мешают близкому расположению информации. Чтобы узнать, правильно ли работает код, вы должны обращаться в какое-то другое место и потому не можете воспользоваться естественной способностью вашего глаза видеть неправильный код, потому что смотреть не на что.
Когда я пишу маленький скрипт, чтобы раз в день собрать какие-то данные и напечатать их, то да, исключения работают великолепно. Нет ничего приятнее, чем забыть про возможные неприятности и просто обернуть всю программу в один большой обработчик исключений try/catch, который пошлет мне по электронной почте сообщение, если что-нибудь случится. Исключения прекрасно подходят для кода «на скорую руку» -скриптов и кода, от которого не зависит ничья жизнь и судьба. Но если вы пишете операционную систему, или систему управления атомной электростанцией, или приложение для управления медицинской техникой, применяемой при операции на сердце, исключения чрезвычайно опасны.
Знаю, некоторые решат (и напрасно), что я плохой программист, если не могу разобраться в исключениях и в том, как они могут улучшить мою жизнь, если только я допущу их в свое сердце. Чтобы писать действительно надежный код, нужно применять простые инструменты, которые учитывают свойственную человеку ненадежность, а не сложные инструменты со скрытыми побочными эффектами и дырявыми абстракциями, предполагающими, что программист не делает ошибок.
Дополнительная литература
Если вы все еще остаетесь фанатиком исключений, прочитайте статьи Раймонда Чена «Cleaner, more elegant, and harder to recognize» («Чище, элегантней и сложней для понимания») и «Cleaner, more elegant, and wrong» («Чище, элегантней и неправильно») (blogs.msdn.com/oldnewthing/ archive/2005/01/14/352949. aspx): «Крайне тяжело увидеть различие между плохим кодом, основанным на исключениях, и не-плохим кодом, основанным на исключениях... исключения слишком сложны, и я не достаточно умен, чтобы с ними работать».
В рассуждениях о Смерти от Макросов, «A Rant Against Flow Control Macros» («Речь против макросов, управляющих исполнением программы», blogs.msdn.com/oldnewthing/archive/2005/01/06/34 7666.aspx), Раймонд описывает другой случай, когда невозможность получить всю информацию в одном и том же месте делает невозможной сопровождение кода. «Когда вы видите код, который использует [макросы], вам приходится копаться в заголовочных файлах, чтобы понять, как это работает».
Для изучения венгерской нотации начните с оригинальной статьи Си-мони «Hungarian Notation» («Венгерская Нотация», msdn. microsoft. com/en-us/library/aa260976(VS. 60).aspx). Дуг Кландер (Doug Klunder) объяснил ее команде Excel в несколько более понятной статье «Hungarian Naming Conventions» («Венгерские соглашения об именовании», www. byteshift. de/msg/ hungarian-notation-doug-klunder). Чтобы узнать больше о венгерской нотации и о том, как ее обрушили составители документации, прочтите пост Ларри Остермана (Larry Osterman) (blogs. msdn. com/larryosterman/archive/ 2004/06/22/162629. aspxf), в особенности комментарий Скотта Людвига (Scott Ludwig) (blogs, msdn. com/larryosterman/archive/2004/06/22/162629. aspx#l63721) или сообщение Рика Шота (Rick Schaut) (blogs, msdn. com/ rick_schaut/archive/2004/02/14/73108. aspx).
***
13 Между прочим, вышло третье издание книги Скотта «Эффективный C++» («Effective C++»), полностью переработанное