Написание, отладка и тестирование функций в C#

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

В этой статье рассматриваются следующие темы:

  • Написание функций
  • Отладка во время разработки
  • Горячая перезагрузка во время разработки
  • Логирование во время разработки и выполнения
  • Модульное тестирование
  • Генерация и обработка исключений в функциях
Содержание

Написание функций

Основополагающий принцип программирования — «Не повторяйся» (DRY, Don’t Repeat Yourself). Если в процессе программирования вы замечаете, что пишете одни и те же операторы снова и снова, то объедините эти операторы в функцию. Функции подобны маленьким программам, которые выполняют одну небольшую задачу. Например, вы можете написать функцию для расчета налога с продаж и затем использовать эту функцию в различных местах финансового приложения.

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

Пример использования функций:

Рассмотрим пример функции, которая рассчитывает налог с продаж. Допустим, вам нужно часто выполнять этот расчет в вашем приложении. Вместо того, чтобы каждый раз писать одно и то же, создайте функцию:

// Функция для расчета налога с продаж
decimal CalculateSalesTax(decimal amount, decimal taxRate)
{
    return amount * taxRate;
}

Теперь вы можете использовать эту функцию в любом месте вашего кода:

decimal price = 100m;
decimal taxRate = 0.08m;
decimal tax = CalculateSalesTax(price, taxRate);
Console.WriteLine($"Налог с продаж: {tax}");

Программы верхнего уровня и функции

Начиная с .NET 6, шаблон проекта для консольных приложений использует функционал программ верхнего уровня, введенный в C# 9.

Когда вы начинаете писать функции, важно понимать, как они работают вместе с автоматически сгенерированным классом Program и его методом <Main>$.

В файле Program.cs вы можете написать инструкции для импорта класса, вызова одного из его методов, определения и вызова функции, как показано в следующем коде:

using static System.Console;
WriteLine("Hello, World!");
DoSomething(); // вызов функции
void DoSomething() // определение функции
{
    WriteLine("Doing something!");
}

Компилятор автоматически генерирует класс Program с функцией <Main>$, затем перемещает ваши инструкции и функции внутрь метода <Main>$, и переименовывает функцию, как показано в следующем коде:

using static System.Console;
partial class Program
{
    static void <Main>$(String[] args)
    {
        WriteLine("Hello, World!");
        <<Main>$>g__DoSomething|0_0(); // вызов функции
        void <<Main>$>g__DoSomething|0_0() // определение локальной функции
        {
            WriteLine("Doing something!");
        }
    }
}

Чтобы компилятор знал, какие инструкции куда перемещать, вы должны следовать нескольким правилам:

  • Инструкции импорта (using) должны располагаться в верхней части файла Program.cs.
  • Инструкции, которые будут помещены в функцию <Main>$, должны находиться в середине файла Program.cs.
  • Функции должны располагаться в нижней части файла Program.cs. Они станут локальными функциями.

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

Улучшение подхода к определению функций

Лучший подход состоит в определении функции в отдельном файле и добавлении ее в качестве статического члена класса Program, как показано в следующем коде:

// в файле с именем Program.Functions.cs
partial class Program
{
    static void DoSomething() // определение нестатической локальной функции
    {
        WriteLine("Doing something!");
    }
}
// в файле Program.cs
using static System.Console;
WriteLine("Hello, World!");
DoSomething(); // вызов функции

Компилятор определяет класс Program с функцией <Main>$, перемещает ваши инструкции внутрь метода <Main>$, а затем объединяет вашу функцию как члена класса Program, как показано в следующем коде:

using static System.Console;
partial class Program
{
    static void <Main>$(String[] args)
    {
        WriteLine("Hello, World!");
        DoSomething(); // вызов функции
    }

    static void DoSomething() // определение функции
    {
        WriteLine("Doing something!");
    }
}

Хорошая практика

Создавайте все функции, которые вы будете вызывать в Program.cs, в отдельном файле и вручную определяйте их внутри частичного класса Program. Это объединит их с автоматически сгенерированным классом Program на том же уровне, что и метод <Main>$, а не в качестве локальных функций внутри метода <Main>$.

Пример таблицы умножения

Допустим, вы хотите помочь вашему ребенку выучить таблицу умножения, и для этого вам нужно упростить создание таблицы умножения для какого-то числа, например, таблицы умножения на 7:

1 x 7 = 7
2 x 7 = 14
3 x 7 = 21
...
10 x 7 = 70
11 x 7 = 77
12 x 7 = 84

Большинство таблиц умножения содержат 10, 12 или 20 строк, в зависимости от уровня подготовки ребенка.

Вы уже изучили оператор for ранее в этой книге, поэтому знаете, что его можно использовать для генерации повторяющихся строк вывода, когда имеется регулярный шаблон, например, таблица умножения на 7 с 12 строками, как показано в следующем коде:

for (int row = 1; row <= 12; row++)
{
    Console.WriteLine($"{row} x 7 = {row * 7}");
}

Однако вместо того, чтобы всегда выводить таблицу умножения на 7 с 12 строками, мы хотим сделать это более гибким, чтобы можно было выводить таблицу умножения для любого числа и любого количества строк. Для этого мы можем создать функцию.

Создание функции для вывода таблицы умножения

Рассмотрим создание функции, которая выводит таблицу умножения для чисел от 0 до 255 с количеством строк до 255 (по умолчанию — 12 строк).

Шаги по созданию проекта:

  1. Создание проекта:
    • Шаблон проекта: Console App/console
    • Файл и папка проекта: WritingFunctions
    • Файл и папка рабочей области/решения: Chapter04
  2. Добавление статического импорта System.Console:
    • Откройте WritingFunctions.csproj и после секции <PropertyGroup> добавьте новую секцию <ItemGroup>, как показано ниже:
      <ItemGroup>
          <Using Include="System.Console" Static="true" />
      </ItemGroup>
      
  3. Создание нового файла класса:
    • Добавьте новый файл класса с именем Program.Functions.cs.
  4. Определение функции TimesTable:
    • В файле Program.Functions.cs добавьте следующую функцию в частичный класс Program:
      partial class Program
      {
          static void TimesTable(byte number, byte size = 12)
          {
              WriteLine($"This is the {number} times table with {size} rows:");
              for (int row = 1; row <= size; row++)
              {
                  WriteLine($"{row} x {number} = {row * number}");
              }
              WriteLine();
          }
      }
      

      В этом коде:

      • TimesTable принимает параметр типа byte с именем number.
      • TimesTable может принимать необязательный параметр типа byte с именем size (по умолчанию 12).
      • TimesTable является статическим методом, так как он будет вызываться статическим методом <Main>$.
      • TimesTable использует оператор for для вывода таблицы умножения для числа, переданного в параметре number с количеством строк, равным size.
  5. Вызов функции в Program.cs:
    • В файле Program.cs удалите существующие операторы и вызовите функцию TimesTable с байтовым значением, например, 7:
      TimesTable(7);
      

      Результат выполнения кода:

      This is the 7 times table with 12 rows:
      1 x 7 = 7
      2 x 7 = 14
      3 x 7 = 21
      4 x 7 = 28
      5 x 7 = 35
      6 x 7 = 42
      7 x 7 = 49
      8 x 7 = 56
      9 x 7 = 63
      10 x 7 = 70
      11 x 7 = 77
      12 x 7 = 84
      
  6. Изменение параметра size:
    • Измените параметр size на 20:
      TimesTable(7, 20);
      
  7. Проверка работы программы:
    • Запустите консольное приложение и убедитесь, что таблица умножения теперь имеет 20 строк.
  8. Тестирование с другими значениями:
    • Измените значение, передаваемое в функцию TimesTable, на другие байтовые значения от 0 до 255 и убедитесь, что вывод корректен.
  9. Обработка ошибок:
    • Если вы попытаетесь передать значение другого типа, например, int, double или string, будет возвращена ошибка:
      Error: (1,12): error CS1503: Argument 1: cannot convert from 'int' to 'byte'
      

Полный пример кода

// В файле Program.Functions.cs
partial class Program
{
    static void TimesTable(byte number, byte size = 12)
    {
        WriteLine($"This is the {number} times table with {size} rows:");
        for (int row = 1; row <= size; row++)
        {
            WriteLine($"{row} x {number} = {row * number}");
        }
        WriteLine();
    }
}

// В файле Program.cs
using static System.Console;

TimesTable(7);     // Таблица умножения на 7 с 12 строками
TimesTable(7, 20); // Таблица умножения на 7 с 20 строками

Краткое отступление о аргументах и параметрах

В повседневной жизни разработчики часто используют термины «аргумент» и «параметр» как синонимы. Строго говоря, эти два термина имеют специфичные и тонкие различия. Но, как и человек может быть одновременно родителем и врачом, оба термина часто применяются к одной и той же вещи.

Параметр — это переменная в определении функции. Например, startDate является параметром функции Hire, как показано в следующем коде:

void Hire(DateTime startDate)
{
    // реализация функции
}

Когда метод вызывается, аргумент — это данные, которые вы передаете в параметры метода. Например, when является переменной, передаваемой как аргумент функции Hire, как показано в следующем коде:

DateTime when = new(year: 2022, month: 11, day: 8);
Hire(when);

Вы можете предпочесть указать имя параметра при передаче аргумента, как показано в следующем коде:

DateTime when = new(year: 2022, month: 11, day: 8);
Hire(startDate: when);

При обсуждении вызова функции Hire, startDate является параметром, а when — аргументом. Ситуация усложняется, когда один и тот же объект может действовать как параметр и как аргумент в зависимости от контекста. Например, внутри реализации функции Hire параметр startDate может быть передан как аргумент другой функции, такой как SaveToDatabase, как показано в следующем коде:

void Hire(DateTime startDate)
{
    ...
    SaveToDatabase(startDate, employeeRecord);
    ...
}

Названия объектов — одна из самых сложных задач в программировании. Классический пример — параметр самой важной функции в C#, Main. Она определяет параметр с именем args, сокращение от «аргументы», как показано в следующем коде:

static void Main(String[] args)
{
    ...
}

Вкратце, параметры определяют входные данные функции, а аргументы передаются функции при вызове.

Если вы читаете официальную документацию Microsoft, они используют фразы «именованные и необязательные аргументы» и «именованные и необязательные параметры» как синонимы, как показано по следующей ссылке: Named and Optional Arguments.

