Към съдържанието
wannacode

Когато нещо се обърка

Проблем

12а вместо 12

Касата за концерта пита колко билета искаш. Билетът е 15 €.

Program.cs
class Program
{
static void Main()
{
Console.Write("Колко билета? ");
int tickets = int.Parse(Console.ReadLine());
Console.WriteLine("Общо: " + tickets * 15 + " €");
}
}

С вход 12 програмата казва „Общо: 180 €“. Но клиентът бърза и натиска една буква повече:

Конзолата
Колко билета? 12а
Unhandled exception. System.FormatException: The input string '12а' was not in a correct format.
at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
at System.Int32.Parse(String s)
at Program.Main() in /projects/Tickets/Program.cs:line 6

Програмата спира. Вместо сумата излиза дълъг текст, а редът „Общо“ изобщо не се стига. Какво значи този текст и как да не спира програмата заради една буква?

Идеята

Изключение

int.Parse трябва да върне число. От „12а“ не може да направи число, а да върне каквото и да е число би било лъжа. Затова методът създава обект, който описва проблема, и го хвърля. Този обект се казва изключение (exception).

Ако никой не го хване, програмата спира и показва какво е станало. Този текст се казва stack trace и се чете отгоре надолу:

  • Първият ред: видът на изключението и съобщението. FormatException: The input string '12а' was not in a correct format.: грешен формат на числото, ето кой текст.
  • Редовете с „at“: къде е станало. Това е стекът от извиквания от урока за рекурсията, отвътре навън. Най-горе е методът, който е хвърлил изключението, а най-долу е Main.
  • Твоят ред: първият ред „at“, който сочи твоя файл. Тук: Program.cs:line 6. Редовете над него са в кода на .NET и рядко ти трябват.

Съобщенията са на английски, защото ги пише езикът, не ти.

Пробвай

Намери грешката

Програмата трябва да отпечата последния резултат от контролното. Тя спира с това:

Грешка
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
at Program.LastScore(Int32[] scores) in /projects/Tickets/Program.cs:line 5
at Program.Main() in /projects/Tickets/Program.cs:line 11

Прочети stack trace-а. На кой ред от програмата е грешката?

Избери реда с грешката.

Идеята

try и catch

Изключението може да се хване. Кодът, който може да хвърли, отива в try („опитай“). Какво да стане при грешка, пишеш в catch („хвани“):

Console.Write("Колко билета? ");
try
{
int tickets = int.Parse(Console.ReadLine());
Console.WriteLine("Общо: " + tickets * 15 + " €");
}
catch (FormatException)
{
Console.WriteLine("Това не е цяло число.");
}
Console.WriteLine("Благодарим ти!");
Вход
12а
Изход
Колко билета? Това не е цяло число.
Благодарим ти!
  • В скобите на catch пишеш кой вид изключение хващаш.
  • Ако в try всичко мине, catch се прескача. С вход 12 изходът е „Общо: 180 €“ и „Благодарим ти!“.
  • След catch програмата продължава нататък. Тя вече не спира.

Console.Write("Колко билета? ");
try
{
int tickets = int.Parse(Console.ReadLine());
Console.WriteLine("Общо: " + tickets * 15 + " €");
}
catch (FormatException)
{
Console.WriteLine("Това не е цяло число.");
}
Console.WriteLine("Благодарим ти!");
Стъпка по стъпка, с думи
  1. Програмата пита колко билета. Въвеждаш „12а“.
  2. В try: int.Parse опитва да направи число от „12а“.
  3. Не може. Методът хвърля FormatException.
  4. Изпълнението скача направо в catch. Редът с „Общо“ се прескача.
  5. catch е за точно този вид грешка: печата съобщението за клиента.
  6. След catch програмата продължава. Не спира.
По-точно, за любопитните
  • Променлива, обявена в try (тук tickets), съществува само до затварящата скоба на try. Ако ти трябва и след блока, обяви я преди try.
  • В C# можеш да не дадеш име на изключението: catch (FormatException). Така е, когато не ти трябва. Ако напишеш име и не го ползваш, компилаторът дава предупреждение.

Пробвай

Кое се изпълнява?

Какво ще отпечата програмата?

Console.WriteLine("Начало");
try
{
Console.WriteLine("Чета");
int seats = int.Parse("десет");
Console.WriteLine("Прочетох " + seats);
}
catch (FormatException)
{
Console.WriteLine("Грешен вход");
}
Console.WriteLine("Край");
Отговор

Идеята

Какво пише в изключението

Изключението е обект, а e в catch (FormatException e) е променливата, която сочи към него. Обектът има метод за съобщението: e.Message.

