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

Отладчик предоставляет средства наблюдения, но не формулирует вопрос за программиста. Точка останова задаёт границу исследования, Step Into, Step Over и Step Out управляют глубиной прохода, Locals показывает локальное состояние текущего кадра, Watch удерживает выбранные выражения, а Call Stack связывает текущий вызов с его предшественниками. Каждое наблюдение сопоставляется с заранее записанным ожидаемым состоянием, инвариантом, предусловием либо постусловием.

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

Опора — Раздел 23: систематическая отладка. Дополнительно используются готовые программы, контракты, инварианты и алгоритмы Разделов 1–22.

Каждая задача выполняется в отдельной копии готового консольного проекта курса с <Nullable>enable</Nullable>. До первого воспроизведения запрещено редактировать код, менять фиксированный вход или добавлять Console.WriteLine в исследуемые вычисляющие методы. Для каждой задачи оформляется карточка: симптом → фиксированный вход → ожидаемый результат → фактический результат → проверяемые гипотезы → последнее правильное состояние → первое неверное состояние → инструкция перехода → механизм причины → минимальное исправление → регрессионные проверки. Если фактический результат заранее не указан, его записывают при первом неизменённом запуске. Обработка исключений, файлы, библиотеки модульного тестирования, логгеры, профилировщики и конструкции последующих разделов запрещены. Аварийно завершающиеся программы исследуются в отдельных запусках без перехвата исключения. Диагностический вывод разрешён только после локализации причины и не заменяет работу отладчиком.


База практических заданий

Уровень I. Базовый

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

1. Неверное начальное состояние накопителя: Исследуется метод:

static long Sum(int[] data)
{
    long sum = 1;

    for (int i = 0; i < data.Length; i++)
    {
        sum += data[i];
    }

    return sum;
}

Для массива [4, -2, 7] ожидается 9, фактически возвращается 10. Поставить точку останова на заголовке цикла, открыть Locals, добавить в Watch выражения sum, i и data.Length. До первой итерации сравнить состояние с инвариантом суммы пустого префикса, затем выполнить одну итерацию через Step Over. Зафиксировать, что первое расхождение существует до входа в цикл. Исправление проверить на пустом массиве, [5] и исходном наборе.

2. Граница ветви: Исследуется метод:

static string Grade(int score)
{
    if (score > 90)
    {
        return "A";
    }

    if (score >= 75)
    {
        return "B";
    }

    return "C";
}

Для score = 90 ожидается "A", фактически возвращается "B". Поставить точку останова на первой проверке, добавить в Watch score > 90 и score >= 90, выполнить ветвление через Step Over и записать выбранный путь. Первое неверное решение принимается на первой проверке. Исправление подтвердить значениями 74, 75, 89, 90, 91 и 100.

3. Аргументы в неверных позициях: Исследуется вызов:

double result =
    WeightedMean(10.0, 1.0, 20.0, 3.0);

метода:

static double WeightedMean(
    double x,
    double y,
    double weightX,
    double weightY)
{
    return
        (x * weightX + y * weightY) /
        (weightX + weightY);
}

По замыслу значения равны 10 и 20, веса — 1 и 3, поэтому ожидается 17.5. Поставить точку останова на вызове, выполнить Step Into и записать параметры при входе в метод. Первое неверное состояние возникает до выполнения тела. Исправить только порядок аргументов, затем повторить проверку с именованными аргументами и равными весами.

4. Пропущенный последний элемент: Исследуется метод:

static long Sum(int[] data)
{
    long sum = 0;

    for (int i = 0; i < data.Length - 1; i++)
    {
        sum += data[i];
    }

    return sum;
}

Для [5, 8, 13] ожидается 26, фактически получается 13. Поставить точку останова внутри цикла и вторую — на return. Наблюдать i, sum, data.Length - 1 и условие i < data.Length. Зафиксировать состояние, в котором цикл завершается при ещё не обработанном допустимом индексе. Проверить исправление на пустой, одноэлементной и трёхэлементной последовательностях.

5. Потеря данных при сдвиге: Исследуется поворот массива вправо:

static void RotateRightOne(int[] data)
{
    if (data.Length < 2)
    {
        return;
    }

    int saved = data[data.Length - 1];

    for (int i = 0; i < data.Length - 1; i++)
    {
        data[i + 1] = data[i];
    }

    data[0] = saved;
}