Хорошая практика: Старайтесь использовать правильный термин в зависимости от контекста, но не будьте педантичными с другими разработчиками, если они «неправильно» используют термин.

Написание функции, возвращающей значение

Предыдущая функция выполняла действия (циклы и вывод в консоль), но не возвращала значение. Допустим, вам нужно рассчитать налог с продаж или налог на добавленную стоимость (НДС). В Европе ставки НДС могут варьироваться от 8% в Швейцарии до 27% в Венгрии. В Соединенных Штатах налог с продаж может варьироваться от 0% в Орегоне до 8.25% в Калифорнии.

Давайте реализуем функцию для расчета налогов в различных регионах мира:

  1. В файле Program.Functions.cs напишите функцию с именем CalculateTax, как показано в следующем коде:
    partial class Program
    {
        ...
        static decimal CalculateTax(decimal amount, string twoLetterRegionCode)
        {
            decimal rate = 0.0M;
            switch (twoLetterRegionCode.ToUpper())
            {
                case "CH": // Швейцария
                    rate = 0.08M;
                    break;
                case "DK": // Дания
                case "NO": // Норвегия
                    rate = 0.25M;
                    break;
                case "GB": // Великобритания
                case "FR": // Франция
                    rate = 0.2M;
                    break;
                case "HU": // Венгрия
                    rate = 0.27M;
                    break;
                case "OR": // Орегон
                case "AK": // Аляска
                case "MT": // Монтана
                    rate = 0.0M;
                    break;
                case "ND": // Северная Дакота
                case "WI": // Висконсин
                case "ME": // Мэн
                case "VA": // Вирджиния
                    rate = 0.05M;
                    break;
                case "CA": // Калифорния
                    rate = 0.0825M;
                    break;
                default: // большинство штатов США
                    rate = 0.06M;
                    break;
            }
            return amount * rate;
        }
    }
    

    В приведенном выше коде обратите внимание на следующее:

    • CalculateTax имеет два входных параметра: amount (сумма потраченных денег) и twoLetterRegionCode (регион, в котором потрачена сумма).
    • CalculateTax выполнит расчет, используя оператор switch, и затем вернет налог с продаж или НДС, который должен быть уплачен на сумму, как десятичное значение. Поэтому перед именем функции объявляется тип возвращаемого значения decimal.
  2. Закомментируйте вызовы метода TimesTable, а затем вызовите метод CalculateTax, передав значения для суммы, например, 149, и действительный код региона, например, FR, как показано в следующем коде:
    decimal taxToPay = CalculateTax(amount: 149, twoLetterRegionCode: "FR");
    WriteLine($"You must pay {taxToPay:C} in tax.");
    
  3. Запустите код и посмотрите результат, как показано в следующем выводе:
    You must pay €29.80 in tax.
    

Мы могли бы форматировать вывод taxToPay как валюту, используя {taxToPay:C}, но это будет использовать вашу локальную культуру для определения символа валюты и десятичных знаков. Например, для меня в Великобритании это будет выглядеть как £29.80.

Улучшение функции CalculateTax

Есть несколько проблем с функцией CalculateTax в текущем виде:

  • Что произойдет, если пользователь введет код региона в нижнем регистре, например, fr или uk?
  • Как можно переписать функцию, чтобы улучшить её?
  • Было бы использование выражения switch вместо оператора switch более ясным?

Давайте улучшим функцию, учитывая эти вопросы.

Улучшенная версия функции CalculateTax с использованием выражения switch

partial class Program
{
    ...
    static decimal CalculateTax(decimal amount, string twoLetterRegionCode)
    {
        twoLetterRegionCode = twoLetterRegionCode.ToUpper();
        
        decimal rate = twoLetterRegionCode switch
        {
            "CH" => 0.08M, // Швейцария
            "DK" or "NO" => 0.25M, // Дания или Норвегия
            "GB" or "FR" => 0.2M, // Великобритания или Франция
            "HU" => 0.27M, // Венгрия
            "OR" or "AK" or "MT" => 0.0M, // Орегон, Аляска или Монтана
            "ND" or "WI" or "ME" or "VA" => 0.05M, // Северная Дакота, Висконсин, Мэн или Вирджиния
            "CA" => 0.0825M, // Калифорния
            _ => 0.06M // большинство штатов США
        };

        return amount * rate;
    }
}

В этой улучшенной версии мы преобразуем twoLetterRegionCode в верхний регистр перед использованием выражения switch. Это гарантирует, что функция будет работать корректно, даже если пользователь введет код региона в нижнем регистре. Выражение switch делает код более компактным и читабельным.

Написание функции для преобразования чисел из количественных в порядковые

Давайте создадим функцию для преобразования количественных чисел (таких как 1, 2, 3) в порядковые английские (такие как 1st, 2nd, 3rd).

  1. В файле Program.Functions.cs напишите функцию с именем CardinalToOrdinal, которая преобразует кардинальное значение типа int в строковое порядковое значение, как показано в следующем коде:
    partial class Program
    {
        static string CardinalToOrdinal(int number)
        {
            int lastTwoDigits = number % 100;
            switch (lastTwoDigits)
            {
                case 11: // особые случаи для 11th до 13th
                case 12:
                case 13:
                    return $"{number:N0}th";
                default:
                    int lastDigit = number % 10;
                    string suffix = lastDigit switch
                    {
                        1 => "st",
                        2 => "nd",
                        3 => "rd",
                        _ => "th"
                    };
                    return $"{number:N0}{suffix}";
            }
        }
    }
    

    В приведенном выше коде обратите внимание на следующее:

    • CardinalToOrdinal имеет один входной параметр int типа number и один выход: строковое значение.
    • Оператор switch используется для обработки особых случаев 11, 12 и 13.
    • Выражение switch обрабатывает все остальные случаи: если последняя цифра 1, то используется суффикс st; если последняя цифра 2, то используется суффикс nd; если последняя цифра 3, то используется суффикс rd; и если последняя цифра любая другая, то используется суффикс th.
  2. В файле Program.Functions.cs напишите функцию с именем RunCardinalToOrdinal, которая использует оператор for для цикла от 1 до 150, вызывая функцию CardinalToOrdinal для каждого числа и выводя возвращенную строку в консоль, разделяя их пробелом, как показано в следующем коде:
    partial class Program
    {
        ...
        static void RunCardinalToOrdinal()
        {
            for (int number = 1; number <= 150; number++)
            {
                Write($"{CardinalToOrdinal(number)} ");
            }
            WriteLine();
        }
    }
    
  3. В файле Program.cs закомментируйте операторы вызова CalculateTax и вызовите метод RunCardinalToOrdinal, как показано в следующем коде:
    RunCardinalToOrdinal();
    
  4. Запустите консольное приложение и просмотрите результаты, как показано в следующем выводе:
    1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th 28th 29th 30th 31st 32nd 33rd 34th 35th 36th 37th 38th 39th 40th 41st 42nd 43rd 44th 45th 46th 47th 48th 49th 50th 51st 52nd 53rd 54th 55th 56th 57th 58th 59th 60th 61st 62nd 63rd 64th 65th 66th 67th 68th 69th 70th 71st 72nd 73rd 74th 75th 76th 77th 78th 79th 80th 81st 82nd 83rd 84th 85th 86th 87th 88th 89th 90th 91st 92nd 93rd 94th 95th 96th 97th 98th 99th 100th 101st 102nd 103rd 104th 105th 106th 107th 108th 109th 110th 111th 112th 113th 114th 115th 116th 117th 118th 119th 120th 121st 122nd 123rd 124th 125th 126th 127th 128th 129th 130th 131st 132nd 133rd 134th 135th 136th 137th 138th 139th 140th 141st 142nd 143rd 144th 145th 146th 147th 148th 149th 150th
    
  5. В функции RunCardinalToOrdinal измените максимальное число на 1500:
    static void RunCardinalToOrdinal()
    {
        for (int number = 1; number <= 1500; number++)
        {
            Write($"{CardinalToOrdinal(number)} ");
        }
        WriteLine();
    }
    
  6. Запустите консольное приложение и просмотрите результаты, как показано частично в следующем выводе:
    ...
    1480th 1481st 1482nd 1483rd 1484th 1485th 1486th 1487th 1488th 1489th 1490th 1491st 1492nd 1493rd 1494th 1495th 1496th 1497th 1498th 1499th 1500th
    

Вычисление факториалов с использованием рекурсии

Факториал числа n (обозначается как n!) представляет собой произведение всех натуральных чисел от 1 до n. Например, факториал числа 5 (5!) равен 120, потому что 5 × 4 × 3 × 2 × 1 = 120. Факториалы могут быть вычислены с использованием рекурсии, когда функция вызывает саму себя.

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

  1. В файле Program.Functions.cs напишите функцию с именем Factorial, как показано в следующем коде:
    partial class Program
    {
        static int Factorial(int number)
        {
            if (number < 0)
            {
                throw new ArgumentException(message:
                    $"The factorial function is defined for non-negative integers only. Input: {number}",
                    paramName: nameof(number));
            }
            else if (number == 0)
            {
                return 1;
            }
            else
            {
                checked // для проверки переполнения
                {
                    return number * Factorial(number - 1);
                }
            }
        }
    
        static void RunFactorial()
        {
            for (int i = -2; i <= 15; i++)
            {
                try
                {
                    WriteLine($"{i}! = {Factorial(i):N0}");
                }
                catch (OverflowException)
                {
                    WriteLine($"{i}! is too big for a 32-bit integer.");
                }
                catch (Exception ex)
                {
                    WriteLine($"{i}! throws {ex.GetType()}: {ex.Message}");
                }
            }
        }
    }
    

    В приведенном выше коде обратите внимание на следующее:

    • Если входной параметр number отрицательный, функция Factorial генерирует исключение ArgumentException.
    • Если входной параметр number равен нулю, функция Factorial возвращает 1.
    • Если входной параметр number больше нуля, функция Factorial рекурсивно вызывает себя, уменьшая значение number на 1 каждый раз.
    • Оператор checked используется для проверки переполнения.
  2. В файле Program.cs закомментируйте вызов метода RunCardinalToOrdinal и вызовите метод RunFactorial, как показано в следующем коде:
    RunFactorial();
    
  3. Запустите консольное приложение и просмотрите результаты, как показано в следующем выводе:
    -2! throws System.ArgumentException: The factorial function is defined for non-negative integers only. Input: -2 (Parameter 'number')
    -1! throws System.ArgumentException: The factorial function is defined for non-negative integers only. Input: -1 (Parameter 'number')
    0! = 1
    1! = 1
    2! = 2
    3! = 6
    4! = 24
    5! = 120
    6! = 720
    7! = 5,040
    8! = 40,320
    9! = 362,880
    10! = 3,628,800
    11! = 39,916,800
    12! = 479,001,600
    13! is too big for a 32-bit integer.
    14! is too big for a 32-bit integer.
    15! is too big for a 32-bit integer.
    

