Фильтры исключений в CLR
Привет, хабралюди. Сегодня мы рассмотрим один из механизмов CLR, который напрямую недоступен для разработчиков на языке C# — фильтры исключений.
Опрос среди моих знакомых программистов на C# показал, что они (само собой) никогда этим механизмом не пользовались и даже не знают о его существовании. Поэтому предлагаю всем любознательным ознакомиться с текстом статьи.
Итак, фильтры исключений — это механизм, который позволяет блоку catch декларировать предусловия, которым должно удовлетворять исключение, дабы быть пойманным данным блоком. Этот механизм работает не совсем так же, как выполнение проверок внутри блока catch .
Под катом — код на VB.NET, F#, CIL и C#, а также проверка различных декомпиляторов на обработку механизма фильтров.
Откуда есть пошли фильтры исключений
Фильтры исключений встроены в среду CLR и являются одним из механизмов, который среда использует при обработке исключения. Последовательность выглядит следующим образом:
На этапе поиска подходящего блока catch CLR выполняет обход своего внутреннего стека обработчиков исключений, а также выполняет фильтры исключений. Обратите внимание — это происходит до выполнения кода в блоке finally . Мы обсудим этот момент позже.
Как это выглядит на VB.NET
Фильтры исключений нативно поддерживаются языком VB.NET. Вот пример того, как выглядит код, использующий фильтры:
При выполнении данного кода будет выдана следующая цепочка сообщений:
Как это выглядит в F#
При подготовке статьи я нашёл в интернете информацию о том, что F# поддерживает фильтры исключений. Что ж, проверим это. Вот пример кода:
Этот код компилируется без фильтров, с обычным catch [mscorlib]System.Object . Мне так и не удалось заставить компилятор F# сделать фильтр исключений. Если вам известны альтернативные способы это сделать — добро пожаловать в комментарии.
Как это выглядит в CIL
CIL (Common Intermediate Language) — это аналог низкоуровневого языка ассемблера для .NET-машины. Скомпилированные сборки можно дизассемблировать в этот язык с помощью инструмента ildasm , и собирать обратно с помощью ilasm , которые поставляются вместе с .NET.
Приведу фрагмент кода на VB.NET, каким я его увидел в ildasm :
Как видно, компилятор VB.NET, конечно, сильно расписал наш код в виде CIL. Больше всего нас интересует блок filter :
Итак, компилятор вынес в блок фильтра проверку типа исключения, а также вызов нашей функции. Если в конце выполнения блока фильтра на стеке лежит значение 1 , то соответствующий этому фильтру блок catch будет выполнен; иначе — нет.
Стоит отметить, что компилятор C# проверки типов не выносит в блок filter , а использует специальную CIL-конструкцию catch с указанием типа. То есть, компилятор C# не использует механизм filter вообще.
Кстати говоря, для генерации этого блока можно использовать метод ILGenerator.BeginExceptFilterBlock (если вы пишете свой компилятор).
Как это выглядит в декомпиляторе
В этом разделе я попробую декомпилировать полученный код с помощью нескольких известных инструментов и посмотреть, что из этого получится.
Последний JetBrains dotPeek 1.1 при попытке декомпиляции сборки с фильтром радостно сообщил следующее:
.NET Reflector 8.2 поступил более адекватно и что-то смог декомпилировать в C#:
Что ж, неплохо — хотя код и некомпилируемый, но по нему хотя бы видно наличие фильтра. То, что фильтр не был расшифрован, можно списать на недостатки C#-транслятора. Попробуем то же самое с транслятором в VB.NET:
Увы, попытка точно так же провалилась — декомпилятор почему-то не смог определить имя фильтрующей функции (хотя, как мы видели выше, ildasm с этим прекрасно справился).
Могу только предположить, что рассмотренные инструменты пока плохо работают с кодом фильтров .NET4.5.
Чем это отличается от проверок в теле блока catch
Рассмотрим фрагмент кода, почти аналогичный коду на VB.NET:
А теперь попробуем найти разницу в поведении между примерами на C# и VB.NET. Всё достаточно просто: выражение throw; в C# теряет номер строки в стеке. Если изменить фильтр так, чтобы он возвращал false , то приложение упадёт с сообщением
Судя по стеку, исключение было сгенерировано на 25 строке (строка throw; ), а не на строке 19 ( throw exception; ). Код на VB.NET в таких же условиях показывает изначальное место выпадения исключения.
UPD. Изначально я ошибочно написал, что throw; теряет весь стек, но в комментариях подсказали, что это действительно совсем не так. Происходит лишь незначительная модификация номера строки в стеке. Причём на mono это не воспроизводится — стек исключения там не меняется после throw; (спасибо kekekeks за эти подробности).
О безопасности
Эрик Липперт в своём блоге рассматривает ситуацию, когда фильтры исключений позволяют вредоносной стороне выполнить свой код с повышенными привилегиями в некоторых случаях.
Коротко: если вы выполняете временное повышение привилегий для какого-то внешнего и потенциально разрушительного кода, то нельзя полагаться на finally , т.к. перед выполненнием блока finally могут быть вызваны фильтры исключений, расположенные выше по стеку вызовов (а злоумышленник может вытворять в коде этих фильтров всё, что ему вздумается). Помните — поиск подходящего блока catch всегда выполняется до выполнения блока finally .
С чего начинается фильтр исключений в конструкции
Представляет само необработанное исключение
Значение, показывающее, считается ли исключение обработанным. Если мы на фильтре помечаем его значение в true, то исключение считается обработанным
Результат метода действия, к которому применяется фильтр исключения
С помощью свойства Exception мы можем получить доступ к выбрасываемому исключению.
Установив свойство ExceptionHandled в true, фильтр тем самым помечает исключение как обработанное.
С помощью свойства Result фильтр управляет результатом действий. Общераспространенной практикой в данном случае является перенаправление пользователя на страницу ошибки или отображение ошибки на экране.
Теперь создадим простенький фильтр, который будет обрабатывать исключение IndexOutOfRangeException :
Здесь в методе OnException первым делом мы проверяем, не установлено ли значение свойства ExceptionHandled . Если оно установлено в true, следовательно, какой-то другой фильтр исключений уже обработал данное исключение. Также проверяется тип исключения. Поскольку мы ловим в данном случае только исключения типа IndexOutOfRangeException , следовательно, нас только они интересуют.
Далее мы устанавливаем результат метода, к которому применен фильтр. Предполагается, что в проекте в каталоге Content у нас находится некоторая страница ExceptionFound.html, которая отображает пользователю сообщение об ошибке.
В данном случае важно пометить исключение как обработанное: exceptionContext.ExceptionHandled = true . Иначе, если мы этого не сделаем, то мы можем увидеть в браузере все диагностическое сообщение об ошибке, которое обычно посылает фреймворк в ответ клиенту.
В данном случае метод Index выбросит необработанное исключение, и оно будет объектом типа IndexOutOfRangeException , а пользователь будет перенаправлен на страницу ExceptionFound.htm .
Подобным образом мы можем обработать и другие типы исключений.
HandleErrorAttribute. Встроенная обработка исключений.
Создавать свои фильтры исключений необязательно, так как во фреймворке имеется встроенная реализация интерфейса IExceptionFilter — атрибут HandleErrorAttribute. Он имеет ряд свойств, с помощью которых мы можем произвести гибкую настройку фильтра:
Представляет тип обрабатываемого исключения. По умолчанию используется System.Exception
Имя представления, которое рендерится данным фильтром. Если значение не задано, то по умолчанию используются следующие пути: /Views/Имя_контроллера/Error.cshtml или /Views/Shared/Error.cshtml
Имя используемой мастер-страницы
При обработке исключения фильтр исключений посылает статусный код HTTP 500 и генерирует указанное в свойстве View представление. Например, используем предыдущий пример с фильтром исключений, применив встроенную реализацию:
Фильтры исключений в C#
В прошлой статье мы рассмотрели вопросы перехвата и обработки исключений в C#. Вместе с этим обработка исключений в C# может быть ещё более гибкой, чем рассмотренная ранее. Так, забегая немного вперед, скажу, что в C# при обработке исключений также возможно использование фильтров. И сегодня мы рассмотрим один из примеров их использования.
Фильтры исключений в C#
Фильтры исключений позволяют обрабатывать или не обрабатывать тот или иной тип исключений в зависимости от определенных условий. Например, возьмем код перехвата и обработки исключений из предыдущей статьи:
Здесь у нас в catch обрабатываются исключения типа OverflowException и DivideByZeroException , а попытка ввести не целое число, а, например, строку отсекается путем использования обычного условного оператора if . Применим фильтр исключений и перепишем блоки с DivideByZeroException следующим образом:
здесь мы в первом блоке catch применили фильтр, используя ключевое слово when . После when в круглых скобках указано логическое выражение. Если логическое выражение равно true , то блок catch обрабатывается, иначе — ищется следующий блок catch в котором можно обработать исключение.
Строго говоря, если нас не интересует какая-либо дополнительная информация об исключении, то имя переменной с которой будет сопоставлен тип исключения можно опускать и сделать обработку, например, так:
Итого
Фильтры исключений позволяют сделать обработку исключений в C# более гибкой. Используя ключевое слово when можно определить логическое выражение истинный результат которого ( true ) позволяет зайти внутрь блока catch и обработать, например, определенный тип исключения.
Name already in use
PiRIS / articles / 5_3_1_6_exceptions.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Обработка исключительных ситуаций. Методы и способы идентификации сбоев и ошибок.
Иногда при выполнении программы возникают ошибки, которые трудно предусмотреть или предвидеть, а иногда и вовсе невозможно. Например, при передачи файла по сети может неожиданно оборваться сетевое подключение. такие ситуации называются исключениями. Язык C# предоставляет разработчикам возможности для обработки таких ситуаций. Для этого в C# предназначена конструкция try. catch. finally.
При использовании блока try. catch..finally вначале пытаются выполниться инструкции в блоке try. Если в этом блоке не возникло исключений, то после его выполнения начинает выполняться блок finally. И затем конструкция try..catch..finally завершает свою работу.
Если же в блоке try вдруг возникает исключение, то обычный порядок выполнения останавливается, и среда CLR (Common Language Runtime) начинает искать блок catch, который может обработать данное исключение. Если нужный блок catch найден, то он выполняется, и после его завершения выполняется блок finally.
Если нужный блок catch не найден, то при возникновении исключения программа аварийно завершает свое выполнение.
Рассмотрим следующий пример:
В данном случае происходит деление числа на 0, что приведет к генерации исключения. И при запуске приложения в режиме отладки мы увидим в Visual Studio окошко, которое информирует об исключении:
В этом окошке мы видим, что возникло исключение, которое представляет тип System.DivideByZeroException, то есть попытка деления на ноль. С помощью пункта View Details можно посмотреть более детальную информацию об исключении.
И в этом случае единственное, что нам остается, это завершить выполнение программы.
Чтобы избежать подобного аварийного завершения программы, следует использовать для обработки исключений конструкцию try. catch. finally. Так, перепишем пример следующим образом:
В данном случае у нас опять же возникнет исключение в блоке try, так как мы пытаемся разделить на ноль. И дойдя до строки
выполнение программы остановится. CLR найдет блок catch и передаст управление этому блоку.
После блока catch будет выполняться блок finally.
Таким образом, программа по-прежнему не будет выполнять деление на ноль и соответственно не будет выводить результат этого деления, но теперь она не будет аварийно завершаться, а исключение будет обрабатываться в блоке catch.
Следует отметить, что в этой конструкции обязателен блок try. При наличии блока catch мы можем опустить блок finally:
И, наоборот, при наличии блока finally мы можем опустить блок catch и не обрабатывать исключение:
Однако, хотя с точки зрения синтаксиса C# такая конструкция вполне корректна, тем не менее, поскольку CLR не сможет найти нужный блок catch, то исключение не будет обработано, и программа аварийно завершится.
Обработка исключений и условные конструкции
Ряд исключительных ситуаций может быть предвиден разработчиком. Например, пусть программа предусматривает ввод числа и вывод его квадрата:
Если пользователь введет не число, а строку, какие-то другие символы, то программа выпадет в ошибку. С одной стороны, здесь как раз та ситуация, когда можно применить блок try..catch, чтобы обработать возможную ошибку. Однако гораздо оптимальнее было бы проверить допустимость преобразования:
Метод Int32.TryParse() возвращает true, если преобразование можно осуществить, и false — если нельзя. При допустимости преобразования переменная x будет содержать введенное число. Так, не используя try. catch можно обработать возможную исключительную ситуацию.
С точки зрения производительности использование блоков try..catch более накладно, чем применение условных конструкций. Поэтому по возможности вместо try..catch лучше использовать условные конструкции на проверку исключительных ситуаций.
Блок catch и фильтры исключений
Определение блока catch
За обработку исключения отвечает блок catch, который может иметь следующие формы:
Обрабатывает любое исключение, которое возникло в блоке try. Выше уже был продемонстрирован пример подобного блока.
Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch.
Например, обработаем только исключения типа DivideByZeroException:
Однако если в блоке try возникнут исключения каких-то других типов, отличных от DivideByZeroException, то они не будут обработаны.
Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch. А вся информация об исключении помещается в переменную данного типа.
Фактически этот случай аналогичен предыдущему за тем исключением, что здесь используется переменная. В данном случае в переменную ex, которая представляет тип DivideByZeroException, помещается информация о возникшем исключени. И с помощью свойства Message мы можем получить сообщение об ошибке.
Если нам не нужна информация об исключении, то переменную можно не использовать как в предыдущем случае.
Фильтры исключений позволяют обрабатывать исключения в зависимости от определенных условий. Для их применения после выражения catch идет выражение when, после которого в скобках указывается условие:
В этом случае обработка исключения в блоке catch производится только в том случае, если условие в выражении when истинно. Например:
В данном случае будет выброшено исключение, так как y=0. Здесь два блока catch, и оба они обрабатывают исключения типа DivideByZeroException, то есть по сути все исключения, генерируемые при делении на ноль. Но поскольку для первого блока указано условие y == 0 && x == 0, то оно не будет обрабатывать исключение — условие, указанное после оператора when возвращает false. Поэтому CLR будет дальше искать соответствующие блоки catch далее и для обработки исключения выберет второй блок catch. В итоге если мы уберем второй блок catch, то исключение вобще не будет обрабатываться.
Типы исключений. Класс Exception
Базовым для всех типов исключений является тип Exception. Этот тип определяет ряд свойств, с помощью которых можно получить информацию об исключении.
InnerException: хранит информацию об исключении, которое послужило причиной текущего исключения
Message: хранит сообщение об исключении, текст ошибки
Source: хранит имя объекта или сборки, которое вызвало исключение
StackTrace: возвращает строковое представление стека вызывов, которые привели к возникновению исключения
TargetSite: возвращает метод, в котором и было вызвано исключение
Например, обработаем исключения типа Exception:
Однако так как тип Exception является базовым типом для всех исключений, то выражение catch (Exception ex) будет обрабатывать все исключения, которые могут возникнуть.
Но также есть более специализированные типы исключений, которые предназначены для обработки каких-то определенных видов исключений. Их довольно много, я приведу лишь некоторые:
DivideByZeroException: представляет исключение, которое генерируется при делении на ноль
ArgumentOutOfRangeException: генерируется, если значение аргумента находится вне диапазона допустимых значений
ArgumentException: генерируется, если в метод для параметра передается некорректное значение
IndexOutOfRangeException: генерируется, если индекс элемента массива или коллекции находится вне диапазона допустимых значений
InvalidCastException: генерируется при попытке произвести недопустимые преобразования типов
NullReferenceException: генерируется при попытке обращения к объекту, который равен null (то есть по сути неопределен)
И при необходимости мы можем разграничить обработку различных типов исключений, включив дополнительные блоки catch:
В данном случае блоки catch обрабатывают исключения типов IndexOutOfRangeException, DivideByZeroException и Exception. Когда в блоке try возникнет исключение, то CLR будет искать нужный блок catch для обработки исключения. Так, в данном случае на строке
происходит обращение к 7-му элементу массива. Однако поскольку в массиве только 4 элемента, то мы получим исключение типа IndexOutOfRangeException. CLR найдет блок catch, который обрабатывает данное исключение, и передаст ему управление.
Следует отметить, что в данном случае в блоке try есть ситуация для генерации второго исключения — деление на ноль. Однако поскольку после генерации IndexOutOfRangeException управление переходит в соответствующий блок catch, то деление на ноль int y = x / 0 в принципе не будет выполняться, поэтому исключение типа DivideByZeroException никогда не будет сгенерировано.
Однако рассмотрим другую ситуацию:
В данном случае в блоке try генерируется исключение типа InvalidCastException, однако соответствующего блока catch для обработки данного исключения нет. Поэтому программа аварийно завершит свое выполнение.
Мы также можем определить для InvalidCastException свой блок catch, однако суть в том, что теоретически в коде могут быть сгенерированы сами различные типы исключений. А определять для всех типов исключений блоки catch, если обработка исключений однотипна, не имеет смысла. И в этом случае мы можем определить блок catch для базового типа Exception:
И в данном случае блок catch (Exception ex)<> будет обрабатывать все исключения кроме DivideByZeroException и IndexOutOfRangeException. При этом блоки catch для более общих, более базовых исключений следует помещать в конце — после блоков catch для более конкретный, специализированных типов. Так как CLR выбирает для обработки исключения первый блок catch, который соответствует типу сгенерированного исключения. Поэтому в данном случае сначала обрабатывается исключение DivideByZeroException и IndexOutOfRangeException, и только потом Exception (так как DivideByZeroException и IndexOutOfRangeException наследуется от класса Exception).
Создание классов исключений
Если нас не устраивают встроенные типы исключений, то мы можем создать свои типы. Базовым классом для всех исключений является класс Exception, соответственно для создания своих типов мы можем унаследовать данный класс.
Допустим, у нас в программе будет ограничение по возрасту:
В классе Person при установке возраста происходит проверка, и если возраст меньше 18, то выбрасывается исключение. Класс Exception принимает в конструкторе в качестве параметра строку, которое затем передается в его свойство Message.
Но иногда удобнее использовать свои классы исключений. Например, в какой-то ситуации мы хотим обработать определенным образом только те исключения, которые относятся к классу Person. Для этих целей мы можем сделать специальный класс PersonException:
По сути класс кроме пустого конструктора ничего не имеет, и то в конструкторе мы просто обращаемся к конструктору базового класса Exception, передавая в него строку message. Но теперь мы можем изменить класс Person, чтобы он выбрасывал исключение именно этого типа и соответственно в основной программе обрабатывать это исключение:
Однако необязательно наследовать свой класс исключений именно от типа Exception, можно взять какой-нибудь другой производный тип. Например, в данном случае мы можем взять тип ArgumentException, который представляет исключение, генерируемое в результате передачи аргументу метода некорректного значения:
Каждый тип исключений может определять какие-то свои свойства. Например, в данном случае мы можем определить в классе свойство для хранения устанавливаемого значения:
В конструкторе класса мы устанавливаем это свойство и при обработке исключения мы его можем получить:
Поиск блока catch при обработке исключений
Если код, который вызывает исключение, не размещен в блоке try или помещен в конструкцию try..catch, которая не содержит соответствующего блока catch для обработки возникшего исключения, то система производит поиск соответствующего обработчика исключения в стеке вызовов.
Например, рассмотрим следующую программу:
В данном случае стек вызовов выглядит следующим образом: метод Main вызывает метод Method1, который, в свою очередь, вызывает метод Method2. И в методе Method2 генерируется исключение DivideByZeroException. Визуально стек вызовов можно представить следующим образом:
Внизу стека метод Main, с которого началось выполнение, и на самом верху метод Method2.
Что будет происходить в данном случае при генерации исключения?
Метод Main вызывает метод Method1, а тот вызывает метод Method2, в котором генерируется исключение DivideByZeroException.
Система видит, что код, который вызывал исключение, помещен в конструкцию try..catch
Система ищет в этой конструкции блок catch, который обрабатывает исключение DivideByZeroException. Однако такого блока catch нет.
Система опускается в стеке вызовов в метод Method1, который вызывал Method2. Здесь вызов Method2 помещен в конструкцию try..catch
Система также ищет в этой конструкции блок catch, который обрабатывает исключение DivideByZeroException. Однако здесь также подобный блок catch отсутствует.
Система далее опускается в стеке вызовов в метод Main, который вызывал Method1. Здесь вызов Method1 помещен в конструкцию try..catch
Система снова ищет в этой конструкции блок catch, который обрабатывает исключение DivideByZeroException. И в данном случае ткой блок найден.
Система наконец нашла нужный блок catch в методе Main, для обработки исключения, которое возникло в методе Method2 — то есть к начальному методу, где непосредственно возникло исключение. Но пока данный блок catch НЕ выполняется. Система поднимается обратно по стеку вызовов в самый верх в метод Method2 и выполняет в нем блок finally:
Далее система возвращается по стеку вызовов вниз в метод Method1 и выполняет в нем блок finally:
Затем система переходит по стеку вызовов вниз в метод Main и выполняет в нем найденный блок catch и последующий блок finally:
Далее выполняется код, который идет в методе Main после конструкции try..catch:
Стоит отметить, что код, который идет после конструкции try. catch в методах Method1 и Method2, не выполняется, потому что обработчик исключения найден именно в методе Main.
Консольный вывод программы:
Генерация исключения и оператор throw
Обычно система сама генерирует исключения при определенных ситуациях, например, при делении числа на ноль. Но язык C# также позволяет генерировать исключения вручную с помощью оператора throw. То есть с помощью этого оператора мы сами можем создать исключение и вызвать его в процессе выполнения.
Например, в нашей программе происходит ввод строки, и мы хотим, чтобы, если длина строки будет больше 6 символов, возникало исключение:
После оператора throw указывается объект исключения, через конструктор которого мы можем передать сообщение об ошибке. Естественно вместо типа Exception мы можем использовать объект любого другого типа исключений.
Затем в блоке catch сгенерированное нами исключение будет обработано.
Подобным образом мы можем генерировать исключения в любом месте программы. Но существует также и другая форма использования оператора throw, когда после данного оператора не указывается объект исключения. В подобном виде оператор throw может использоваться только в блоке catch:
В данном случае при вводе строки с длиной больше 6 символов возникнет исключение, которое будет обработано внутренним блоком catch. Однако поскольку в этом блоке используется оператор throw, то исключение будет передано дальше внешнему блоку catch.
Методы поиска ошибок в программах
Международный стандарт ANSI/IEEE-729-83 разделяет все ошибки в разработке программ на следующие типы.
Ошибка (error) — состояние программы, при котором выдаются неправильные результаты, причиной которых являются изъяны (flaw) в операторах программы или в технологическом процессе ее разработки, что приводит к неправильной интерпретации исходной информации, следовательно, и к неверному решению.
Дефект (fault) в программе — следствие ошибок разработчика на любом из этапов разработки, которая может содержаться в исходных или проектных спецификациях, текстах кодов программ, эксплуатационной документация и т.п. В процессе выполнения программы может быть обнаружен дефект или сбой.
Отказ (failure) — это отклонение программы от функционирования или невозможность программы выполнять функции, определенные требованиями и ограничениями, что рассматривается как событие, способствующее переходу программы в неработоспособное состояние из-за ошибок, скрытых в ней дефектов или сбоев в среде функционирования [7.6, 7.11]. Отказ может быть результатом следующих причин:
- ошибочная спецификация или пропущенное требование, означающее, что спецификация точно не отражает того, что предполагал пользователь;
- спецификация может содержать требование, которое невозможно выполнить на данной аппаратуре и программном обеспечении;
- проект программы может содержать ошибки (например, база данных спроектирована без средств защиты от несанкционированного доступа пользователя, а требуется защита);
- программа может быть неправильной, т.е. она выполняет несвойственный алгоритм или он реализован не полностью.
Таким образом, отказы, как правило, являются результатами одной или более ошибок в программе, а также наличия разного рода дефектов.
Ошибки на этапах процесса тестирования. Приведенные типы ошибок распределяются по этапам ЖЦ и им соответствуют такие источники их возникновения:
- непреднамеренное отклонение разработчиков от рабочих стандартов или планов реализации;
- спецификации функциональных и интерфейсных требований выполнены без соблюдения стандартов разработки, что приводит к нарушению функционирования программ;
- организации процесса разработки — несовершенная или недостаточное управление руководителем проекта ресурсами (человеческими, техническими, программными и т.д.) и вопросами тестирования и интеграции элементов проекта.
Рассмотрим процесс тестирования, исходя из рекомендаций стандарта ISO/IEC 12207, и приведем типы ошибок, которые обнаруживаются на каждом процессе ЖЦ.
Процесс разработки требований. При определении исходной концепции системы и исходных требований к системе возникают ошибки аналитиков при спецификации верхнего уровня системы и построении концептуальной модели предметной области.
Характерными ошибками этого процесса являются:
- неадекватность спецификации требований конечным пользователям;
- некорректность спецификации взаимодействия ПО со средой функционирования или с пользователями;
- несоответствие требований заказчика к отдельным и общим свойствам ПО;
- некорректность описания функциональных характеристик;
- необеспеченность инструментальными средствами всех аспектов реализации требований заказчика и др.
Процесс проектирования. Ошибки при проектировании компонентов могут возникать при описании алгоритмов, логики управления, структур данных, интерфейсов, логики моделирования потоков данных, форматов ввода-вывода и др. В основе этих ошибок лежат дефекты спецификаций аналитиков и недоработки проектировщиков. К ним относятся ошибки, связанные:
- с определением интерфейса пользователя со средой;
- с описанием функций (неадекватность целей и задач компонентов, которые обнаруживаются при проверке комплекса компонентов);
- с определением процесса обработки информации и взаимодействия между процессами (результат некорректного определения взаимосвязей компонентов и процессов);
- с некорректным заданием данных и их структур при описании отдельных компонентов и ПС в целом;
- с некорректным описанием алгоритмов модулей;
- с определением условий возникновения возможных ошибок в программе;
- с нарушением принятых для проекта стандартов и технологий.
Этап кодирования. На данном этапе возникают ошибки, которые являются результатом дефектов проектирования, ошибок программистов и менеджеров в процессе разработки и отладки системы. Причиной ошибок являются:
- бесконтрольность значений входных параметров, индексов массивов, параметров циклов, выходных результатов, деления на 0 и др.;
- неправильная обработка нерегулярных ситуаций при анализе кодов возврата от вызываемых подпрограмм, функций и др.;
- нарушение стандартов кодирования (плохие комментарии, нерациональное выделение модулей и компонент и др.);
- использование одного имени для обозначения разных объектов или разных имен одного объекта, плохая мнемоника имен;
- несогласованное внесение изменений в программу разными разработчиками и др.
Процесс тестирования. На этом процессе ошибки допускаются программистами и тестировщиками при выполнении технологии сборки и тестирования, выбора тестовых наборов и сценариев тестирования и др. Отказы в программном обеспечении, вызванные такого рода ошибками, должны выявляться, устраняться и не отражаться на статистике ошибок компонент и программного обеспечения в целом.
Процесс сопровождения. На процессе сопровождения обнаруживаются ошибки, причиной которых являются недоработки и дефекты эксплуатационной документации, недостаточные показатели модифицируемости и удобочитаемости, а также некомпетентность лиц, ответственных за сопровождение и/или усовершенствование ПО. В зависимости от сущности вносимых изменений на этом этапе могут возникать практически любые ошибки, аналогичные ранее перечисленным ошибкам на предыдущих этапах.
Все ошибки, которые возникают в программах, принято подразделять на следующие классы:
- логические и функциональные ошибки;
- ошибки вычислений и времени выполнения;
- ошибки вводавывода и манипулирования данными;
- ошибки интерфейсов;
- ошибки объема данных и др.
Логические ошибки являются причиной нарушения логики алгоритма, внутренней несогласованности переменных и операторов, а также правил программирования. Функциональные ошибки — следствие неправильно определенных функций, нарушения порядка их применения или отсутствия полноты их реализации и т.д.
Ошибки вычислений возникают по причине неточности исходных данных и реализованных формул, погрешностей методов, неправильного применения операций вычислений или операндов. Ошибки времени выполнения связаны с необеспечением требуемой скорости обработки запросов или времени восстановления программы.
Ошибки ввода-вывода и манипулирования данными являются следствием некачественной подготовки данных для выполнения программы, сбоев при занесении их в базы данных или при выборке из нее.
Ошибки интерфейса относятся к ошибкам взаимосвязи отдельных элементов друг с другом, что проявляется при передаче данных между ними, а также при взаимодействии со средой функционирования.
Ошибки объема относятся к данным и являются следствием того, что реализованные методы доступа и размеры баз данных не удовлетворяют реальным объемам информации системы или интенсивности их обработки.
Приведенные основные классы ошибок свойственны разным типам компонентов ПО и проявляются они в программах по разному. Так, при работе с БД возникают ошибки представления и манипулирования данными, логические ошибки в задании прикладных процедур обработки данных и др. В программах вычислительного характера преобладают ошибки вычислений, а в программах управления и обработки — логические и функциональные ошибки. В ПО, которое состоит из множества разноплановых программ, реализующих разные функции, могут содержаться ошибки разных типов. Ошибки интерфейсов и нарушение объема характерны для любого типа систем.
Анализ типов ошибок в программах является необходимым условием создания планов тестирования и методов тестирования для обеспечения правильности ПО.
На современном этапе развития средств поддержки разработки ПО (CASE-технологии, объектно-ориентированные методы и средства проектирования моделей и программ) проводится такое проектирование, при котором ПО защищается от наиболее типичных ошибок и тем самым предотвращается появление программных дефектов.
Связь ошибки с отказом. Наличие ошибки в программе, как правило, приводит к отказу ПО при его функционировании. Для анализа причинно-следственных связей «ошибкаотказ» выполняются следующие действия:
- идентификация изъянов в технологиях проектирования и программирования;
- взаимосвязь изъянов процесса проектирования и допускаемых человеком ошибок;
- классификация отказов, изъянов и возможных ошибок, а также дефектов на каждом этапе разработки;
- сопоставление ошибок человека, допускаемых на определенном процессе разработки, и дефектов в объекте, как следствий ошибок спецификации проекта, моделей программ;
- проверка и защита от ошибок на всех этапах ЖЦ, а также обнаружение дефектов на каждом этапе разработки;
- сопоставление дефектов и отказов в ПО для разработки системы взаимосвязей и методики локализации, сбора и анализа информации об отказах и дефектах;
- разработка подходов к процессам документирования и испытания ПО.
Конечная цель причинно-следственных связей «ошибка-отказ» заключается в определении методов и средств тестирования и обнаружения ошибок определенных классов, а также критериев завершения тестирования на множестве наборов данных; в определении путей совершенствования организации процесса разработки, тестирования и сопровождения ПО.
Приведем следующую классификацию типов отказов:
- аппаратный, при котором общесистемное ПО не работоспособно;
- информационный, вызванный ошибками во входных данных и передаче данных по каналам связи, а также при сбое устройств ввода (следствие аппаратных отказов);
- эргономический, вызванный ошибками оператора при его взаимодействии с машиной (этот отказ — вторичный отказ, может привести к информационному или функциональному отказам);
- программный, при наличии ошибок в компонентах и др.
Некоторые ошибки могут быть следствием недоработок при определении требований, проекта, генерации выходного кода или документации. С другой стороны, они порождаются в процессе разработки программы или при разработке интерфейсов отдельных элементов программы (нарушение порядка параметров, меньше или больше параметров и т.п.).
Источники ошибок. Ошибки могут быть порождены в процессе разработки проекта, компонентов, кода и документации. Как правило, они обнаруживаются при выполнении или сопровождении программного обеспечения в самых неожиданных и разных ее точках.
Некоторые ошибки в программе могут быть следствием недоработок при определении требований, проекта, генерации кода или документации. С другой стороны, ошибки порождаются в процессе разработки программы или интерфейсов ее элементов (например, при нарушении порядка задания параметров связи — меньше или больше, чем требуется и т.п.).
Причиной появления ошибок — непонимание требований заказчика; неточная спецификация требований в документах проекта и др. Это приводит к тому, что реализуются некоторые функции системы, которые будут работать не так, как предлагает заказчик. В связи с этим проводится совместное обсуждение заказчиком и разработчиком некоторых деталей требований для их уточнения.
Команда разработчиков системы может также изменить синтаксис и семантику описания системы. Однако некоторые ошибки могут быть не обнаружены (например, неправильно заданы индексы или значения переменных этих операторов).