Для [1, 2, 3, 4] ожидается [4, 1, 2, 3], фактически получается [4, 1, 1, 1]. Остановиться на присваивании внутри цикла, добавить весь массив и i в Watch, после каждого Step Over записывать содержимое. Найти первую инструкцию, после которой ещё не перенесённое значение становится недоступным. Исправление должно изменить направление обхода, а не вводить дополнительный массив.

6. Рекурсивный переход и стек: Исследуется метод:

static int SumTo(int n)
{
    if (n <= 0)
    {
        return 0;
    }

    return n + SumTo(n - 2);
}

Для n = 4 ожидается 10, фактически возвращается 6. Поставить точку останова на входе в метод, последовательно выполнять Step Into до базового случая и записать значения n во всех кадрах через Call Stack. Затем применять Step Out и проследить обратное вычисление. Указать первый неверный рекурсивный аргумент и подтвердить исправление для n = 0, 1 и 4.

7. Несогласованные границы бинарного поиска: Исследуется поиск на полуинтервале:

static int BinarySearch(
    int[] data,
    int target)
{
    int lo = 0;
    int hi = data.Length;

    while (lo < hi)
    {
        int mid = lo + (hi - lo) / 2;

        if (data[mid] < target)
        {
            lo = mid + 1;
        }
        else
        {
            hi = mid - 1;
        }
    }

    return
        lo < data.Length &&
        data[lo] == target
            ? lo
            : -1;
}

Для [1, 3, 5, 7, 9] и цели 7 ожидается индекс 3, фактически возвращается -1. Поставить точку останова в начале цикла, наблюдать lo, mid, hi, data[mid] и утверждение «если цель существует, её индекс принадлежит [lo, hi)». Найти обновление, исключающее ещё возможную позицию, и восстановить согласованную полуоткрытую модель.

8. Копия, оказавшаяся псевдонимом: Исследуется фрагмент:

int[] source = { 3, 1, 2 };
int[] snapshot = source;

Array.Sort(snapshot);

По условию snapshot должен быть отсортированной независимой копией, а source — остаться [3, 1, 2]; фактически меняются оба имени. Остановиться после присваивания и перед Array.Sort, проверить ReferenceEquals(source, snapshot), затем выполнить сортировку через Step Over. Первое неверное отношение между переменными возникает до изменения элементов. Исправление выполнить поэлементным копированием и подтвердить независимость после сортировки.

9. Граница неправильного измерения: Исследуется сумма таблицы:

static long Sum(int[,] data)
{
    long sum = 0;

    for (int row = 0;
         row < data.GetLength(0);
         row++)
    {
        for (int column = 0;
             column < data.GetLength(0);
             column++)
        {
            sum += data[row, column];
        }
    }

    return sum;
}

Для таблицы [[1,2,3],[4,5,6]] ожидается 21, фактически возвращается 12. Поставить точку останова во внутреннем цикле, добавить в Watch обе длины измерений и текущие координаты. Зафиксировать первый допустимый элемент, который никогда не становится текущим, и связать его пропуск с неверной границей. Проверить квадратную и две неквадратные формы.

10. Перезапуск псевдослучайной последовательности: Исследуется метод:

static int RollDie()
{
    Random rng = new Random(42);
    return rng.Next(1, 7);
}

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

11. Изменение только локальной копии структуры: Исследуется метод:

static void Normalize(Fraction value)
{
    long gcd = Gcd(
        Math.Abs(value.Numerator),
        Math.Abs(value.Denominator));

    value.Numerator /= gcd;
    value.Denominator /= gcd;

    if (value.Denominator < 0)
    {
        value.Numerator = -value.Numerator;
        value.Denominator = -value.Denominator;
    }
}

После Normalize(fraction) значение 6/-8 остаётся неизменным, хотя перед выходом из метода локальные поля равны -3/4. Поставить точку останова перед вызовом, выполнить Step Into, проверить поля перед return, затем применить Step Out и сравнить переменную вызывающего кода. Первое корректное локальное состояние теряется на границе возврата из-за передачи структуры по значению. Исправление должно выбрать явный контракт: ref либо возврат нового значения.

12. Накопленное время вместо отдельных серий: Исследуется фрагмент:

Stopwatch stopwatch = new Stopwatch();
long[] times = new long[sizes.Length];

