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

Различни грешки и finally

Проблем

Сметката за нула души

Програмата дели сметката в ресторанта по равно. Вече знаеш за грешния вход и го хващаш:

Program.cs
class Program
{
static void Main()
{
try
{
int bill = int.Parse(Console.ReadLine());
int people = int.Parse(Console.ReadLine());
Console.WriteLine("На човек: " + bill / people + " €");
}
catch (FormatException)
{
Console.WriteLine("Въведи цели числа.");
}
}
}

Сметката е 90 €, а някой въвежда 0 за броя на хората. Програмата пак спира:

Грешка
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
at Program.Main() in /projects/Bill/Program.cs:line 9

catch е там, но не помага. Хваща само FormatException, а това е друг вид изключение.

Идеята

Няколко catch

След един try може да има няколко catch, по един за всеки вид грешка:

try
{
int bill = int.Parse(Console.ReadLine());
int people = int.Parse(Console.ReadLine());
Console.WriteLine("На човек: " + bill / people + " €");
}
catch (FormatException)
{
Console.WriteLine("Въведи цели числа.");
}
catch (DivideByZeroException)
{
Console.WriteLine("Хората не може да са 0.");
}
Вход Изход
90 и 3 На човек: 30 €
90 и 0 Хората не може да са 0.
90 и трима Въведи цели числа.

Блоковете се проверяват отгоре надолу. Изпълнява се първият, чийто вид отговаря, и само той. Останалите се прескачат.

try
{
int bill = int.Parse(Console.ReadLine());
int people = int.Parse(Console.ReadLine());
Console.WriteLine("На човек: " + bill / people + " €");
}
catch (FormatException)
{
Console.WriteLine("Въведи цели числа.");
}
catch (DivideByZeroException)
{
Console.WriteLine("Хората не може да са 0.");
}
Стъпка по стъпка, с думи
  1. Първо пускане: сметка 90 €, хората са 0.
  2. 90 / 0 е деление на нула: хвърля се DivideByZeroException.
  3. Първият catch е за FormatException. Не пасва, прескача се.
  4. Вторият пасва. Изпълнява се той и само той.
  5. Второ пускане: вместо брой хора е написано „трима“.
  6. int.Parse не може да направи число: хвърля се FormatException.
  7. Пасва още първият catch. До втория не се стига.
  8. Блоковете се проверяват отгоре надолу. Изпълнява се първият, който пасва.
По-точно, за любопитните
  • catch може да има и условие: catch (FormatException) when (text == ""). Блокът хваща само ако условието е вярно.

Идеята

Семейството на изключенията

Изключенията са класове, свързани с наследяване. Ето най-честите:

Йерархията
Exception
├── SystemException
│ ├── ArithmeticException
│ │ ├── DivideByZeroException
│ │ └── OverflowException
│ ├── FormatException
│ ├── IndexOutOfRangeException
│ ├── NullReferenceException
│ ├── ArgumentException
│ │ ├── ArgumentNullException
│ │ └── ArgumentOutOfRangeException
│ └── IOException файлове, мрежа
└── ApplicationException

catch хваща посочения вид и всичките му наследници. Това е полиморфизмът от модула за наследяването: променлива от родителски тип може да сочи обект на наследника. Затова catch (Exception e) хваща всичко:

static void Split(string billText, string peopleText)
{
try
{
int bill = int.Parse(billText);
int people = int.Parse(peopleText);
Console.WriteLine("На човек: " + bill / people + " €");
}
catch (Exception e)
{
Console.WriteLine(e.GetType().Name + ": " + e.Message);
}
}
Split("90", "3");
Split("90", "0");
Split("90", "трима");
Изход
На човек: 30 €
DivideByZeroException: Attempted to divide by zero.
FormatException: The input string 'трима' was not in a correct format.

e.GetType().Name дава името на истинския клас на обекта. Променливата е от тип Exception, но обектът е от наследник.

По-точно, за любопитните
  • StackOverflowException не може да се хване изобщо: .NET спира програмата веднага. OutOfMemoryException може, но рядко има смисъл.
  • ApplicationException е бил замислен като родител на изключенията на програмите. На практика свои изключения се правят направо от Exception.
  • Хващай толкова конкретно, колкото можеш. catch (Exception e) скрива и грешки, които не си очаквал, например бъг в собствения ти код.

Пробвай

Кой catch?

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

int[] seats = { 4, 2, 6 };
try
{
Console.WriteLine("Маса 4: " + seats[3] + " места");
}
catch (ArithmeticException)
{
Console.WriteLine("Деление на нула");
}
catch (SystemException)
{
Console.WriteLine("Грешка при изпълнение");
}
catch (Exception)
{
Console.WriteLine("Обща грешка");
}
Отговор

Пробвай

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

Тази програма не се компилира. На кой ред е грешката?

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

Идеята

Компилаторът не задължава