Результаты показывают, что для отрицательных чисел генерируется исключение ArgumentException. Для больших чисел, начиная с 13!, происходит переполнение, и функция генерирует исключение OverflowException.

Документирование функций с использованием XML комментариев

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

Пример добавления XML комментариев

  1. В файле Program.Functions.cs найдите функцию CardinalToOrdinal и добавьте XML комментарии выше функции. Для этого наберите три прямых косых черты ///, и редактор кода автоматически создаст шаблон XML комментариев.
  2. Заполните XML комментарии соответствующей информацией. Например:
    /// <summary>
    /// Pass a 32-bit integer and it will be converted into its ordinal equivalent.
    /// </summary>
    /// <param name="number">Number as a cardinal value e.g. 1, 2, 3, and so on.</param>
    /// <returns>Number as an ordinal value e.g. 1st, 2nd, 3rd, and so on.</returns>
    static string CardinalToOrdinal(int number)
    {
        int lastTwoDigits = number % 100;
        switch (lastTwoDigits)
        {
            case 11: // special cases for 11th to 13th
            case 12:
            case 13:
                return $"{number:N0}th";
            default:
                int lastDigit = number % 10;
                string suffix = lastDigit switch
                {
                    1 => "st",
                    2 => "nd",
                    3 => "rd",
                    _ => "th"
                };
                return $"{number:N0}{suffix}";
        }
    }
    

Объяснение

  • <summary>: Описание функции, объясняющее, что делает функция.
  • <param name=»number»>: Описание параметра number, указывающее, что это за параметр и какие значения он может принимать.
  • <returns>: Описание возвращаемого значения, объясняющее, что возвращает функция.

Пример использования XML комментариев для других функций

Добавим XML комментарии к функции Factorial:

/// <summary>
/// Calculates the factorial of a non-negative integer using recursion.
/// </summary>
/// <param name="number">A non-negative integer.</param>
/// <returns>The factorial of the given integer.</returns>
/// <exception cref="ArgumentException">Thrown when the input is a negative integer.</exception>
/// <exception cref="OverflowException">Thrown when the factorial result exceeds the limits of a 32-bit integer.</exception>
static int Factorial(int number)
{
    if (number < 0)
    {
        throw new ArgumentException(message:
            $"The factorial function is defined for non-negative integers only. Input: {number}",
            paramName: nameof(number));
    }
    else if (number == 0)
    {
        return 1;
    }
    else
    {
        checked // для проверки переполнения
        {
            return number * Factorial(number - 1);
        }
    }
}

Пример вызова функции с подсказками

Когда вы вызываете функцию в вашем коде, редактор кода покажет вам расширенные подсказки, основанные на ваших XML комментариях:

static void Main()
{
    // Вызов функции с подсказками
    string ordinal = CardinalToOrdinal(5);
    int factorial = Factorial(5);
}

Полезные советы

  • Документируйте все функции, кроме локальных функций. Локальные функции не могут быть использованы за пределами их объявления, поэтому нет смысла генерировать для них документацию.
  • Используйте инструменты для генерации документации. XML комментарии можно использовать с инструментами, такими как Sandcastle, для создания подробной справочной документации.

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

Использование лямбда-выражений в реализациях функций

Рассмотрим использование лямбда-выражений для реализации функций в C#. Мы проиллюстрируем это на примере вычисления чисел Фибоначчи.

Пример императивного подхода

  1. Создайте функцию FibImperative в файле Program.Functions.cs, которая реализует императивный стиль вычисления чисел Фибоначчи.
    static int FibImperative(int term)
    {
        if (term == 1)
        {
            return 0;
        }
        else if (term == 2)
        {
            return 1;
        }
        else
        {
            return FibImperative(term - 1) + FibImperative(term - 2);
        }
    }
    
  2. Создайте функцию RunFibImperative в том же файле, которая будет вызывать FibImperative и выводить результат для первых 30 чисел Фибоначчи.
    static void RunFibImperative()
    {
        for (int i = 1; i <= 30; i++)
        {
            WriteLine("The {0} term of the Fibonacci sequence is {1:N0}.",
                arg0: CardinalToOrdinal(i),
                arg1: FibImperative(term: i));
        }
    }
    
  3. В файле Program.cs закомментируйте вызовы других методов и добавьте вызов метода RunFibImperative.
    // Закомментируйте другие вызовы методов
    // RunCardinalToOrdinal();
    // RunFactorial();
    
    RunFibImperative();
    
  4. Запустите приложение и убедитесь, что оно выводит правильные результаты для первых 30 чисел Фибоначчи.

Пример декларативного подхода

  1. Создайте функцию FibFunctional в файле Program.Functions.cs, которая реализует функциональный стиль вычисления чисел Фибоначчи с использованием лямбда-выражений.
    static int FibFunctional(int term) =>
        term switch
        {
            1 => 0,
            2 => 1,
            _ => FibFunctional(term - 1) + FibFunctional(term - 2)
        };
    
  2. Создайте функцию RunFibFunctional в том же файле, которая будет вызывать FibFunctional и выводить результат для первых 30 чисел Фибоначчи.
    static void RunFibFunctional()
    {
        for (int i = 1; i <= 30; i++)
        {
            WriteLine("The {0} term of the Fibonacci sequence is {1:N0}.",
                arg0: CardinalToOrdinal(i),
                arg1: FibFunctional(term: i));
        }
    }
    
  3. В файле Program.cs закомментируйте вызов метода RunFibImperative и добавьте вызов метода RunFibFunctional.
    // Закомментируйте другие вызовы методов
    // RunCardinalToOrdinal();
    // RunFactorial();
    RunFibImperative();
    
    // Добавьте вызов метода RunFibFunctional
    RunFibFunctional();
    
  4. Запустите приложение и убедитесь, что оно выводит правильные результаты для первых 30 чисел Фибоначчи.

Заключение

Использование лямбда-выражений и декларативного стиля в C# позволяет писать более лаконичный и читаемый код, особенно для математических вычислений, таких как вычисление чисел Фибоначчи. Это делает код более функциональным и уменьшает вероятность ошибок, связанных с изменением состояния переменных.

Отладка во время разработки

Настройка отладчика в Visual Studio Code

Чтобы эффективно использовать отладочные инструменты в Visual Studio Code, необходимо изменить настройку консоли на integratedTerminal для взаимодействия с программой во время отладки:

  1. Откройте файл launch.json в папке .vscode вашего проекта.
  2. Измените настройку консоли с internalConsole на integratedTerminal, как показано ниже:
    {
        "version": "0.2.0",
        "configurations": [
            {
                ...
                "console": "integratedTerminal",
                ...
            }
        ]
    }
    

Создание кода с намеренной ошибкой

Для изучения отладки создадим консольное приложение с намеренной ошибкой, которую будем исправлять с помощью инструментов отладки:

  1. Создайте новый проект консольного приложения под названием Debugging в рабочем пространстве/решении Chapter04.
    • В Visual Studio Code выберите Debugging в качестве активного проекта OmniSharp. Если появится предупреждающее сообщение о недостающих активах, нажмите Yes, чтобы добавить их.
    • В Visual Studio 2022 установите Debugging в качестве стартового проекта для решения.
  2. Модифицируйте Debugging.csproj, чтобы статически импортировать System.Console для всех файлов кода:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
      </PropertyGroup>
      <ItemGroup>
        <Using Include="System.Console" />
      </ItemGroup>
    </Project>
    
  3. В файле Program.cs добавьте функцию с намеренной ошибкой:
    using static System.Console;
    
    class Program
    {
        static void Main()
        {
            double a = 4.5;
            double b = 2.5;
            double answer = Add(a, b);
            WriteLine($"{a} + {b} = {answer}");
            WriteLine("Press ENTER to end the app.");
            ReadLine(); // wait for user to press ENTER
        }
    
        static double Add(double a, double b)
        {
            return a * b; // deliberate bug!
        }
    }
    
  4. Запустите консольное приложение и убедитесь, что результат неверен:
    4.5 + 2.5 = 11.25
    Press ENTER to end the app.
    

Установка точки останова и начало отладки

Точки останова позволяют остановить выполнение программы на определенной строке кода для проверки состояния программы и нахождения ошибок:

  1. Установите точку останова на строке double answer = Add(a, b); в функции Main.
  2. Начните отладку:
    • В Visual Studio Code нажмите F5 или выберите Run > Start Debugging.
    • В Visual Studio 2022 нажмите F5 или выберите Debug > Start Debugging.
  3. Программа остановится на точке останова. Проверьте значения переменных a и b.
  4. Шаг за шагом выполните программу, используя F10 (Step Over) для перехода к следующей строке. Обратите внимание на значение переменной answer.
  5. Обнаружив ошибку в функции Add, исправьте ее:
    static double Add(double a, double b)
    {
        return a + b; // fixed bug
    }
    
  6. Снова запустите отладку и убедитесь, что результат теперь правильный:

Отладка во время разработки в Visual Studio 2022

Установка точки останова и начало отладки

  1. Щелкните по первой строке кода, где объявляется переменная a.
  2. Перейдите в меню Debug | Toggle Breakpoint или нажмите F9. В левом поле появится красный кружок, а строка будет выделена красным цветом, указывая, что точка останова установлена, как показано на рисунке ниже.
  3. Перейдите в Debug | Start Debugging или нажмите F5. Visual Studio запустит консольное приложение и остановится на точке останова. Это называется режимом останова. Откроются окна Locals (показывающее текущие значения локальных переменных), Watch 1 (показывающее любые выражения для наблюдения, которые вы определили), Call Stack, Exception Settings и Immediate Window. Появится панель инструментов Debugging. Строка, которая будет выполнена следующей, выделена желтым цветом, и желтая стрелка указывает на строку из поля слева.

