Различни грешки и finally
Проблем
Сметката за нула души
Програмата дели сметката в ресторанта по равно. Вече знаеш за грешния вход и го хващаш:
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 9catch е там, но не помага. Хваща само 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.");}Стъпка по стъпка, с думи
- Първо пускане: сметка 90 €, хората са 0.
- 90 / 0 е деление на нула: хвърля се DivideByZeroException.
- Първият catch е за FormatException. Не пасва, прескача се.
- Вторият пасва. Изпълнява се той и само той.
- Второ пускане: вместо брой хора е написано „трима“.
- int.Parse не може да направи число: хвърля се FormatException.
- Пасва още първият catch. До втория не се стига.
- Блоковете се проверяват отгоре надолу. Изпълнява се първият, който пасва.
По-точно, за любопитните
catchможе да има и условие:catch (FormatException) when (text == ""). Блокът хваща само ако условието е вярно.
Идеята
Семейството на изключенията
Изключенията са класове, свързани с наследяване. Ето най-честите:
Exception├── SystemException│ ├── ArithmeticException│ │ ├── DivideByZeroException│ │ └── OverflowException│ ├── FormatException│ ├── IndexOutOfRangeException│ ├── NullReferenceException│ ├── ArgumentException│ │ ├── ArgumentNullException│ │ └── ArgumentOutOfRangeException│ └── IOException файлове, мрежа└── ApplicationExceptioncatch хваща посочения вид и всичките му наследници. Това е полиморфизмът от модула за наследяването: променлива от родителски тип може да сочи обект на наследника. Затова 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("Обща грешка");}seats[3] е извън масива с 3 елемента. IndexOutOfRangeException не е ArithmeticException, затова първият блок се прескача. Той е наследник на SystemException, затова вторият отговаря и се изпълнява. До третия не се стига, макар че и той би отговарял.Пробвай
Намери грешката
Избери реда с грешката.
Exception е родител на DivideByZeroException. Първият catch хваща всичко, а вторият никога не може да се стигне. Компилаторът го казва: „CS0160: A previous catch clause already catches all exceptions of this or of a super type ('Exception')“. Правилото е от конкретното към общото: размени двата блока.Идеята
Компилаторът не задължава
В 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));return в try не прескача finally. Методът първо изпълнява finally, после връща текста. Затова „Записах опита.“ се отпечатва преди резултата, който WriteLine получава чак когато методът свърши.
Упражнение
Програми с няколко 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/484 / 4 = 2184/0Деление на нула.Още задачи
- Терминалът. Метод
Pay(string amountText)печата „Терминалът е свързан.“, после „Платени X €“ или съобщение за грешен вход, а въвfinally„Терминалът е изключен.“ Извикай го с „25“ и с „двайсет“. - Средна оценка без сривове. Прочети 5 реда с оценки. Пропусни тези, които не са цели числа (изключение) или не са от 2 до 6 (
if). Отпечатай средното с целочислено деление. Ако нито една оценка не е вярна, деленето на нула хвърля: хвани го и отпечатай „Няма верни оценки.“ - Предвиди и провери. Напиши
tryсcatchиfinally, в който всеки блок печата по ред. Предвиди изхода при успех, при хванато и при нехванато изключение и после го пусни.
Тест