for (int i = 0; i < sizes.Length; i++)
{
    stopwatch.Start();
    Work(sizes[i]);
    stopwatch.Stop();

    times[i] = stopwatch.ElapsedTicks;
}

Ожидаются отдельные длительности каждого размера, фактически каждый следующий результат включает предыдущие. Поставить точки останова перед Start и после Stop, наблюдать i, IsRunning и ElapsedTicks. Найти первое состояние перед новым запуском, нарушающее постусловие независимого замера. Исправление подтвердить через Restart либо Reset с последующим Start, не меняя измеряемое ядро.


Уровень II. Продвинутый

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

1. Недостижимая ветвь классификации: Исследуется метод:

static string ClassifyTriangle(
    int a,
    int b,
    int c)
{
    if (a <= 0 || b <= 0 || c <= 0 ||
        a + b <= c ||
        a + c <= b ||
        b + c <= a)
    {
        return "Invalid";
    }

    if (a == b || b == c || a == c)
    {
        return "Isosceles";
    }

    if (a == b && b == c)
    {
        return "Equilateral";
    }

    return "Scalene";
}

Для (3,3,3) ожидается "Equilateral", фактически возвращается "Isosceles". Найти первое решение ветвления, после которого правильный результат становится недостижимым. Исправление подтвердить равносторонним, равнобедренным, разносторонним и недопустимым треугольниками.

2. Искусственный максимум: Исследуется метод:

static int Max(int[] data)
{
    int max = 0;

    for (int i = 0; i < data.Length; i++)
    {
        if (data[i] > max)
        {
            max = data[i];
        }
    }

    return max;
}

Предусловие — непустой массив. Для [-5,-2,-8] ожидается -2, фактически возвращается 0. Локализовать момент, в котором состояние уже не может соответствовать максимуму обработанного префикса. Исправление не должно вводить отдельную ветвь для отрицательных массивов.

3. Незавершённый массив префиксных сумм: Исследуется метод:

static long[] BuildPrefix(int[] data)
{
    long[] prefix =
        new long[data.Length + 1];

    for (int i = 1;
         i < data.Length;
         i++)
    {
        prefix[i] =
            prefix[i - 1] +
            data[i - 1];
    }

    return prefix;
}

Для [3,-2,7,1] ожидается [0,3,1,8,9], фактически возвращается [0,3,1,8,0]. Самостоятельно подобрать точки остановки, найти первый индекс результата, который не получает требуемого значения, и исправить только границу прохода.

4. Нарушенный инвариант уплотнения: Исследуется метод:

static int CompactNonZero(int[] data)
{
    int write = 0;

    for (int read = 0;
         read < data.Length;
         read++)
    {
        if (data[read] != 0)
        {
            data[write] = data[read];
        }

        write++;
    }

    return write;
}

Для [0,3,0,-2,5] ожидается логический префикс [3,-2,5] длины 3; фактически возвращается длина 5, а массив не уплотнён. Найти первую итерацию, после которой [0, write) перестаёт содержать только принятые элементы просмотренного префикса.

5. Потеря меры прогресса: Исследуется метод:

static int CountDigits(int value)
{
    int count = 0;

    while (value > 0)
    {
        count++;

        if (value % 10 == 0)
        {
            continue;
        }

        value /= 10;
    }

    return count;
}

Для value = 120 ожидается 3, но метод не завершается. Начать выполнение с точкой останова на заголовке цикла, проследить несколько итераций и определить состояние, которое повторяется без уменьшения меры value. Исправление должно гарантировать прогресс на каждом пути продолжения цикла.

6. Смешение моделей бинарного поиска: Исследуется метод:

static int BinarySearch(
    int[] data,
    int target)
{
    int lo = 0;
    int hi = data.Length;

    while (lo <= hi)
    {
        int mid =
            lo + (hi - lo) / 2;

        if (data[mid] == target)
        {
            return mid;
        }

        if (data[mid] < target)
        {
            lo = mid + 1;
        }
        else
        {
            hi = mid - 1;
        }
    }

    return -1;
}

Для [1,3,5,7,9] и цели 10 программа аварийно завершается. Проследить границы до первой недопустимой координаты и определить, какие части метода принадлежат закрытой, а какие полуоткрытой модели. Исправление должно последовательно выбрать одну модель.

7. Неверная форма транспонированной таблицы: Исследуется метод:

static int[,] Transpose(int[,] source)
{
    int rows = source.GetLength(0);
    int columns = source.GetLength(1);

    int[,] result =
        new int[rows, columns];

    for (int row = 0; row < rows; row++)
    {
        for (int column = 0;
             column < columns;
             column++)
        {
            result[column, row] =
                source[row, column];
        }
    }

    return result;
}

Квадратные таблицы проходят проверку, а таблица 2 × 3 аварийно завершается. Найти первый набор координат, допустимый для источника и недопустимый для результата. Исправление подтвердить формами 1 × 4, 2 × 3, 3 × 2 и 3 × 3.

8. Затравка из отсутствующего элемента: Исследуется поиск максимума:

static int Max(int[]?[] data)
{
    int max = data[0]![0];

    for (int row = 0;
         row < data.Length;
         row++)
    {
        if (data[row] is null)
        {
            continue;
        }

        for (int column = 0;
             column < data[row]!.Length;
             column++)
        {
            if (data[row]![column] > max)
            {
                max = data[row]![column];
            }
        }
    }

    return max;
}

Для [null, [], [-3,-1]] ожидается -1, программа завершается до обхода. Найти первое небезопасное предположение, сформулировать корректное предусловие либо способ инициализации первым реально существующим элементом и отдельно определить контракт структуры без единого элемента.

9. Пропущенная последняя цифра: Исследуется фрагмент ручного разбора:

int sign =
    text[0] == '-' ? -1 : 1;

int start =
    text[0] == '-' ? 1 : 0;

int value = 0;

for (int i = start;
     i < text.Length - 1;
     i++)
{
    value =
        value * 10 +
        (text[i] - '0');
}

value *= sign;

Предварительная проверка знака и цифр выполнена правильно. Для "-123" ожидается -123, фактически получается -12. Найти первую точку, в которой оставшийся необработанный суффикс уже не будет посещён, и проверить исправление на "0", "-7" и "214".

10. Изменение копий элементов структуры: Исследуется метод:

static void ScaleAccepted(
    Sample[] data,
    double factor)
{
    foreach (Sample sample in data)
    {
        Sample copy = sample;

        if (copy.Accepted)
        {
            copy.Value *= factor;
        }
    }
}

После вызова принятые элементы массива должны изменить Value, но массив остаётся прежним. Самостоятельно определить, какие значения существуют внутри итерации, какие из них являются копиями и в какой момент изменённое состояние становится недоступным. Исправление должно менять элементы массива по индексам либо использовать иной явный контракт.

11. Загрязнение выходного буфера предыдущим вызовом: Исследуется метод:

static void CountFaces(
    int[] rolls,
    int[] counts)
{
    for (int i = 0;
         i < rolls.Length;
         i++)
    {
        counts[rolls[i] - 1]++;
    }
}

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

12. Переполнение скрывает математическую сумму: Исследуется метод:

static int Sum(int[] data)
{
    int sum = 0;

    for (int i = 0;
         i < data.Length;
         i++)
    {
        sum += data[i];
    }

    return sum;
}

Для [1_500_000_000, 1_000_000_000] математическая сумма равна 2_500_000_000, фактически возвращается отрицательное значение. До запуска выписать представимый диапазон int, затем пошагово найти первое присваивание, после которого математический результат и значение переменной расходятся. Классифицировать механизм как неверно выбранный диапазон типа, а не ошибку цикла. Исправление проверить на обычных, граничных и переполняющих данных.


Уровень III. Экспертный

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

1. Неверное начальное состояние в алгоритме максимального участка: Программа получает подготовленный массив, проверяет его непустоту, вызывает поиск максимального непустого подмассива и форматирует диапазон. На положительных и смешанных данных результат верен, но для [-5,-2,-8] возвращается пустой участок с суммой 0 вместо элемента -2. В ядре поддерживаются currentSum, bestSum, начала текущего и лучшего участков; оба накопителя инициализированы нулём.

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

2. Координатная ошибка, скрытая квадратной формой: Программа поворачивает таблицу на 90° по часовой стрелке, затем вычисляет суммы строк результата. Все квадратные тесты проходят, а таблица 2 × 3 либо завершается аварийно, либо размещает элементы неверно. В проекте отдельно вычисляются форма результата, отображение координат и обход источника.