Панель инструментов отладки

Visual Studio 2022 имеет отдельную панель инструментов для отладки и кнопки на стандартной панели инструментов для запуска или продолжения отладки и горячей перезагрузки изменений в коде. Описание кнопок:

  • Start/Continue/F5: Запускает проект или продолжает выполнение проекта до конца или до следующей точки останова.
  • Hot Reload: Перезагружает скомпилированные изменения в коде без необходимости перезапуска приложения.
  • Break All: Прерывает выполнение программы на следующей доступной строке кода.
  • Stop Debugging/Stop/Shift + F5: Останавливает сеанс отладки.
  • Restart/Ctrl + Shift + F5: Останавливает и сразу же перезапускает программу с подключенным отладчиком.
  • Show Next Statement: Перемещает текущий курсор к следующему оператору, который будет выполнен.
  • Step Into/F11, Step Over/F10, Step Out/Shift + F11: Пошаговое выполнение операторов кода различными способами.
  • Show Threads in Source: Позволяет просматривать и работать с потоками в приложении, которое вы отлаживаете.

Отладка в Visual Studio Code

Установка точки останова и начало отладки

  1. Щелкните по первой строке кода, где объявляется переменная a.
  2. Перейдите в меню Run | Toggle Breakpoint или нажмите F9. В левом поле появится красный кружок, указывающий, что точка останова установлена.
  3. Перейдите в View | Run или в левом меню нажмите на значок Run and Debug (треугольная кнопка «play» и «bug»).
  4. В верхней части окна RUN AND DEBUG выберите .NET Core Launch (console) (Debugging) в выпадающем списке справа от кнопки Start Debugging (зеленая треугольная кнопка «play»).

Если вы не видите вариант для отладки проекта, значит проект не имеет необходимых активов для отладки. Чтобы создать папку .vscode для проекта, перейдите в View | Command Palette, выберите OmniSharp: Select Project, а затем выберите проект Debugging. После нескольких секунд, когда появится запрос «Required assets to build and debug are missing from ‘Debugging’. Add them?», нажмите Yes, чтобы добавить недостающие активы.

  1. В верхней части окна RUN AND DEBUG нажмите кнопку Start Debugging (зеленая треугольная кнопка «play»), перейдите в Run | Start Debugging или нажмите F5. Visual Studio Code запустит консольное приложение и остановится на точке останова. Строка, которая будет выполнена следующей, выделена желтым цветом, а желтая стрелка указывает на строку из поля слева, как показано ниже.

Окна отладки

При отладке в Visual Studio Code и Visual Studio 2022 появляются дополнительные окна, которые позволяют вам отслеживать полезную информацию, такую как переменные, в процессе выполнения кода. Самые полезные окна описаны ниже:

  • VARIABLES (Локальные переменные): Показывает имя, значение и тип любых локальных переменных автоматически. Наблюдайте за этим окном, пока вы шагаете по коду.
  • WATCH (Наблюдение): Показывает значение переменных и выражений, которые вы вручную вводите.
  • CALL STACK (Стек вызовов): Показывает стек вызовов функций.
  • BREAKPOINTS (Точки останова): Показывает все ваши точки останова и позволяет управлять ими.

Когда приложение находится в режиме останова, также доступно полезное окно в нижней части области редактирования:

  • DEBUG CONSOLE или Immediate Window (Консоль отладки): Позволяет взаимодействовать с кодом в реальном времени. Вы можете запрашивать состояние программы, например, вводя имя переменной или выражение.

Пошаговая отладка кода

Давайте рассмотрим несколько способов пошаговой отладки кода в Visual Studio или Visual Studio Code:

  1. Перейдите в Run или Debug | Step Into, нажмите кнопку Step Into на панели инструментов или нажмите F11. Жёлтая подсветка переместится на одну строку вперед.
  2. Перейдите в Run или Debug | Step Over, нажмите кнопку Step Over на панели инструментов или нажмите F10. Жёлтая подсветка переместится на одну строку вперед. На данный момент вы не увидите разницы между Step Into и Step Over.

Вы должны сейчас быть на строке, вызывающей метод Add.

Разница между Step Into и Step Over видна, когда вы собираетесь выполнить вызов метода:

  • Если нажать Step Into, отладчик войдет в метод, позволяя пройти через каждую строку метода.
  • Если нажать Step Over, метод будет выполнен целиком, не переходя внутрь метода.
  1. Нажмите Step Into, чтобы войти внутрь метода.
  2. Наведите указатель мыши на параметры a или b в окне редактирования кода, чтобы увидеть текущие значения в появившемся всплывающем окне.
  3. Выделите выражение a * b, щелкните правой кнопкой мыши по выражению и выберите Add to Watch или Add Watch. Выражение добавится в окно WATCH, показывая, что операция умножает a на b, давая результат 11.25.
  4. В окне WATCH щелкните правой кнопкой мыши по выражению и выберите Remove Expression или Delete Watch.
  5. Исправьте ошибку, изменив * на + в функции Add.
  6. Остановите отладку, перекомпилируйте и перезапустите отладку, нажав кнопку Restart (круговая стрелка) или нажав Ctrl + Shift + F5.
  7. Пошагово выполните функцию, обратите внимание, что она теперь правильно вычисляется, и нажмите кнопку Continue или F5.
  8. В Visual Studio Code обратите внимание, что вывод в консоль во время отладки появляется в окне DEBUG CONSOLE, а не в окне TERMINAL.

Настройка точек останова

Создание более сложных точек останова:

  1. Если вы все еще отлаживаете, нажмите кнопку Stop на панели инструментов отладки, перейдите в Run или Debug | Stop Debugging, или нажмите Shift + F5.
  2. Перейдите в Run | Remove All Breakpoints или Debug | Delete All Breakpoints.
  3. Щелкните по строке WriteLine, выводящей ответ.
  4. Установите точку останова, нажав F9 или перейдя в Run или Debug | Toggle Breakpoint.
  5. Щелкните правой кнопкой мыши по точке останова и выберите соответствующее меню для вашего редактора кода:
    • В Visual Studio Code выберите Edit Breakpoint…
    • В Visual Studio 2022 выберите Conditions…
  6. Введите выражение, например, answer должно быть больше чем 9, и нажмите Enter, чтобы принять это условие. Выражение должно быть истинным, чтобы точка останова сработала.
  7. Начните отладку и обратите внимание, что точка останова не сработала.
  8. Остановите отладку.
  9. Измените точку останова или ее условия на выражение меньше чем 9.
  10. Начните отладку и обратите внимание, что точка останова сработала.
  11. Остановите отладку.
  12. Измените точку останова или ее условия (в Visual Studio 2022 нажмите Add condition), выберите Hit Count и введите число, например, 3, что означает, что точка останова сработает только после того, как она будет достигнута три раза.
  13. Наведите указатель мыши на красный круг точки останова, чтобы увидеть сводку.

Вы исправили ошибку с помощью инструментов отладки и увидели некоторые возможности для настройки точек останова.

Горячая перезагрузка во время разработки

Горячая перезагрузка (Hot Reload) — это функция, которая позволяет разработчику применять изменения кода во время работы приложения и сразу видеть результат. Это полезно для быстрого исправления ошибок. Горячая перезагрузка также известна как Edit and Continue. Список изменений, поддерживающих горячую перезагрузку, можно найти по следующей ссылке: https://aka.ms/dotnet/hot-reload.

Незадолго до выпуска .NET 6 один из высокопоставленных сотрудников Microsoft вызвал разногласия, пытаясь сделать эту функцию доступной только в Visual Studio. К счастью, благодаря усилиям сообщества с открытым исходным кодом внутри Microsoft, это решение было отменено. Горячая перезагрузка осталась доступной и через командную строку.

Пример использования горячей перезагрузки

  1. Используйте предпочитаемый инструмент кодирования, чтобы добавить новый проект консольного приложения с именем HotReloading в рабочее пространство/решение Chapter04.
    • В Visual Studio Code выберите HotReloading в качестве активного проекта OmniSharp. Когда появится предупреждающее сообщение о том, что требуемые активы отсутствуют, нажмите Yes, чтобы добавить их.
  2. Измените файл HotReloading.csproj, чтобы статически импортировать System.Console для всех файлов кода.
  3. В Program.cs удалите существующие операторы и напишите сообщение в консольный вывод каждые две секунды, как показано в следующем коде:
    /* Visual Studio: запустите приложение, измените сообщение, нажмите кнопку Hot Reload.
    * Visual Studio Code: запустите приложение с помощью dotnet watch, измените сообщение. */
    while (true)
    {
        WriteLine("Hello, Hot Reload!");
        await Task.Delay(2000);
    }
    

Горячая перезагрузка с использованием Visual Studio 2022

  1. В Visual Studio запустите консольное приложение и обратите внимание, что сообщение выводится каждые две секунды.
  2. Измените Hello на Goodbye, перейдите в Debug | Apply Code Changes или нажмите кнопку Hot Reload на панели инструментов и убедитесь, что изменение применяется без необходимости перезапуска консольного приложения.
  3. Раскройте меню кнопки Hot Reload и выберите Hot Reload on File Save.
  4. Измените сообщение снова, сохраните файл и обратите внимание, что консольное приложение обновляется автоматически.

Горячая перезагрузка с использованием Visual Studio Code и командной строки

  1. В Visual Studio Code в TERMINAL запустите консольное приложение с помощью dotnet watch и обратите внимание на вывод, показывающий, что горячая перезагрузка активирована:
    dotnet watch 🔥🔥 Hot reload enabled. For a list of supported edits, see
    https://aka.ms/dotnet/hot-reload.
    💡💡 Press "Ctrl + R" to restart.
    dotnet watch 🔧🔧 Building...
    Determining projects to restore...
    All projects are up-to-date for restore.
    HotReloading -> C:\cs11dotnet7\Chapter04\HotReloading\bin\Debug\net7.0\HotReloading.dll
    dotnet watch 🚀🚀 Started
    Hello, Hot Reload!
    Hello, Hot Reload!
    Hello, Hot Reload!
    
  2. В Visual Studio Code измените Hello на Goodbye и обратите внимание, что через пару секунд изменение применяется без необходимости перезапуска консольного приложения:
    Hello, Hot Reload!
    Hello, Hot Reload!
    Goodbye, Hot Reload!
    