try
{
int tickets = int.Parse(Console.ReadLine());
Console.WriteLine("Общо: " + tickets * 15 + " €");
}
catch (FormatException e)
{
Console.WriteLine("Грешка: " + e.Message);
}
Вход
12а
Изход
Грешка: The input string '12а' was not in a correct format.

Съобщението е полезно за теб, когато търсиш грешката. На клиента обикновено показваш свой текст на неговия език, като „Това не е цяло число.“

Идеята

Питай, докато е вярно

Клиентът сбърка. Не е нужно да спираш. Питай отново:

int tickets;
while (true)
{
Console.Write("Колко билета? ");
try
{
tickets = int.Parse(Console.ReadLine());
break;
}
catch (FormatException)
{
Console.WriteLine("Въведи цяло число, например 3.");
}
}
Console.WriteLine("Общо: " + tickets * 15 + " €");
Вход
12а
дванайсет
12
Изход
Колко билета? Въведи цяло число, например 3.
Колко билета? Въведи цяло число, например 3.
Колко билета? Общо: 180 €

while (true) се върти, докато break не го прекъсне. А break се стига само ако Parse е успял. При грешка изпълнението скача в catch, след него цикълът започва отначало и пита пак.

По-точно, за любопитните
  • Когато грешният вход е нещо нормално, в C# има по-удобен начин: int.TryParse. Той не хвърля изключение, а връща true или false, и записва числото в out параметъра от урока за параметрите:
int tickets;
Console.Write("Колко билета? ");
while (!int.TryParse(Console.ReadLine(), out tickets))
{
Console.WriteLine("Въведи цяло число, например 3.");
Console.Write("Колко билета? ");
}
Console.WriteLine("Общо: " + tickets * 15 + " €");

Изключенията са за ситуации, които не очакваш често. За проверка на вход TryParse е по-кратък и по-бърз.

Пробвай

Деление на нула

Не всяко изключение идва от входа. Средната стойност на поръчка е приходът, делен на броя поръчки. Но в първия ден поръчки няма. С цели числа revenue / orders спира програмата:

Грешка
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.

А с дробни числа?

Какво ще отпечата програмата?

int revenue = 90;
int orders = 0;
double average = (double)revenue / orders;
Console.WriteLine(average);
Отговор
По-точно, за любопитните
  • Тук изходът е Infinity. На компютър с регионални настройки, например български или американски, C# печата знака ∞.
  • Целите числа нямат стойност за безкрайност, затова там остава само изключението. При дробните числа правилата идват от стандарта за дробни числа IEEE 754, който всички езици следват.

Упражнение

Програми, които не спират

Реши задачите в редактора си. Не печатай въпроси, за да съвпадне изходът ти с примерния.

1. Текст към число. Напиши метод ToNumber(string text), който връща текста като double. Прочети два реда. За всеки отпечатай числото или съобщение, че не е число. Хвани изключението в Main.

Вход
123.45
123.abc
Изход
Числото е: 123.45
Грешка: 123.abc не е число.

На компютър с български настройки double.Parse иска запетая: 123,45. Виж урока „Вход и изход“.

2. Няма такъв продукт. Цените са в масив { 120, 75, 300, 45, 89 }. Прочети номер на продукт от 1 до 5 и отпечатай цената му. Ако номерът е извън масива, хвани изключението и го кажи. С вход 2 изходът е Цена: 75 €.

Вход
7
Изход
Няма продукт с номер 7.

3. Средна поръчка. Прочети прихода и броя поръчки (цели числа) и отпечатай средното с целочислено деление. Ако поръчки няма, хвани изключението. С вход 900 и 4 изходът е Средно: 225 €.

Вход
90
0
Изход
Още няма поръчки.

Още задачи

  • Възраст, докато е вярна. Питай за възраст, докато входът не е цяло число от 0 до 120. За текст като „двайсет“ кажи „Това не е число.“, а за 150 кажи „Възрастта е от 0 до 120.“ Едното е изключение, другото е if.
  • Сума на верните. Прочети 5 реда. Събери тези, които са цели числа, и преброй пропуснатите: при вход 10, abc, 5, 7.5, 3 изходът е Сума: 18, пропуснати: 2.
  • Деление по желание. Прочети две цели числа и отпечатай частното. При текст вместо число и при деление на нула програмата не спира, а пита отново за двете числа.
  • Без изключение. Реши задача 1 с double.TryParse вместо с try/catch.

Тест

Провери се

1. Какво е изключение?
2. Програмата спря с дълъг текст за грешка. Откъде да започнеш да четеш?
3. В try има четири реда. Вторият хвърля изключение, а catch го хваща. Какво става с третия и четвъртия?
4. Какво става при int x = 90 / orders; при orders = 0?
5. catch (FormatException e) хваща ли грешката от scores[10] в масив с 5 елемента?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·