Найти минимальную неквадратную форму, на которой проявляется дефект, составить таблицу соответствий для угловых элементов и определить первую формулу, использующую размер не того измерения. Классифицировать причину как ошибку формы либо координатного отображения и проверить формы 1 × n, m × 1, 2 × 3 и 3 × 2.

3. Потеря логических данных между методами: Программа удаляет нули из массива на месте, записывает нули в освободившийся хвост, сортирует результат и копирует первые count элементов. Для [4,0,2,0,3] ожидается [2,3,4], фактически получается [0,0,2]. Каждый метод — уплотнение, сортировка и копирование префикса — проходит часть локальных проверок.

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

4. Снимок, разделяющий изменяемую нагрузку: Структура Batch содержит номер версии, строковую метку и double[] Values. Метод Snapshot возвращает структуру обычным присваиванием. После создания снимка исходные значения масштабируются на месте; отчёт по снимку неожиданно показывает масштабированные данные. Между этими действиями выполняются сортировка, статистика и форматирование, не влияющие на механизм.

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

5. Порядок запуска меняет победителя случайных стратегий: Две стратегии получают один общий Random, но прекращают опыт по разным условиям и поэтому потребляют разное число значений. При одинаковом seed порядок A → B объявляет лучшей стратегию A, а порядок B → A — стратегию B.

Сначала сократить число опытов и записать точное расписание вызовов Next для обоих порядков. Затем доказать, что одинаковое зерно не означает одинаковые входы стратегий при общем изменяемом источнике. Минимальное исправление должно заранее построить общие сценарии либо создать независимые воспроизводимые потоки по явному протоколу. После правки перестановка порядка запуска не должна менять сравниваемые данные.

6. Потеря пустого поля в текстовом конвейере: Программа разбирает запись:

surname;score;comment;status

Пустой комментарий является допустимым. Вход:

Ivanov;91.5;;passed

после Split превращается в три части, вследствие чего passed интерпретируется как комментарий, а статус объявляется отсутствующим. В проекте также есть Trim, числовой разбор и форматирование результата.

Сократить программу до одной строки и операции разбиения, определить первое изменение структуры данных и доказать, что причина — политика удаления пустых частей, а не Trim, культура числового разбора или форматирование. Регрессионный набор должен различать пустое среднее поле, пустое последнее поле, два соседних разделителя и полностью заполненную запись.

7. Ошибочный эталон вместо дефекта программы: Метод складывает 0.1 и 0.2 как double. В карточке дефекта записано ожидание:

result == 0.3

Фактически условие ложно, хотя вычислительный путь и арифметическая операция соответствуют коду. До изменения метода исследовать двоичное представление через формат "R", абсолютную разность и соседние сравнения. Установить, что первое неверное состояние находится не в программе, а в точном ожидаемом равенстве.

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

8. Два дефекта, скрытые удачным тестом: Исследуется среднее массива:

static double Mean(int[] data)
{
    long sum = 0;

    for (int i = 0;
         i < data.Length - 1;
         i++)
    {
        sum += data[i];
    }

    return
        (double)sum /
        (data.Length - 1);
}

Для [5,5,5] метод случайно возвращает ожидаемое 5, а для [2,4,6]3 вместо 4. Требуется:

  1. построить минимальный вход, раскрывающий симптом;
  2. найти первое расхождение;
  3. внести только одно минимальное исправление;
  4. повторить трассировку и показать, что результат всё ещё неверен, но уже по другому механизму;
  5. локализовать второй дефект;
  6. подтвердить итоговый вариант на длинах 1, 2, 3, на одинаковых и различных значениях.

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


Обязательные контрольные наборы