Логирование во время разработки и выполнения программы

Когда вы считаете, что устранили все ошибки в своем коде, вы компилируете версию для выпуска и разворачиваете приложение, чтобы люди могли его использовать. Однако ни один код не может быть полностью свободным от ошибок, и во время выполнения могут возникать неожиданные ошибки.

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

Хорошая практика

Добавляйте код для логирования по всему приложению, особенно в местах, где могут возникать исключения. Это позволит вам просматривать логи и использовать их для поиска и устранения проблем.

Понимание возможностей логирования

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

  • Apache log4net
  • NLog
  • Serilog

Логирование с использованием Debug и Trace

Существует два типа, которые можно использовать для простого логирования в вашем коде: Debug и Trace.

  • Класс Debug используется для добавления логирования, которое пишется только во время разработки.
  • Класс Trace используется для логирования как во время разработки, так и во время выполнения.

Вы уже видели использование типа Console и его метода WriteLine для вывода в консольное окно. Аналогично, классы Debug и Trace предоставляют большую гибкость в выборе места для записи логов.

Запись в стандартный слушатель трассировки

Один из слушателей трассировки, класс DefaultTraceListener, настраивается автоматически и записывает в окно DEBUG CONSOLE в Visual Studio Code или в окно Debug в Visual Studio. Вы можете настроить другие слушатели трассировки с помощью кода.

Пример использования слушателей трассировки

  1. Используйте предпочитаемый инструмент кодирования, чтобы добавить новый проект консольного приложения с именем Instrumenting в рабочее пространство/решение Chapter04.
    • В Visual Studio Code выберите Instrumenting в качестве активного проекта OmniSharp. Когда появится предупреждающее сообщение о том, что требуемые активы отсутствуют, нажмите Yes, чтобы добавить их.
  2. В файле Program.cs удалите существующие операторы и добавьте пространство имен System.Diagnostics:
    using System.Diagnostics;
    
  3. В файле Program.cs добавьте сообщение от классов Debug и Trace:
    Debug.WriteLine("Debug говорит: Я наблюдаю!");
    Trace.WriteLine("Trace говорит: Я наблюдаю!");
    
  4. В Visual Studio перейдите в View | Output и убедитесь, что выбрано Show output from: Debug.
  5. Начните отладку консольного приложения Instrumenting и обратите внимание, что в окне DEBUG CONSOLE в Visual Studio Code или в окне Output в Visual Studio 2022 отображаются два сообщения, смешанные с другой отладочной информацией, такой как загруженные DLL библиотеки.

Настройка слушателей трассировки

Теперь мы настроим другой слушатель трассировки, который будет записывать логи в текстовый файл:

  1. Перед вызовами Debug и Trace методов WriteLine добавьте код для создания нового текстового файла на рабочем столе и передачи его новому слушателю трассировки, который умеет записывать в текстовый файл. Также включите автоматическое сбрасывание буфера, как показано в следующем примере:
    string logPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "log.txt");
    Console.WriteLine($"Writing to: {logPath}");
    TextWriterTraceListener logFile = new(File.CreateText(logPath));
    Trace.Listeners.Add(logFile);
    // text writer is buffered, so this option calls
    // Flush() on all listeners after writing
    Trace.AutoFlush = true;
  2. Хорошая практика

    Любой тип, представляющий файл, обычно использует буфер для улучшения производительности. Вместо того чтобы записывать данные сразу в файл, они сначала записываются в буфер в памяти, и только когда буфер заполняется, данные записываются в файл одним блоком. Это поведение может сбивать с толку при отладке, так как мы не видим результаты сразу. Включение AutoFlush означает, что метод Flush будет вызываться автоматически после каждой записи.

  3. Запустите приложение в конфигурации для выпуска (Release):
    • В Visual Studio Code введите следующую команду в окне TERMINAL для проекта Instrumenting и обратите внимание, что ничего не произойдет на экране:
      dotnet run --configuration Release
      
    • В Visual Studio 2022 на стандартной панели инструментов выберите Release в выпадающем списке конфигураций решения, затем перейдите к Debug | Start Without Debugging.
  4. На рабочем столе откройте файл с именем log.txt и убедитесь, что он содержит сообщение Trace says, I am watching!.
  5. Запустите приложение в конфигурации для отладки (Debug):
    • В Visual Studio Code введите следующую команду в окне TERMINAL для проекта Instrumenting:
      dotnet run --configuration Debug
      
    • В Visual Studio на стандартной панели инструментов выберите Debug в выпадающем списке конфигураций решения, затем перейдите к Debug | Start Debugging.
  6. На рабочем столе откройте файл с именем log.txt и убедитесь, что он содержит оба сообщения: Debug says, I am watching! и Trace says, I am watching!.

Хорошая практика

При запуске в конфигурации Debug активны и Debug, и Trace, и они будут записывать логи для всех слушателей трассировки. При запуске в конфигурации Release только Trace будет записывать логи. Таким образом, вы можете свободно использовать вызовы Debug.WriteLine в вашем коде, зная, что они автоматически будут исключены при сборке версии для выпуска и не повлияют на производительность.

Управление уровнями трассировки

Вызовы Trace.WriteLine остаются в вашем коде даже после выпуска. Поэтому важно иметь возможность тонко контролировать, когда они будут выводиться. Это можно сделать с помощью переключателя трассировки (trace switch).

Значение переключателя трассировки может быть задано числом или словом. Например, число 3 может быть заменено словом Info, как показано в следующей таблице:

Число Слово Описание
0 Off Ничего не выводится.
1 Error Выводятся только ошибки.
2 Warning Выводятся ошибки и предупреждения.
3 Info Выводятся ошибки, предупреждения и информационные сообщения.
4 Verbose Выводятся все уровни сообщений.

Давайте рассмотрим использование переключателей трассировки. Сначала добавим несколько NuGet-пакетов в наш проект для загрузки конфигурационных настроек из JSON-файла appsettings.

Добавление пакетов в проект в Visual Studio 2022

Visual Studio имеет графический интерфейс для добавления пакетов:

  1. В обозревателе решений (Solution Explorer) щелкните правой кнопкой мыши проект Instrumenting и выберите Manage NuGet Packages.
  2. Перейдите на вкладку Browse.
  3. Введите в поле поиска Microsoft.Extensions.Configuration.
  4. Выберите каждый из следующих пакетов NuGet и нажмите кнопку Install:
    • Microsoft.Extensions.Configuration
    • Microsoft.Extensions.Configuration.Binder
    • Microsoft.Extensions.Configuration.FileExtensions
    • Microsoft.Extensions.Configuration.Json

    Хорошая практика

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

Добавление пакетов в проект в Visual Studio Code

Visual Studio Code не имеет механизма для добавления NuGet-пакетов через интерфейс, поэтому мы используем командную строку:

  1. Перейдите в окно TERMINAL для проекта Instrumenting.
  2. Введите следующую команду:
    dotnet add package Microsoft.Extensions.Configuration
    
  3. Введите следующую команду:
  4. Введите следующую команду:
  5. Введите следующую команду:

Команда dotnet add package добавляет ссылку на NuGet-пакет в ваш файл проекта. Пакет будет загружен во время процесса сборки. Команда dotnet add reference добавляет ссылку на другой проект в ваш файл проекта. Ссылочный проект будет скомпилирован, если это необходимо, во время процесса сборки.

Настройка переключателя трассировки

Теперь, когда все необходимые пакеты добавлены, давайте настроим переключатель трассировки.

  1. Создайте файл appsettings.json в корне проекта и добавьте в него следующий код:
    {
      "Logging": {
        "LogLevel": {
          "Default": "Info"
        }
      }
    }
    
  2. В файле Program.cs добавьте следующие using-директивы:
    using Microsoft.Extensions.Configuration;
    using System.Diagnostics;
    
  3. Добавьте код для загрузки настроек из файла appsettings.json и настройки переключателя трассировки:
    var builder = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    
    IConfiguration configuration = builder.Build();
    
    SourceSwitch sourceSwitch = new SourceSwitch("SourceSwitch", configuration["Logging:LogLevel:Default"]);
    TraceSource traceSource = new TraceSource("TraceSource", SourceLevels.All);
    traceSource.Switch = sourceSwitch;
    traceSource.Listeners.Add(new ConsoleTraceListener());
    
    traceSource.TraceEvent(TraceEventType.Information, 0, "Пример информационного сообщения.");
    traceSource.TraceEvent(TraceEventType.Warning, 0, "Пример предупреждения.");
    traceSource.TraceEvent(TraceEventType.Error, 0, "Пример ошибки.");
    

Этот код загружает настройки из appsettings.json и использует их для настройки переключателя трассировки sourceSwitch. Теперь вы можете управлять уровнями логирования с помощью файла конфигурации.

Обзор пакетов проекта