В Java има проверени изключения: компилаторът не приема програмата, докато не ги хванеш или не ги обявиш. В C# няма такива. Нито едно изключение не е задължително за хващане:

static void PrintReceipt()
{
Console.WriteLine("Печатам бележката...");
Thread.Sleep(300);
Console.WriteLine("Готово.");
}
PrintReceipt();
Изход
Печатам бележката...
Готово.

Thread.Sleep спира програмата за 300 милисекунди и може да хвърли изключение, но компилаторът не иска нищо. Същото е за int.Parse.

Затова отговорността е твоя. Кои изключения хвърля един метод, пише в документацията му, в раздела „Exceptions“. Прочети го, преди да решиш дали да хващаш.

Идеята

finally: винаги

Принтерът на касата трябва да се освободи след всяка бележка, иначе следващата няма да се отпечата. Каквото и да стане, успех или грешка:

static void PrintReceipt(string qtyText)
{
Console.WriteLine("Принтерът е зает.");
try
{
int qty = int.Parse(qtyText);
Console.WriteLine("Кафе: " + qty + " x 2 € = " + qty * 2 + " €");
}
catch (FormatException)
{
Console.WriteLine("Грешно количество: " + qtyText);
}
finally
{
Console.WriteLine("Принтерът е свободен.");
}
}
PrintReceipt("3");
PrintReceipt("три");
Изход
Принтерът е зает.
Кафе: 3 x 2 € = 6 €
Принтерът е свободен.
Принтерът е зает.
Грешно количество: три
Принтерът е свободен.

finally се изпълнява винаги: когато всичко мине, когато изключението е хванато и дори когато нито един от тези catch не го хваща. В последния случай finally се изпълнява, а изключението продължава нагоре към следващия try, който може да го хване.

Защо не просто ред след try/catch? Защото той се пропуска, ако изключението е от вид, който catch не хваща, или ако в try има return. finally не се пропуска.

По-точно, за любопитните
  • Може и try с finally, без catch: изключението не се хваща, но почистването се изпълнява.
  • За ресурси, които трябва да се затварят, като файлове и връзки с база данни, има по-кратък запис: using. Ще го срещнеш при работа с файлове и бази данни. Той сам затваря ресурса, както би го затворил finally.
  • finally не се изпълнява само ако програмата бъде спряна изцяло, например с Environment.Exit(0).
  • Ако изключението не се хваща никъде в програмата, .NET може да я спре, без да изпълни finally блоковете. Затова хващай изключенията поне в Main, ако в finally има нещо важно.

Пробвай

finally и return

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

static string Reserve(int guests)
{
try
{
if (guests > 6)
{
return "Няма маса за " + guests;
}
return "Маса за " + guests;
}
finally
{
Console.WriteLine("Записах опита.");
}
}
Console.WriteLine(Reserve(8));
Отговор

Упражнение

Програми с няколко catch

Реши задачите в редактора си. Не печатай въпроси.

1. Цена на човек. Цените са в масив { 120, 75, 300 }. Прочети номер на продукт (от 1) и броя на хората и отпечатай цената на човек с целочислено деление. Хвани три неща: несъществуващ продукт, нула души и всичко останало (тогава отпечатай съобщението на изключението).

Вход Изход
2 и 3 На човек: 25 €
5 и 2 Няма такъв продукт.
1 и 0 Броят на хората не може да е 0.
1 и двама Грешка: The input string 'двама' was not in a correct format.

2. Калкулатор. Прочети цяло число, оператор (+, -, * или /) и второ цяло число, всяко на свой ред. Отпечатай сметката. При текст вместо число кажи „Въведи цели числа.“, при деление на нула „Деление на нула.“, а при друг оператор „Непознат оператор: “ и оператора.

Вход
84
/
4
Изход
84 / 4 = 21
Вход
84
/
0
Изход
Деление на нула.

Още задачи

  • Терминалът. Метод Pay(string amountText) печата „Терминалът е свързан.“, после „Платени X €“ или съобщение за грешен вход, а във finally „Терминалът е изключен.“ Извикай го с „25“ и с „двайсет“.
  • Средна оценка без сривове. Прочети 5 реда с оценки. Пропусни тези, които не са цели числа (изключение) или не са от 2 до 6 (if). Отпечатай средното с целочислено деление. Ако нито една оценка не е вярна, деленето на нула хвърля: хвани го и отпечатай „Няма верни оценки.“
  • Предвиди и провери. Напиши try с catch и finally, в който всеки блок печата по ред. Предвиди изхода при успех, при хванато и при нехванато изключение и после го пусни.

Тест

Провери се

1. Изключението отговаря на два от трите catch блока. Кой се изпълнява?
2. Хваща ли catch (SystemException e) изключение FormatException?
3. Защо catch (Exception e) трябва да е последен?
4. Кога се изпълнява finally?
5. Компилаторът на C# задължава ли те да хванеш FormatException около int.Parse?

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