Dimka
Деятель
Команда клуба
Offline
Пол:
|
|
« Ответ #30 : 04-02-2011 22:51 » |
|
Вот я тоже сегодня, когда пытался Димке объяснить суть моих претензий к идее "исключения на C", прежде всего пытался выразить, что я чувствую, когда подумаю, что строго императивный код вдруг посредине набора инструкций могут прервать и выдернуть на три уровня вверх в точку обработки. Интуитивную тревогу, вот что Я с BASIC начинал, поэтому врождённого отвращения к GOTO у меня нет. Но я знаю, когда GOTO использовать не надо ни при каких обстоятельствах Когда при помощи GOTO перемешиваются два и более алгоритма, два и более потока управления, если они не разделены на уровне исходного кода. При наличии разделения потоков любым способом GOTO - это не то, что нормальный, а единственный инструмент при работе с одним процессором. По этой причине, кстати, меня до того заинтересовали сопрограммы, что я и примеров понаписал, и даже лекцию студентам прочитал, и практические занятия провёл. Сегодня за сыром ты мне говорил, что в случае ошибок для тебя нормально делать return из функции, и что функция с десятком return - обычное дело. Сие есть тот же самый GOTO, только в профиль Поэтому с этой точки зрения я не вижу причин к ломанию копий - и throw, и return (а также break, continue и прочие фичи C/C++) не являются "строго императивным кодом" (точнее, строго структурным). Тем не менее, одно у тебя не вызывает "интуитивной тревоги", а другое вызывает. И здесь я начинаю подозревать, что это вопрос эстетический, вкуса, привычек; рациональных оснований для тревоги я тут не вижу никаких (а также не чувствую подсознательно). Возможно дело в том, что ты воспринимаешь написанный императивный код как один поток инструкций. Я (и Dale, похоже, тоже) воспрнимаю это как множество потоков. Именно поэтому здесь часто упоминаются слова "прерывания", "сценарии" и иже с ними. В данном случае GOTO работает как переключатель этих потоков. И в случае прерывания, и в случае исключения, и в случае синхронизации параллельных потоков возникает задача переключить процессор с исполнения одного потока (который в первом и последнем случае временно приостанавливается, а во втором - останавливается навсегда) на какой-то другой альтернативный поток. В чём для меня прелесть try-catch - это в возможности каждый поток записать в отдельной секции и тем самым визуально разделить их в коде. Это облегчает работу с кодом, но, конечно, чтение кода перестаёт быть похожим на чтение романа и становится похожим на работу со справочником. Возможно, это у меня от плотной работы с GUI пошло - там нет единого потока и единого алгоритма, там сплошные обработчики, из которых нужно собрать работающую систему. Но это, действительно, другой способ мышления программиста, другой способ восприятия программ. А в случае параллельного программирования я вообще не знаю иного способа - ведь в общем случае при реальном параллелизме любая инструкция одного потока может оказаться одновременной с любой инструкцией другого потока. Конечно, это вызывало "интуитивную тревогу" ещё у Дейкстры, но... се ля ви. Кому нужно и зачем? Я предпочитаю перехватывать их в том месте, где их можно осмысленно обработать, а не просто передать код завершения дальше по цепочке вызовов в надежде, что им кто-то заинтересуется. И это совсем не обязательно "очень рано". Это несколько другая тема, но тут я согласен с Вадом. Исключения не должны уходить с уровня подсистемы на уровень (над)системы. Если это происходит, возникает явление, которое называется "дырявая абстракция". Если подсистема инкапсулирует в себе особенности своей реализации, то никакой код ошибки, никакое внутреннее исключение, имеющее смысл только в рамках реализации, и ни о чём не говорящее на уровне интерфейса, не должно покинуть подсистему. В этом случае делается перехват всех исключений реализации, а вместо них порождается новое исключение, соответствующее интерфейсу. Поскольку лично я стараюсь структурировать системы таким образом, чтобы всякая система состояла из малого (обозримого) количества подсистем (и потому она всегда оказывается небольшой на своём уровне абстракции), постольку в рамках вышеизложенных соображений об инкапсуляции перехват исключений чаще всего происходит "как можно раньше".
|
|
« Последнее редактирование: 04-02-2011 22:53 от Dimka »
|
Записан
|
Программировать - значит понимать (К. Нюгард) Невывернутое лучше, чем вправленное (М. Аврелий) Многие готовы скорее умереть, чем подумать (Б. Рассел)
|
|
|
Dale
|
|
« Ответ #31 : 05-02-2011 07:09 » |
|
Исключения не должны уходить с уровня подсистемы на уровень (над)системы. Если это происходит, возникает явление, которое называется "дырявая абстракция". Если подсистема инкапсулирует в себе особенности своей реализации, то никакой код ошибки, никакое внутреннее исключение, имеющее смысл только в рамках реализации, и ни о чём не говорящее на уровне интерфейса, не должно покинуть подсистему. Де-факто иключения давно стали частью интерфейса. К сожалению, в формальной грамматике того же C# это не нашло отражения (нет никакого аналога инструкции throws, которая предупреждала бы, что метод может выбросить такие-то исключения), но в MSDN это нормальная практика: в документации любого метода наравне с описаниями входных и выходных параметров обязательно присутствует раздел с перечнем всех исключений, которые можно ожидать от него. Поэтому никаких дыр в абстракциях исключения не проделывают при разумном использовании (ну а при неразумном практически любой инструмент лишь калечит горе-мастера), фактически это лишь расширение поведения метода/функции/подпрограммы/.... Возьмем такой пример: repeat write("Введите значения a, b, c: ") read(a, b, c)
try { вычисления с глубиной вложенности вызовов > 1, в которых используется наш треугольник } catch(e) { if (e - одна из ошибок входных параметров) then установить флаг ошибки входных данных if (другая причина исключения) ... }
if (флаг ошибки входных данных установлен) { write("Ошибка исходных данных") по-хорошему не мешает уточнить ее причину (нулевая или отрицательная сторона, треугольник не строится и т.д., сделав сообщение максимально информативным } until (флаг ошибки входных данных сброшен)
В данном случае не имеет смысла перехватывать исключение на промежуточных уровнях вызова, поскольку там все равно ничего полезного нельзя сделать. Исправить ошибку можно лишь в главной программе, повторив попытку ввода. Разумеется, исключения должны быть информативными. Получить в главной программе нечто вроде "division by zero" или "invalid handle 0xBC0018A4", когда в стеке 12 уровней вложенности вызовов, невеликое счастье. Но повторюсь: ни один инструмент не способен превратить дурака в гения, и исключения тут - не исключение (в прямом смысле). Понимающий получает в свой арсенал еще одно полезное средство, непонимающий - еще одно приключение на свой многострадальный зад.
|
|
|
Записан
|
Всего лишь неделя кодирования с последующей неделей отладки могут сэкономить целый час, потраченный на планирование программы. - Дж. Коплин.
Ходить по воде и разрабатывать программное обеспечение по спецификациям очень просто, когда и то, и другое заморожено. - Edward V. Berard
Любые проблемы в информатике решаются добавлением еще одного уровня косвенности – кроме, разумеется, проблемы переизбытка уровней косвенности. — Дэвид Уилер.
|
|
|
Вад
|
|
« Ответ #32 : 05-02-2011 09:01 » |
|
Сегодня за сыром ты мне говорил, что в случае ошибок для тебя нормально делать return из функции, и что функция с десятком return - обычное дело. Сие есть тот же самый GOTO, только в профиль Поэтому с этой точки зрения я не вижу причин к ломанию копий - и throw, и return (а также break, continue и прочие фичи C/C++) не являются "строго императивным кодом" (точнее, строго структурным). Тем не менее, одно у тебя не вызывает "интуитивной тревоги", а другое вызывает. И здесь я начинаю подозревать, что это вопрос эстетический, вкуса, привычек; рациональных оснований для тревоги я тут не вижу никаких (а также не чувствую подсознательно). Возможно дело в том, что ты воспринимаешь написанный императивный код как один поток инструкций. Я (и Dale, похоже, тоже) воспрнимаю это как множество потоков. Ну, во-первых, да, как человек, дописавшийся когда-то до ассемблера, я воспринимаю всё это как один поток инструкций. Когда я пишу многопоточную программу - я и воспринимаю её как независимые потоки инструкций (если не затрагивать вопросов синхронизации). В этом смысле, меня goto тревожит, но в разумных пределах. Во-вторых же, я воспринимаю функцию как единый блок логики, что ли. То есть, условный return в середине функции не нарушает логики выполнения, потому что выбрасывает меня за пределы чётко означенного блока на один уровень. break не тревожит по той же причине: он имеет чётко предсказуемый и локальный смысл (по этой причине, кстати, я считаю, что применение в java break-а, позволяющего прервать любой из вложенных циклов с помощью метки -- неудачная практика, указывающая на проблемы с описанием логики). Да и сам goto использовать без возникновения тревоги я смогу без выхода за границы одной функции. То есть, все эти формы условного перехода контролируемы, пока они лежат внутри моего логического блока и не пытаются делать что-то более сложное. А именно, то, что ты называешь выходом с уровня подсистемы на уровень надсистемы: изменение не только своего потока выполняемых команд, но и оного потока для надсистемы. И вот когда подсистема начинает влиять на то, как себя надсистема ведёт, в языке, которому обычно это не свойственно, это меня тревожит. Наверное, просто потому, что надо перестраивать мышление и забыть о простоте программирования на C Вот только, кажется, в том-то смысл этого языка для меня и был, что не нужно было думать, так сказать, о прерываниях.
|
|
« Последнее редактирование: 05-02-2011 09:03 от Вад »
|
Записан
|
|
|
|
Dimka
Деятель
Команда клуба
Offline
Пол:
|
|
« Ответ #33 : 05-02-2011 09:13 » |
|
Dale, ты не понял. Если на одном уровне абстракции программа пытается, например, использовать курсы валют для каких-то расчётов, то вся подсистема, отвечающая за курсы валют, должна иметь соглашение о порядке предоставления информации. Например, всегда выдавать самый свежий известный системе курс, а если такогового нет, то брать 1. void calc_cost(int goods_count, struct amount price, struct amount *cost) { if(cost == NULL) { return; } cost->currency = USD; cost->value = goods_count * price.value * get_exchange_rate(price.currency, cost.currency); } Тут потенциальной ошибкой может быть только переполнение при умножении, чего (по здравому размышлению) в 99.(9)% случаев не ожидается, а если и просиходит, то это не влечёт нарушение работы функции - проверки избыточны. По соглашению нужно получить курс валюты из справочника, а если её там нету, то вернуть 1. Предположим, что справочник возвращает либо курс, либо бросает исключения "неизвестная валюта" и "нет данных": decimal get_exchange_rate(currency currency_1, currency currency_2) { struct search_conditions conditions; init_search_conditions(&conditions); decimal exchage_rate; try { conditions.seek = SC_SEEK_LAST; exchage_rate = find_exchange_rate(currency_1, currency_2, conditions); } catch(struct exception *error) { exchage_rate = 1.0; } return exchage_rate; } Здесь нам не важна внутренняя причина, по которой find_exchange_rate не смог выполнить запрос, ибо на любую причину у нас только одна реакция. Исключения выше не идут. Здесь возврат 1.0 на любые ошибки - то, о чём говорил Вад про кодеров на Java. Это подозрительное решение, скверно влияющее на предсказуемое поведение системы с точки зрения пользователя. Пользователь никогда не знает, получит ли он правильный результат расчёта или неправильный из-за внутренних ошибок. Запросы в справочнике обрабатываются следующим образом: если хотя бы одной из указанных валют нет, бросаем исключение "неизвестная валюта"; если валюты есть, то получаем данные по условию выбора, иначе бросаем исключение "нет данных"; если условием выбора была последняя запись, то проверяем её свежесть; если получена более старая запись, чем требуется, пробуем получить с удалённого сервера новые данные. decimal find_exchange_rate(currency currency_1, currency currency_2, struct search_conditions conditions) { struct exception error; struct exchange_rate_record dictionary_record; time_t current_time; double days_old; if(!exists_currency_in_exchange_rate_dictionary(currency_1) || !exists_currency_in_exchange_rate_dictionary(currency_2)) { error.id = ERR_UNKNOWN_CURRENCY; throw error; } try { select_exchange_rate(currency_1, currency_2, conditions, &dictionary_record); time(¤t_time); days_old = difftime(current_time, dictionary_record.date) / 24 * 60 * 60; if(days_old > 1) { update_exchange_rate(); select_exchange_rate(currency_1, currency_2, conditions, &dictionary_record); } } catch(struct exception *error) { error.id = ERR_NO_DATA; throw error; } return dictionary_record.exchange_rate; } Здесь используются две вспомогательные функции select_exchange_rate и update_exchange_rate. Как они реализованы, совершенно неинтересно ни на этом уровне, ни на вышележащем. Если select_exchange_rate работает с файлом, она может бросать исключения, связанные с открытием, чтением файла, отсутствием прав доступа; если select_exchange_rate работает с базой данных, она может бросать исключения, связанные с невозможностью подключения, отказом в авторизации, внутренними сбоями в работе СУБД; если update_exchange_rate получает данные от внешнего веб-сервиса в интернете, она может бросать целый ворох исключений, связанных с работой в сети, отказами веб-сервиса, а также ошибками в формате получаемых данных. Речь идёт о том, что если это не перехватить, не упаковать в своё собственное исключение "нет данных", то всю эту "бороду" исключений и связанных с ними catch секций придётся вытаскивать на более высокий уровень, на котором ничего этого не должно быть видно. Потому что все указанные исключения - это особенности реализации справочника валют. Если их выпустить наружу и тем предположить, что кто-то сможет (и будет) их анализировать и обрабатывать по своему усмотрению, получим нарушение инкапсуляции и связанность по рукам и ногам при попытках смены реализации хранилища данных. Т.е. исключения должны проектироваться в интерфейсе наравне с данными и процедурами их обработки, и никакие исключения, не включённые в интерфейс по замыслу автора интерфейса, не должны туда попадать "по факту" - разработчик обязуется их перехватить и преобразовать в интерфейсные исключения, иначе это можно расценить как нарушение контракта, недокументированное поведение. Если хорошо подумать, то поскольку функция select_exchange_rate может быть реализована разными способами (работа с файлом, работа с СУБД), то даже она не должна бросать исключения, связанные с обработкой файла или взаимодействием с СУБД - они должны быть инкапсулированы в ней и обобщены до каких-то других исключений. Это и влечёт стратегию перехвата исключений как можно ближе к месту их возникновения. В пределе эта стратегия приводит к тому, что выбор между исключениями и возвратом кода ошибки становится безразличным - никакое исключение не проходит выше 1 уровня вызовов функции по стеку вызовов.
|
|
« Последнее редактирование: 05-02-2011 09:25 от Dimka »
|
Записан
|
Программировать - значит понимать (К. Нюгард) Невывернутое лучше, чем вправленное (М. Аврелий) Многие готовы скорее умереть, чем подумать (Б. Рассел)
|
|
|
Dale
|
|
« Ответ #34 : 05-02-2011 11:16 » |
|
Dale, ты не понял. ... Речь идёт о том, что если это не перехватить, не упаковать в своё собственное исключение "нет данных", то всю эту "бороду" исключений и связанных с ними catch секций придётся вытаскивать на более высокий уровень, на котором ничего этого не должно быть видно. Что же тут мной не понято? О том и речь ведется: Разумеется, исключения должны быть информативными. Получить в главной программе нечто вроде "division by zero" или "invalid handle 0xBC0018A4", когда в стеке 12 уровней вложенности вызовов, невеликое счастье. Низкоуровневые исключения не несут никакой информации на высших уровнях абстракции, поэтому их просто невозможно там корректно обработать. Нигде в статье и моих комментариях не утверждается, что перехватывать исключения нужно непременно на высших уровнях абстракции. Равно бессмысленно утверждать, что их обязательно нужно перехватывать на нижних. Делать это нужно на том уровне, где эта информация еще значима и можно предпринять осмысленные действия. Частный случай такого действия - выброс нового, более высокоуровнего исключения, но это вовсе не обязательно. Это и влечёт стратегию перехвата исключений как можно ближе к месту их возникновения. В пределе эта стратегия приводит к тому, что выбор между исключениями и возвратом кода ошибки становится безразличным - никакое исключение не проходит выше 1 уровня вызовов функции по стеку вызовов. Я бы полностью согласился, если бы "ближе" и "уровень" рассматривались с точки зрения уровней абстракции. Когда же речь идет о близости уровней в физическом плане (глубина стека вызовов), эти доводы становятся менее убедительны. Например, каждое применение рефакторинга "Извлечение метода" добавляет один физический уровень вызова, оставляя абстракцию на том же уровне. Другой пример - сложное математическое выражение, когда вычисляется функция от функции от функции... Глубина стека вызовов большая, но это в данном случае ничего не значит. Скажем, если мы вычисляем ln(sqrt(x)), функции вычисления логарифма вовсе не обязательно обрабатывать исключение, возникшее при вычислении корня, хотя они и соседствуют в стеке.
|
|
|
Записан
|
Всего лишь неделя кодирования с последующей неделей отладки могут сэкономить целый час, потраченный на планирование программы. - Дж. Коплин.
Ходить по воде и разрабатывать программное обеспечение по спецификациям очень просто, когда и то, и другое заморожено. - Edward V. Berard
Любые проблемы в информатике решаются добавлением еще одного уровня косвенности – кроме, разумеется, проблемы переизбытка уровней косвенности. — Дэвид Уилер.
|
|
|
Dimka
Деятель
Команда клуба
Offline
Пол:
|
|
« Ответ #35 : 05-02-2011 11:33 » |
|
Например, каждое применение рефакторинга "Извлечение метода" добавляет один физический уровень вызова, оставляя абстракцию на том же уровне. Вот это странно. Какой смысл выделять часть кода в отдельный метод, если при этом не абстрагироваться от его реализации?
|
|
|
Записан
|
Программировать - значит понимать (К. Нюгард) Невывернутое лучше, чем вправленное (М. Аврелий) Многие готовы скорее умереть, чем подумать (Б. Рассел)
|
|
|
Dale
|
|
« Ответ #36 : 05-02-2011 11:41 » |
|
Вот это странно. Какой смысл выделять часть кода в отдельный метод, если при этом не абстрагироваться от его реализации? Например, при устранении дублирования одинаковых фрагментов кода. Или при разбиении чрезмерно длинного метода на обозримые фрагменты. Добавлено через 28 минут и 35 секунд:Вот еще пример ситуации, когда глубина стека вызова никак не связана с уровнями абстракции. Допустим, у нас есть модуль для рисования графика функции, модуль аффинных преобразований и прибор со встроенным микропроцессором и графическим дисплеем. Прибор измеряет некоторые параметры, после чего показывает результаты на графике в одном из окошек на дисплее. Для этого он: - добавляет очередную точку к массиву данных;
- строит график при помощи соответствующего модуля, получая метафайл графических данных;
- разворачивает изображение на 90 градусов против часовой стрелки (особенность дисплея, повернутого в ландшафтную ориентацию);
- масштабирует изображение по осям X и Y, чтобы оно вписалось в рзмеры окошка;
- осуществляет параллельный перенос изображения, отображая на выделенную для графика область.
Поскольку нас не интересуют промежуточные результаты, мы можем записать результирующее преобразование в виде: перенести(dx, dy, масштабировать(kx, ky, повернуть(90, построить(dataArray))));Любая из этих операций в принципе может вызвать исключение (например, выход за пределы канвы). С другой стороны, масштабирование не является абстракцией для переноса, а поворот - для масштабирования. Эти операции представляют один уровень абстракции, а стек вызовов отражает лишь последовательность их применения. Перехват исключений в данном случае на предыдущем уровне, хотя технически легко реализуется, вряд ли имеет реальный смысл - функция масштабирования просто не знает, что делать с ошибкой функции поворота.
|
|
« Последнее редактирование: 05-02-2011 14:11 от Dale »
|
Записан
|
Всего лишь неделя кодирования с последующей неделей отладки могут сэкономить целый час, потраченный на планирование программы. - Дж. Коплин.
Ходить по воде и разрабатывать программное обеспечение по спецификациям очень просто, когда и то, и другое заморожено. - Edward V. Berard
Любые проблемы в информатике решаются добавлением еще одного уровня косвенности – кроме, разумеется, проблемы переизбытка уровней косвенности. — Дэвид Уилер.
|
|
|
Dimka
Деятель
Команда клуба
Offline
Пол:
|
|
« Ответ #37 : 05-02-2011 16:01 » |
|
Dale, признаться, мне твои примеры непонятны. если мы вычисляем ln(sqrt(x)), функции вычисления логарифма вовсе не обязательно обрабатывать исключение, возникшее при вычислении корня Логарифм ничего не знает про корень. Это вызывающая программа знает про обе эти функции. Ты говоришь так, будто в логарифм передаётся указатель на функцию расчёта корня, и логарифм вызывает эту функцию. Здесь же вызывающая программа вызывает расчёт корня, получает результат и только потом вызывает расчёт логарифма, передавая ему этот результат в качестве аргумента. Вложенность вызова всегда 1, данные в стеке не соседствуют - они там сменяют друг друга во времени. перенести(dx, dy, масштабировать(kx, ky, повернуть(90, построить(dataArray)))); Аналогично.
|
|
|
Записан
|
Программировать - значит понимать (К. Нюгард) Невывернутое лучше, чем вправленное (М. Аврелий) Многие готовы скорее умереть, чем подумать (Б. Рассел)
|
|
|
Dale
|
|
« Ответ #38 : 05-02-2011 18:13 » |
|
Dale, признаться, мне твои примеры непонятны. Точно, второпях не то написал. Предыдущий пример явно не в тему в случае C. Это я параллельно работаю над своим интерпретатором, там действительно такие вложения функций приводят к вложенным вызовам.
|
|
|
Записан
|
Всего лишь неделя кодирования с последующей неделей отладки могут сэкономить целый час, потраченный на планирование программы. - Дж. Коплин.
Ходить по воде и разрабатывать программное обеспечение по спецификациям очень просто, когда и то, и другое заморожено. - Edward V. Berard
Любые проблемы в информатике решаются добавлением еще одного уровня косвенности – кроме, разумеется, проблемы переизбытка уровней косвенности. — Дэвид Уилер.
|
|
|
RXL
|
|
« Ответ #39 : 01-08-2011 19:02 » |
|
Интересная особенность использования Throw() конце функций с возвращаемым значением: последнее всегда нужно указывать. int func(int x) { if (x > 0) return x;
Throw(E_INVALID_VALUE); return 0; } Если не указать последний return, то компиляция будет с предупреждением. Например, в gcc: "control reaches end of non-void function". Не знаю, что на этот счет в VC или в BCB придумано, но если рассчитывать только на gcc, то можно добавить атрибут noreturn в прототип Throw(). void Throw(CEXCEPTION_T ExceptionID) __attribute__ ((noreturn)); Проверил компиляцией в asm: код становится короче даже без оптимизации, а с оптимизацией (-O2 или -O3) - короче, чем без атрибута.
|
|
« Последнее редактирование: 01-08-2011 19:33 от RXL »
|
Записан
|
... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
|
|
|
RXL
|
|
« Ответ #40 : 01-08-2011 19:13 » |
|
И фишка, не оговоренная в статье: возможность пересмотра настроечных макросов в файле CExceptionConfig.h. Можно изменить макросы CEXCEPTION_NONE, CEXCEPTION_NUM_ID, CEXCEPTION_GET_ID и CEXCEPTION_T. Особенно полезно для микроконтроллеров - первый и последний. #ifndef CEXCEPTIONCONFIG_H #define CEXCEPTIONCONFIG_H
#define CEXCEPTION_NONE 0xff #define CEXCEPTION_T unsigned char
#endif /* CEXCEPTIONCONFIG_H */ Но теперь нужно при компиляции всех модулей, включающих заголовок CException.h, нужно определить макрос CEXCEPTION_USE_CONFIG_FILE. $ $ gcc -Wall -DCEXCEPTION_USE_CONFIG_FILE -с test_exceptions.c CException.c $
В принципе, ничего не мешает исправить нужные макросы сразу в CException.h.
|
|
|
Записан
|
... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
|
|
|
RXL
|
|
« Ответ #41 : 02-08-2011 21:00 » |
|
Посмотрел состав контекста в WinAVR: прилично - 23 или 24 байта. Главное не слишком увлекаться этой приятной возможностью. /* jmp_buf: offset size description 0 16 call-saved registers (r2-r17) 16 2 frame pointer (r29:r28) 18 2 stack pointer (SPH:SPL) 20 1 status register (SREG) 21 2/3 return address (PC) (2 bytes used for <=128Kw flash) 23/24 = total size */
|
|
|
Записан
|
... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
|
|
|
Dale
|
|
« Ответ #42 : 03-08-2011 06:12 » |
|
Для небольших программ глубины вложения блоков 3-4 уровня вроде должно хватить. Для самых младших моделей многовато, для средних должно потянуть.
|
|
|
Записан
|
Всего лишь неделя кодирования с последующей неделей отладки могут сэкономить целый час, потраченный на планирование программы. - Дж. Коплин.
Ходить по воде и разрабатывать программное обеспечение по спецификациям очень просто, когда и то, и другое заморожено. - Edward V. Berard
Любые проблемы в информатике решаются добавлением еще одного уровня косвенности – кроме, разумеется, проблемы переизбытка уровней косвенности. — Дэвид Уилер.
|
|
|
|