После добавления пакетов NuGet, мы можем увидеть ссылки на них в файле проекта. Давайте рассмотрим, как это сделать:

  1. Откройте Instrumenting.csproj и найдите секцию <ItemGroup>, в которой перечислены добавленные NuGet пакеты:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
      </ItemGroup>
    </Project>
    
  2. Добавьте файл appsettings.json в папку проекта Instrumenting.
  3. В файле appsettings.json определите настройку PacktSwitch с уровнем Level, как показано ниже:
    {
      "PacktSwitch": {
        "Level": "Info"
      }
    }
    
  4.  В Visual Studio 2022, в обозревателе решений (Solution Explorer), щелкните правой кнопкой мыши по файлу appsettings.json, выберите Properties, а затем в окне свойств измените Copy to Output Directory на Copy if newer. Это необходимо, так как Visual Studio выполняет консольное приложение в папке Instrumenting\bin\Debug\net7.0 или Instrumenting\bin\Release\net7.0, в отличие от Visual Studio Code, которое выполняет приложение в папке проекта.
  5. В файле Program.cs добавьте пространство имен Microsoft.Extensions.Configuration:
    using Microsoft.Extensions.Configuration;
    
  6. Добавьте код в конец файла Program.cs, чтобы создать конфигурационный билд, который ищет файл appsettings.json в текущей папке, собирает конфигурацию, создает переключатель трассировки (trace switch), устанавливает его уровень, привязываясь к конфигурации, и выводит четыре уровня переключателя трассировки:
    using System.Diagnostics;
    
    Console.WriteLine("Чтение из appsettings.json в {0}", Directory.GetCurrentDirectory());
    
    var builder = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
        
    IConfigurationRoot configuration = builder.Build();
    
    TraceSwitch ts = new(
        displayName: "PacktSwitch",
        description: "Этот переключатель настроен через конфигурацию JSON."
    );
    
    configuration.GetSection("PacktSwitch").Bind(ts);
    
    Trace.WriteLineIf(ts.TraceError, "Ошибка трассировки");
    Trace.WriteLineIf(ts.TraceWarning, "Предупреждение трассировки");
    Trace.WriteLineIf(ts.TraceInfo, "Информация трассировки");
    Trace.WriteLineIf(ts.TraceVerbose, "Подробная трассировка");
    
    Console.ReadLine();
    
  7. Установите точку останова на операторе Bind.
  8. Начните отладку консольного приложения Instrumenting.
  9. В окне VARIABLES или Locals разверните выражение переменной ts и убедитесь, что его уровень (Level) установлен в Off, а TraceError, TraceWarning и другие значения равны false.
  10. Шагните в вызов метода Bind с помощью кнопок Step Into или Step Over или нажатием клавиш F11 или F10 и убедитесь, что выражение переменной ts обновилось до уровня Info.
  11. Шагните в или через четыре вызова Trace.WriteLineIf и убедитесь, что все уровни до Info выводятся в DEBUG CONSOLE или Output - Debug окне, кроме Verbose.
  12. Остановите отладку.
  13. Измените файл appsettings.json, чтобы установить уровень 2, что означает предупреждение:
    {
      "PacktSwitch": {
        "Level": "2"
      }
    }
    
  14. Сохраните изменения.
  15. В Visual Studio Code выполните консольное приложение, введя следующую команду в окне TERMINAL для проекта Instrumenting:
    dotnet run --configuration Release
    
  16. В Visual Studio, в стандартной панели инструментов, выберите Release в раскрывающемся списке Solution Configurations и запустите консольное приложение, выбрав Debug | Start Without Debugging.
  17. Откройте файл с именем log.txt и убедитесь, что в этот раз выводятся только уровни ошибок и предупреждений:
    Trace says, I am watching!
    Trace error
    Trace warning
    

Если аргумент не передан, уровень переключателя трассировки по умолчанию установлен в Off (0), поэтому ни один из уровней переключателя не выводится.

Юнит-тестирование

Юнит-тестирование – это отличный способ выявления ошибок на ранних этапах разработки. Некоторые разработчики даже следуют принципу написания юнит-тестов до написания основного кода, что называется разработкой, управляемой тестами (Test-Driven Development, TDD). У Microsoft есть собственный фреймворк для юнит-тестирования под названием MSTest, а также существует NUnit. Мы же будем использовать бесплатный и открытый сторонний фреймворк xUnit.net. xUnit был создан той же командой, что и NUnit, но с учетом исправлений ошибок предыдущих версий. xUnit более расширяем и имеет лучшую поддержку сообщества.

Типы тестирования

Юнит-тестирование – это лишь один из многих типов тестирования. Вот краткий обзор других видов тестирования:

  • Юнит-тестирование: Тестирует наименьшую единицу кода, как правило, метод или функцию. Юнит-тесты выполняются на изолированной части кода, часто с использованием mock’ов. Каждая единица должна иметь несколько тестов: с типичными входными данными и ожидаемыми результатами, с граничными значениями для тестирования экстремальных случаев и с намеренно неправильными данными для проверки обработки ошибок.
  • Интеграционное тестирование: Проверяет, как меньшие единицы и более крупные компоненты работают вместе как целое. Иногда включает интеграцию с внешними компонентами, для которых у вас нет исходного кода.
  • Системное тестирование: Тестирует всю системную среду, в которой будет работать ваше программное обеспечение.
  • Тестирование производительности: Оценивает, как ваше программное обеспечение работает, например, насколько быстро веб-страница загружается (например, менее чем за 20 миллисекунд).
  • Нагрузочное тестирование: Определяет, сколько одновременных запросов может обработать ваше программное обеспечение, сохраняя при этом необходимую производительность, например, 10 000 одновременных посетителей сайта.
  • Приемочное тестирование пользователей (UAT): Проверяет, могут ли пользователи успешно завершить свои задачи, используя ваше программное обеспечение.

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

Создание библиотеки классов, которая требует тестирования

Для начала мы создадим функцию, которая будет нуждаться в тестировании. Мы создадим ее в проекте библиотеки классов, который будет отдельным от консольного приложения. Библиотека классов – это пакет кода, который может быть распространен и подключен к другим .NET приложениям. Следуйте инструкциям ниже, чтобы создать проект библиотеки классов и настроить его для тестирования.

1. Создание проекта библиотеки классов

Используйте ваше любимое средство разработки, чтобы добавить новый проект библиотеки классов с именем CalculatorLib в рабочее пространство/решение Chapter04. К этому моменту у вас уже должно быть создано несколько проектов консольных приложений, добавленных в решение Visual Studio 2022 или рабочее пространство Visual Studio Code. Единственное отличие при добавлении библиотеки классов – это выбор другого шаблона проекта. Остальные шаги такие же, как при добавлении консольного приложения. Для вашего удобства повторю эти шаги как для Visual Studio 2022, так и для Visual Studio Code.