ЗадачаФиксированный симптом и проверки
I.1[4,-2,7] → 10 вместо 9; затем [], [5]
I.290 → B вместо A; границы 74, 75, 89, 90, 91, 100
I.3вызов из условия даёт не 17.5; затем именованные аргументы и равные веса
I.4[5,8,13] → 13 вместо 26; затем [], [7]
I.5[1,2,3,4] → [4,1,1,1]; затем [], [7], [1,2]
I.6SumTo(4) → 6 вместо 10; затем 0, 1, 4
I.7[1,3,5,7,9], цель 7 → -1; также цели 1, 9, 4
I.8[3,1,2]; исходный массив не должен меняться
I.9таблица 2 × 3, ожидаемая сумма 21, фактическая 12; затем 3 × 2 и 3 × 3
I.10десять одинаковых бросков при seed = 42
I.116/-8 остаётся неизменным вместо -3/4
I.12накопленные ElapsedTicks вместо независимых замеров
II.1(3,3,3) → Isosceles; затем четыре класса треугольников
II.2[-5,-2,-8] → 0; затем [7], [1,9,3]
II.3[3,-2,7,1] → [0,3,1,8,0]
II.4[0,3,0,-2,5], ожидаемая длина 3, фактическая 5
II.5120 не завершает CountDigits; затем 1, 10, 101
II.6[1,3,5,7,9], цель 10; затем присутствующие и отсутствующие цели
II.7формы 2 × 3, 1 × 4, 3 × 2, 3 × 3
II.8[null, [], [-3,-1]]; затем структура без единого элемента
II.9"-123" → -12; затем "0", "-7", "214"
II.10три структуры, из которых две приняты; после масштабирования массив не меняется
II.11две серии бросков и один повторно используемый массив частот
II.12[1_500_000_000,1_000_000_000]; затем безопасные и граничные суммы
III.1все отрицательные данные; минимальный пример длины 1
III.2квадратная форма проходит, неквадратная обнаруживает дефект
III.3[4,0,2,0,3] → [0,0,2] вместо [2,3,4]
III.4снимок меняется после масштабирования исходного массива
III.5перестановка порядка A/B меняет победителя при одном seed
III.6"Ivanov;91.5;;passed" и записи с пустыми краевыми полями
III.70.1 + 0.2, точное ожидание 0.3 и проверка с допуском
III.8[5,5,5] скрывает дефекты; [2,4,6] раскрывает их последовательно

Итог модуля

Ученик фиксирует воспроизводимый симптом и проверяемое ожидание, выбирает точки останова и наблюдаемые выражения и использует Step Into, Step Over, Step Out, Locals, Watch и Call Stack для поиска перехода от последнего правильного к первому неверному состоянию. Он распознаёт ошибки начального значения, ветви, границы, меры прогресса, диапазона типа, координатного отображения, глубины копии и состояния между вызовами, отличает дефект программы от ошибочного эталона и строит минимально воспроизводимый пример для межметодной причины. Исправление остаётся минимальным, после каждой правки исходный симптом воспроизводится заново, а достаточность подтверждается граничными, вырожденными и регрессионными проверками, включая случаи случайного прохождения и нескольких одновременно действующих дефектов.

Покрытие опоры и границы

  • Симптом, фиксированный вход, ожидаемый и фактический результат: все задачи.
  • Проверяемая гипотеза и минимальный набор наблюдений: II.1–II.12, III.1–III.8.
  • Точки останова, Locals и Watch: I.1–I.12; самостоятельный выбор — II.1–II.12.
  • Step Into, Step Over, Step Out и Call Stack: I.3, I.6, I.10–I.11; III.1, III.5.
  • Последнее правильное и первое неверное состояние: все задачи.
  • Неверное начальное состояние: I.1, II.2, III.1.
  • Неверная ветвь и недостижимый случай: I.2, II.1.
  • Ошибки границы и модели диапазона: I.4, I.7, I.9, II.3, II.6–II.9, III.2.
  • Потеря данных и неправильное направление изменения: I.5, II.4, III.3.
  • Нарушение меры прогресса и незавершимость: I.6, II.5.
  • Нарушение контракта передачи и копирования: I.8, I.11, II.10, III.4.
  • Скрытое изменяемое состояние между вызовами: I.10, I.12, II.11, III.5.
  • Переполнение и неверный диапазон типа: II.12.
  • Минимально воспроизводимый пример и удаление несущественного контекста: III.1–III.8.
  • Ошибочный эталон и проверка ожидания: III.7.
  • Несколько дефектов и случайно проходящий тест: III.8.
  • Минимальное исправление и регрессионные проверки: все задачи после локализации.

За границей: обработка исключений и политика отказа рассматриваются в Разделе 24, а файловый ввод-вывод — в Разделе 25. Логгирование, модульные и интеграционные тестовые фреймворки, удалённая отладка, дампы памяти, профилировщики, дизассемблирование, отладка многопоточности, гонок данных и производственных систем относятся к последующим курсам. Диагностический вывод и автоматические проверки могут дополнять исследование, но не заменяют доказательство первого неверного состояния и механизма причины.