Если вы используете Visual Studio 2022:
  1. Перейдите в Файл | Добавить | Новый проект.
  2. В диалоговом окне Добавить новый проект, в Последние шаблоны проектов, выберите Библиотека классов [C#] и нажмите Далее.
  3. В диалоговом окне Настройка нового проекта, в поле Имя проекта введите CalculatorLib, оставьте расположение по умолчанию (C:\cs11dotnet7\Chapter04), и нажмите Далее.
  4. В диалоговом окне Дополнительная информация, выберите .NET 8.0, затем нажмите Создать.
Если вы используете Visual Studio Code:
  1. Перейдите в Файл | Добавить папку в рабочее пространство….
  2. В папке Chapter04 нажмите кнопку Новая папка, создайте новую папку с именем CalculatorLib, выберите ее и нажмите Добавить.
  3. Когда вас спросят, доверяете ли вы этой папке, нажмите Да.
  4. Перейдите в Терминал | Новый терминал, в появившемся выпадающем списке выберите CalculatorLib.
  5. В TERMINAL убедитесь, что вы находитесь в папке CalculatorLib, затем введите следующую команду для создания новой библиотеки классов: dotnet new classlib.
  6. Перейдите в Вид | Палитра команд, затем выберите OmniSharp: Выбрать проект.
  7. В выпадающем списке выберите проект CalculatorLib, и когда вас спросят, добавлять ли необходимые активы для отладки, нажмите Да.

2. Переименование файла и создание класса

В проекте CalculatorLib переименуйте файл Class1.cs в Calculator.cs.

Измените содержимое файла, чтобы определить класс Calculator с намеренной ошибкой:

namespace CalculatorLib
{
    public class Calculator
    {
        public double Add(double a, double b)
        {
            return a * b; // Ошибка: должно быть сложение, а не умножение
        }
    }
}

3. Компиляция проекта библиотеки классов

  • В Visual Studio 2022 перейдите в Построение | Построить CalculatorLib.
  • В Visual Studio Code в TERMINAL введите команду dotnet build.

4. Создание xUnit тестового проекта

Используйте ваше любимое средство разработки, чтобы добавить новый проект xUnit с именем CalculatorLibUnitTests в рабочее пространство/решение Chapter04.

5. Добавление ссылки на проект библиотеки классов

Если вы используете Visual Studio 2022:
  1. В Обозревателе решений выберите проект CalculatorLibUnitTests.
  2. Перейдите в Проект | Добавить ссылку на проект…, отметьте галочкой проект CalculatorLib, затем нажмите OK.
Если вы используете Visual Studio Code:
  1. Используйте команду dotnet add reference, чтобы добавить ссылку на проект CalculatorLib.
  2. Или откройте файл CalculatorLibUnitTests.csproj и измените конфигурацию, добавив группу элементов с ссылкой на проект CalculatorLib, как показано ниже:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <IsPackable>false</IsPackable>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
        <PackageReference Include="xunit" Version="2.4.1" />
        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
          <PrivateAssets>all</PrivateAssets>
        </PackageReference>
        <PackageReference Include="coverlet.collector" Version="3.1.0">
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
          <PrivateAssets>all</PrivateAssets>
        </PackageReference>
      </ItemGroup>
      <ItemGroup>
        <ProjectReference Include="..\CalculatorLib\CalculatorLib.csproj" />
      </ItemGroup>
    </Project>
    

6. Построение проекта с тестами

Постройте проект CalculatorLibUnitTests, чтобы убедиться, что все зависимости настроены правильно и проект готов к написанию и запуску тестов.

Написание юнит-тестов

Хорошо написанный юнит-тест должен состоять из трех частей:

  1. Arrange (Подготовка): В этой части объявляются и инициализируются переменные для входных и выходных данных.
  2. Act (Действие): В этой части выполняется тестируемая единица кода. В нашем случае это вызов метода, который мы хотим протестировать.
  3. Assert (Проверка): В этой части выполняются утверждения о выходных данных. Утверждение – это предположение, что результат соответствует ожидаемому. Если это не так, тест считается проваленным. Например, при сложении 2 и 2 мы ожидаем результат 4.

Теперь напишем несколько юнит-тестов для класса Calculator.

  1. Переименуйте файл UnitTest1.cs в CalculatorUnitTests.cs и откройте его.
  2. Если вы используете Visual Studio Code, переименуйте класс в CalculatorUnitTests. (Visual Studio предложит вам переименовать класс при переименовании файла.)
  3. Импортируйте пространство имен CalculatorLib.
  4. Измените класс CalculatorUnitTests, чтобы добавить два метода тестирования: один для сложения 2 и 2, и другой для сложения 2 и 3.

Ниже приведен пример кода:

using CalculatorLib;
using Xunit;

namespace CalculatorLibUnitTests
{
    public class CalculatorUnitTests
    {
        [Fact]
        public void TestAdding2And2()
        {
            // arrange
            double a = 2;
            double b = 2;
            double expected = 4;
            Calculator calc = new();

            // act
            double actual = calc.Add(a, b);

            // assert
            Assert.Equal(expected, actual);
        }

        [Fact]
        public void TestAdding2And3()
        {
            // arrange
            double a = 2;
            double b = 3;
            double expected = 5;
            Calculator calc = new();

            // act
            double actual = calc.Add(a, b);

            // assert
            Assert.Equal(expected, actual);
        }
    }
}

Объяснение:

  • [Fact]: Атрибут, который указывает, что метод является тестовым.
  • TestAdding2And2 и TestAdding2And3: Эти методы тестируют, что сложение чисел выполняется правильно.
  • В обоих методах сначала задаются переменные (arrange), затем вызывается метод сложения (act), и, наконец, проверяется, что результат соответствует ожидаемому (assert).

Запуск юнит-тестов в Visual Studio 2022

Теперь, когда тесты написаны, мы готовы их запустить и посмотреть результаты:

  1. В Visual Studio перейдите в Тест | Запустить все тесты.
  2. В Обозревателе тестов (Test Explorer) обратите внимание на результаты: два теста были выполнены, один тест прошел, а другой не прошел, как показано на рисунке 4.20.

Запуск юнит-тестов в Visual Studio Code

Если вы используете Visual Studio Code, выполните следующие шаги:

  1. В TERMINAL проекта CalculatorLibUnitTests запустите тесты с помощью следующей команды:
    dotnet test
    
  1. Обратите внимание, что результаты показывают, что два теста были выполнены, один тест прошел, а другой не прошел, как показано на рисунке 4.21.

Исправление ошибки

Теперь вы можете исправить ошибку:

  1. Исправьте ошибку в методе Add в классе Calculator. Измените оператор умножения (*) на оператор сложения (+):
    public double Add(double a, double b)
    {
        return a + b; // Исправление: теперь операция выполняет сложение
    }
    
  2. Запустите юнит-тесты снова, чтобы убедиться, что ошибка исправлена, и теперь оба теста проходят успешно.

Генерация и обработка исключений в функциях

В предыдущей статье мы познакомились с исключениями и с тем, как использовать оператор try-catch для их обработки. Однако, вы должны обрабатывать исключения только в том случае, если у вас достаточно информации для решения проблемы. Если это не так, то стоит позволить исключению передаваться по стеку вызовов на более высокий уровень.

Понимание ошибок использования и ошибок выполнения

Ошибки использования возникают, когда программист неправильно использует функцию, например, передает недопустимые значения параметров. Эти ошибки можно избежать, если программист изменит свой код, чтобы передавать допустимые значения. Некоторые новички в C# и .NET полагают, что исключений можно всегда избежать, так как считают все ошибки ошибками использования. Ошибки использования должны быть устранены до запуска программы в продуктивной среде.

Ошибки выполнения возникают во время выполнения программы и не могут быть исправлены только путем написания «лучшего» кода. Ошибки выполнения можно разделить на ошибки программы и системные ошибки. Например, если вы пытаетесь получить доступ к сетевому ресурсу, но сеть недоступна, вам нужно обработать эту системную ошибку, например, записав исключение в лог и попытавшись снова через некоторое время. Но некоторые системные ошибки, такие как недостаток памяти, не могут быть исправлены программно. Если вы пытаетесь открыть файл, которого не существует, вы можете поймать это исключение и создать новый файл программно. Ошибки программы можно исправить программно, тогда как системные ошибки часто не поддаются исправлению.

Часто выбрасываемые исключения в функциях

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

При определении своих функций с параметрами, ваш код должен проверять значения параметров и выбрасывать исключения, если их значения не позволят функции корректно выполнить свои задачи. Например, если аргумент функции не должен быть null, выбросьте ArgumentNullException. В других ситуациях вы можете использовать ArgumentException, NotSupportedException или InvalidOperationException. Для любого исключения важно включать сообщение, которое описывает проблему для того, кто будет его читать (обычно это разработчик, использующий вашу библиотеку, или конечный пользователь, если это верхний уровень GUI приложения), как показано в следующем примере:

static void Withdraw(string accountName, decimal amount)
{
    if (accountName is null)
    {
        throw new ArgumentNullException(paramName: nameof(accountName));
    }
    if (amount < 0)
    {
        throw new ArgumentException(
            message: $"{nameof(amount)} не может быть меньше нуля.");
    }
    // обработка параметров
}

Хорошая практика: Если функция не может успешно выполнить свою операцию, это следует считать ее неудачей и сообщить об этом, выбросив исключение.

Вместо того чтобы писать if-оператор и затем выбрасывать новое исключение, в .NET 6 введен удобный метод для выброса исключения, если аргумент равен null, как показано в следующем коде:

static void Withdraw(string accountName, decimal amount)
{
    ArgumentNullException.ThrowIfNull(accountName);
    // обработка параметров
}

C# 11 также представил оператор проверки !! для выполнения той же задачи, но позже он был удален после критики, как показано в следующем коде:

static void Withdraw(string accountName!!, decimal amount)
{
    // обработка параметров
}

Не нужно писать try-catch для обработки ошибок типа «ошибка использования». Вы хотите, чтобы приложение завершилось, чтобы заставить программиста, вызывающего функцию, исправить свой код. Эти ошибки должны быть исправлены до развертывания в продуктивной среде. Это не значит, что ваш код не должен выбрасывать такие исключения. Он должен работать так, чтобы заставить других программистов корректно вызывать ваши функции!

Устройство стека вызовов в C#

Когда вы запускаете консольное приложение на .NET, его выполнение начинается с метода Main. Этот метод является точкой входа программы. В зависимости от того, как вы организовали свой код, Main может быть явно определён в вашем классе Program, или же компилятор автоматически создаст метод с именем <Main>$ для упрощённых программ, использующих top-level statements.

Схема стека вызова функций

Метод Main вызывает другие методы, которые, в свою очередь, могут вызывать другие методы, и так далее. Эти методы могут находиться как в текущем проекте, так и в других проектах, на которые ссылается ваше приложение, включая библиотеки, загруженные через NuGet.

На изображении выше (рисунок 4.22) показан пример подобной цепочки вызовов, где методы из разных библиотек взаимодействуют друг с другом.

Пример работы со стеком вызовов

Рассмотрим пример, чтобы понять, как мы можем перехватывать и обрабатывать исключения в таком стеке вызовов.

  1. Создание библиотеки классов:
    • Создайте новый проект типа Class Library и назовите его CallStackExceptionHandlingLib.
    • Переименуйте автоматически созданный файл Class1.cs в Calculator.cs.
    • В файле Calculator.cs напишите следующий код:
      using static System.Console;
      namespace CallStackExceptionHandlingLib
      {
          public class Calculator
          {
              public static void Gamma() 
              {
                  WriteLine("In Gamma");
                  Delta();
              }
      
              private static void Delta()
              {
                  WriteLine("In Delta");
                  File.OpenText("bad file path");
              }
          }
      }
      
  2. Создание консольного приложения:
    • Создайте новый консольный проект и назовите его CallStackExceptionHandling.
    • В этом проекте добавьте ссылку на библиотеку CallStackExceptionHandlingLib, которую вы создали ранее.
    • В файле Program.cs удалите весь существующий код и замените его следующим
      using CallStackExceptionHandlingLib;
      using static System.Console;
      
      WriteLine("In Main");
      Alpha();
      
      void Alpha()
      {
          WriteLine("In Alpha");
          Beta();
      }
      
      void Beta()
      {
          WriteLine("In Beta");
          Calculator.Gamma();
      }
      
  3. Запуск приложения:
    • Запустите консольное приложение без подключения отладчика. Вы увидите следующий результат:
      In Main
      In Alpha
      In Beta
      In Gamma
      In Delta
      Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\cs11dotnet7\Chapter04\CallStackExceptionHandling\bin\Debug\net7.0\bad file path'.
      

Разбор результатов

Когда программа выполняется, метод Main вызывает метод Alpha, который в свою очередь вызывает метод Beta. Beta вызывает метод Gamma из вашей библиотеки CallStackExceptionHandlingLib, а Gamma вызывает Delta. В методе Delta происходит попытка открыть несуществующий файл, что вызывает исключение FileNotFoundException.

Стек вызовов (call stack) показывает последовательность вызовов методов, начиная с Main и заканчивая Delta, где произошло исключение. Если в каком-либо из этих методов был бы блок try-catch, исключение могло бы быть перехвачено и обработано. Если исключение не перехватывается, оно «поднимается» по стеку вызовов до тех пор, пока не достигнет вершины, где .NET выводит информацию об исключении.

Совет: запуск без отладчика

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

Где обрабатывать исключения

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

Повторное выбрасывание исключений

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

Существует три способа повторного выбрасывания исключения внутри блока catch:

  1. Повторное выбрасывание пойманного исключения с сохранением исходного стека вызовов:
    throw;
    


    Этот способ позволяет сохранить всю информацию о стеке вызовов, что особенно полезно при отладке.

  2. Повторное выбрасывание исключения на текущем уровне стека вызовов:
    throw ex;
    

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

  3. Оборачивание пойманного исключения в новое:
    throw new InvalidOperationException("Сообщение об ошибке", ex);
    

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

Пример повторного выбрасывания исключений

Предположим, что функция Gamma может привести к возникновению ошибки. В таком случае можно использовать один из методов повторного выбрасывания исключений:

try
{
    Calculator.Gamma();
}
catch (IOException ex)
{
    LogException(ex);

    // Повторное выбрасывание исключения как будто оно произошло здесь (теряется информация о стеке вызовов)
    throw ex;

    // Повторное выбрасывание исключения с сохранением исходного стека вызовов
    throw;

    // Выбрасывание нового исключения с вложением исходного исключения
    throw new InvalidOperationException(
        message: "Произошла ошибка в расчетах. Дополнительная информация во вложенном исключении.",
        innerException: ex);
}

Применение в нашем примере

Чтобы увидеть это в действии, добавим блок try-catch в метод Beta:

void Beta()
{
    WriteLine("In Beta");
    try
    {
        Calculator.Gamma();
    }
    catch (Exception ex)
    {
        WriteLine($"Поймано исключение: {ex.Message}");
        throw ex; // Теряем информацию о стеке вызовов
    }
}

Если запустить программу с этим кодом, вы заметите, что в выводе будут отсутствовать некоторые детали стека вызовов.

Теперь заменим throw ex; на throw;, чтобы сохранить исходный стек вызовов:

void Beta()
{
    WriteLine("In Beta");
    try
    {
        Calculator.Gamma();
    }
    catch (Exception ex)
    {
        WriteLine($"Поймано исключение: {ex.Message}");
        throw; // Сохраняем информацию о стеке вызовов
    }
}

После этого, если снова запустить программу, в выводе появятся все детали стека вызовов, что поможет лучше понять, где произошла ошибка.

Этот пример демонстрирует, как правильно работать с исключениями, чтобы не потерять важную информацию при их обработке.

Реализация паттерна «тестер-исполнитель»

Паттерн «тестер-исполнитель» (tester-doer) позволяет избежать некоторых исключений, но не исключает их полностью. Этот паттерн использует пару функций: одна проверяет условие, другая выполняет действие, которое может завершиться ошибкой, если условие не выполнено.

Примером реализации этого паттерна в .NET является добавление элемента в коллекцию. Прежде чем вызвать метод Add, можно проверить, является ли коллекция доступной только для чтения. Если она только для чтения, вызов метода Add приведет к исключению.

Пример использования паттерна «тестер-исполнитель» может выглядеть так:

if (!bankAccount.IsOverdrawn())
{
    bankAccount.Withdraw(amount);
}

Здесь перед попыткой снять деньги со счета проверяется, не превышен ли лимит. Если проверка проходит успешно, выполняется операция снятия средств.

Проблемы паттерна «тестер-исполнитель»

Паттерн «тестер-исполнитель» может вносить дополнительные накладные расходы на производительность. Поэтому иногда целесообразно использовать «try pattern», который объединяет тестирование и выполнение в одной функции. Пример такого подхода мы видели в методе TryParse.

Еще одна проблема с паттерном «тестер-исполнитель» возникает при работе с многопоточными приложениями. Например, один поток может вызвать функцию проверки и получить положительный результат. Однако другой поток может изменить состояние до того, как первый поток завершит выполнение. Это приводит к «состоянию гонки» (race condition), когда первый поток продолжает выполнение, предполагая, что все в порядке, хотя это уже не так. Разрешение таких ситуаций выходит за рамки данной статьи.

Если вы решите реализовать собственную функцию по принципу «try pattern» и эта функция завершится неудачей, не забудьте присвоить выходному параметру значение по умолчанию и вернуть false. Вот пример:

static bool TryParse(string? input, out Person value)
{
    if (someFailure)
    {
        value = default(Person);
        return false;
    }
    // успешно распарсили строку в объект Person
    value = new Person() { /* инициализация свойств */ };
    return true;
}

В этом примере, если функция сталкивается с проблемой (например, входные данные не могут быть обработаны), она возвращает false, указывая на неудачу, и устанавливает выходной параметр value в значение по умолчанию для типа Person.

Задания на закрепление:

Задание 1: Напишите функцию, которая вычисляет факториал числа и протестируйте её с разными входными данными.

Задание 2: Напишите функцию, которая находит максимальное число в массиве. Реализуйте тестирование этой функции.

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

Задание 4: Напишите функцию, которая выбрасывает исключение, если переданное ей число отрицательное. Добавьте обработку этого исключения.

Задание 5: Создайте модульный тест для функции, которая проверяет, является ли строка палиндромом.

Задание 6: Реализуйте функцию, которая логирует все исключения в файл. Протестируйте её с различными типами исключений.

Задание 7: Напишите функцию, которая использует try-catch для безопасного деления чисел. Протестируйте деление на ноль.

Задание 8: Реализуйте функцию, которая проверяет наличие элемента в массиве (tester-doer pattern). Протестируйте её с разными входными данными.

Задание 9: Напишите функцию, которая пытается преобразовать строку в целое число (TryParse). Если преобразование не удается, функция должна возвращать значение по умолчанию.

Задание 10: Реализуйте цепочку вызовов функций и отслеживайте стек вызовов. Сгенерируйте исключение и посмотрите, как оно поднимается по стеку вызовов.

Решения
using System;
using System.IO;

class Program
{
    static void Main(string[] args)
    {
        // Задание 1: Вычисление факториала
        Console.WriteLine("Факториал 5: " + Factorial(5)); // Ожидается 120

        // Задание 2: Нахождение максимального числа в массиве
        int[] numbers = { 1, 5, 3, 9, 2 };
        Console.WriteLine("Максимальное число: " + FindMax(numbers)); // Ожидается 9

        // Задание 3: Горячая перезагрузка (предполагается работа в среде, поддерживающей эту функцию)
        Console.WriteLine("Результат сложения: " + Add(3, 4)); // Ожидается 7

        // Задание 4: Исключение для отрицательного числа
        try
        {
            CheckNegative(-5); // Должно выбросить исключение
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine("Ошибка: " + ex.Message);
        }

        // Задание 5: Проверка строки на палиндром
        Console.WriteLine("Строка 'level' палиндром: " + IsPalindrome("level")); // Ожидается true

        // Задание 6: Логирование исключений в файл
        try
        {
            ThrowException();
        }
        catch (Exception ex)
        {
            LogException(ex); // Логирование исключения в файл
        }

        // Задание 7: Безопасное деление с использованием try-catch
        Console.WriteLine("Результат деления: " + SafeDivide(10, 2)); // Ожидается 5
        Console.WriteLine("Результат деления: " + SafeDivide(10, 0)); // Должно выдать сообщение об ошибке

        // Задание 8: Проверка элемента в массиве (Tester-Doer pattern)
        int valueToFind = 5;
        if (Contains(numbers, valueToFind))
        {
            Console.WriteLine($"Массив содержит значение {valueToFind}");
        }
        else
        {
            Console.WriteLine($"Массив не содержит значение {valueToFind}");
        }

        // Задание 9: Преобразование строки в число (TryParse)
        int parsedNumber;
        if (TryParseNumber("123", out parsedNumber))
        {
            Console.WriteLine("Успешное преобразование: " + parsedNumber);
        }
        else
        {
            Console.WriteLine("Не удалось преобразовать строку в число.");
        }

        // Задание 10: Отслеживание стека вызовов
        try
        {
            Method1(); // Вызов цепочки методов
        }
        catch (Exception ex)
        {
            Console.WriteLine("Исключение перехвачено: " + ex.Message);
            Console.WriteLine("Стек вызовов: " + ex.StackTrace);
        }
    }

    // Решение для задания 1
    static int Factorial(int n)
    {
        if (n <= 1) return 1;
        return n * Factorial(n - 1);
    }

    // Решение для задания 2
    static int FindMax(int[] numbers)
    {
        int max = numbers[0];
        foreach (int num in numbers)
        {
            if (num > max) max = num;
        }
        return max;
    }

    // Решение для задания 3
    static int Add(int a, int b)
    {
        return a + b; // Возможно изменение во время горячей перезагрузки
    }

    // Решение для задания 4
    static void CheckNegative(int number)
    {
        if (number < 0)
            throw new ArgumentException("Число не должно быть отрицательным.");
    }

    // Решение для задания 5
    static bool IsPalindrome(string text)
    {
        int length = text.Length;
        for (int i = 0; i < length / 2; i++)
        {
            if (text[i] != text[length - 1 - i])
                return false;
        }
        return true;
    }

    // Решение для задания 6
    static void ThrowException()
    {
        throw new InvalidOperationException("Произошла ошибка.");
    }

    static void LogException(Exception ex)
    {
        File.AppendAllText("errors.log", $"{DateTime.Now}: {ex.Message}\n");
    }

    // Решение для задания 7
    static int SafeDivide(int a, int b)
    {
        try
        {
            return a / b;
        }
        catch (DivideByZeroException)
        {
            Console.WriteLine("Ошибка: Деление на ноль.");
            return 0;
        }
    }

    // Решение для задания 8
    static bool Contains(int[] array, int value)
    {
        foreach (int item in array)
        {
            if (item == value)
                return true;
        }
        return false;
    }

    // Решение для задания 9
    static bool TryParseNumber(string input, out int number)
    {
        if (int.TryParse(input, out number))
        {
            return true;
        }
        else
        {
            number = default;
            return false;
        }
    }

    // Решение для задания 10
    static void Method1()
    {
        Method2();
    }

    static void Method2()
    {
        Method3();
    }

    static void Method3()
    {
        throw new Exception("Ошибка в Method3."); // Генерация исключения
    }
}

 

Проектные задания

1. Преподаватель автошколы: Приложение для расчета итоговой оценки курсанта

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

Требования:

  • Функция для ввода оценок по теории и практике.
  • Функция для расчета итогового балла с учетом веса каждой составляющей (например, теория — 40%, практика — 50%, дополнительные баллы — 10%).
  • Функция для вывода итоговой оценки с комментариями (сдал/не сдал).

2. Сисадмин: Приложение для мониторинга ресурсов системы

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

Требования:

  • Функция для получения и вывода информации о доступном дисковом пространстве.
  • Функция для получения и вывода текущей загрузки процессора.
  • Функция для получения и вывода доступной оперативной памяти.
  • Функция для вывода предупреждений, если какой-либо из параметров превышает допустимый порог.

3. Повар: Приложение для управления рецептами

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

Требования:

  • Функция для добавления нового рецепта (с указанием названия блюда, ингредиентов, и пошаговой инструкции).
  • Функция для вывода списка всех рецептов.
  • Функция для поиска рецепта по ключевому ингредиенту.
  • Функция для редактирования и удаления рецептов.

4. Геолог: Приложение для расчета объема породы

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

Требования:

  • Функция для ввода параметров скважины (диаметр и глубина).
  • Функция для расчета объема цилиндра (скважины) на основе введенных параметров.
  • Функция для вывода результата и возможного запаса на усадку/расширение породы.

5. Медик: Приложение для расчета индекса массы тела (ИМТ)

Описание: Создайте консольное приложение, которое поможет медику рассчитать индекс массы тела (ИМТ) пациента. Приложение должно принимать вес и рост пациента и выдавать результат с соответствующими рекомендациями.

Требования:

  • Функция для ввода роста и веса пациента.
  • Функция для расчета ИМТ по формуле: ИМТ = вес (кг) / (рост (м) * рост (м)).
  • Функция для вывода результата и рекомендаций на основе рассчитанного ИМТ (нормальный вес, недостаток веса, избыточный вес и т.д.).
Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.