Vead
Objekt-orienteeritud keeltes ei kasutata tavaliselt vigade esitamiseks numbrilisi koode nagu keeltes, kus objektid puuduvad. Objekt-orienteeritud keeltes on olemas spetsiaalsed vigade objektid, mille nimetuseks on erind (exception
).
Millest selline nimetus, mis viitab millelegi erilisele? Tarkvara disainimise hea tava on see, et tarkvara on mõõdukalt lollikindel ja selle pangestamine lõpp-kasutaja poolt ei ole võimalik. See tähendab seda, et programm suudab kontrollida oma sisendeid ja väljundeid ning käituda kasutajale arusaadavalt. Kuid on eriolukordi, millega me arvestada ei oska või ei arvesta praktilistel põhjustel. Sellistel juhtudel tekibki programmis erandlik olukord, mida väljendab viga ehk erind ehk exception
.
Näiteid erandlikest olukordadest:
- arvutil napib vaba mälu või kettapinda
- riistvaraline tõrge (kõvaketas ilmutab lagunemise märke)
- mõni väline komponent, mida kasutame, satub ootamatusse veaolukorda
Exception klass
Kõige üldisem erindite klass on Exception. See on baasklassiks kõigile teistele erinditele. Tegemist on küllaltki lihtsa klassiga, mille olulisemad omadused on järgmised:
- Message - veateade, mida kuvada kasutajale või mis selgitab vea olemust lähemalt
- StackTrace - teekond vea tekkekohast sinnani, kus viga kinni püüti
- InnerException - kas veaga koos anti veel mõni sellega seonduv viga
Vigade lugemine on oluline oskus
Oskus lugeda vigu ning leida nende tekkekoht koodis on väga oluline. Veateated ja stack trace
sisaldavad olulist infot nii vea tekkekoha kui olemuse kohta. Vigade klassidel on tihti täiendavaid omadusi, mis veast veel paremini aru aitavad saada.
.NET raamistikus on veel kaks olulist erindit, mida kasutatakse baasklassidena ja mis mõlevad pärivad Exception
klassist:
- SystemException - seda kasutavad oma vigade baasklassina .NET raamistiku koosseisu kuuluvad klassid (meie kirjutatud koodis peaks selle klassi nimi esinema ainult erijuhtudel)
- ApplicationException - see on mõeldud nende erindite baasklassiks, mis on meie rakenduste koosseisus
Praktika töömailt
Kuigi ametlik soovitus ütleb, et meie vigade baasklassiks peaks olema ApplicationException
või kui me enda erindit ei tee, siis peaksime vähemasti looma ApplicationException
tüüpi vea, hiilitakse sellest praktikas pahatihti mööda ja kasutatakse otse Exception
klassi.
Vigade tekitamine
Et koodis viga anda, peame välja valima vea tüübi. Kui sobivat tüüpi ei ole, siis võime selle ise luua või võtta ApplicationException
aluseks. Vea andmiseks peame looma veaobjekti ning kasutama throw
märksõna, et viga liikuma panna. Kõige lihtsamal juhul näeb vea andmine välja selline.
throw new ApplicationException("My custom exception");
.NET raamistik pakub mitmeid valmis vigade klasse, mida me võime ka oma koodis kasutada.
ArgumentNullException
ArgumentNullException
on viga juhuks kui mõni meetodi või konstruktori parameetritest on null, mis seda olla ei tohi.
public class FileLogger
{
private readonly FileStream _logStream;
private readonly StreamWriter _logWriter;
public FileLogger(string filePath)
{
if(filePath == null)
{
throw new ArgumentNullException("filePath");
}
}
// Logimise meetodid järgnevad
}
var logger = new FileLogger(null);
using System;
using System.IO;
namespace OOP
{
public class FileLogger
{
private readonly FileStream _logStream;
private readonly StreamWriter _logWriter;
public FileLogger(string filePath)
{
if (filePath == null)
{
throw new ArgumentNullException("filePath");
}
}
// Logimise meetodid järgnevad
}
class Program
{
static void Main()
{
var logger = new FileLogger(null);
}
}
}
InvalidOperationException
InvalidOperationException
on mõeldud juhtudeks kui kutsutakse klassi mõnda meetodi, kuid see pole antud hetkel või antud oludes lubatud.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CustomersProvider
{
private bool _dataIsLoaded;
private IList<Customer> _customers;
public IList<Customer> Customers
{
get
{
if (!_dataIsLoaded)
{
throw new InvalidOperationException("Data is not loaded");
}
return _customers;
}
}
public void LoadData()
{
_dataIsLoaded = true;
// Load customers data
}
}
var provider = new CustomersProvider();
var customers = provider.Customers;
using System;
using System.Collections.Generic;
namespace OOP
{
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CustomersProvider
{
private bool _dataIsLoaded;
private IList<Customer> _customers;
public IList<Customer> Customers
{
get
{
if (!_dataIsLoaded)
{
throw new InvalidOperationException("Data is not loaded");
}
return _customers;
}
}
public void LoadData()
{
_dataIsLoaded = true;
// Load customers data
}
}
class Program
{
static void Main()
{
var provider = new CustomersProvider();
var customers = provider.Customers;
}
}
}
Tulemuseks on järgnev viga:
Unhandled exception. System.InvalidOperationException: Data is not loaded
at OOP.CustomersProvider.get_Customers() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 23
at OOP.Program.Main() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 43
NotImplementedException
NotImplementedException
on mõeldud arendajate omavaheliseks kasutuseks. Selle vea annavad meetodid ja omadused, mis on klassil juba küljes, kuid milles päris funktsionaalsust veel pole. Selle vea abil saame teavitada teisi arendajaid, et üks või teine osa klassist pole veel valmis.
Näite huvides võtame jälle käsile ILogger liidese ja mängime, et meil on pooleli varasemast tuttava DebugLoggeri kirjutamine. Et DebugLogger klassil on ILogger liides küljes, siis peavad DebugLoggeril olema kõik meetodid, mida liides nõuab. Ilma nendeta meil kompileerimine ei õnnestuks. Debug() ja Info() meetodid on meil valmis, kuid Warn() ja Error() meetoditeni me pole veel jõudnud ja nende kahe kasutamise tulemuseks on NotImplementException
.
public interface ILogger
{
void Debug(string message);
void Info(string message);
void Warn(string message);
void Error(string message);
}
public class DebugLogger : ILogger
{
public void Debug(string message)
{
System.Diagnostics.Debug.WriteLine("DEBUG: " + message);
}
public void Info(string message)
{
System.Diagnostics.Debug.WriteLine("INFO: ", message);
}
public void Warn(string message)
{
throw new NotImplementedException();
}
public void Error(string message)
{
throw new NotImplementedException();
}
}
var logger = new DebugLogger();
logger.Info("Just an informative message");
logger.Warn("This is warning");
using System;
namespace OOP
{
public interface ILogger
{
void Debug(string message);
void Info(string message);
void Warn(string message);
void Error(string message);
}
public class DebugLogger : ILogger
{
public void Debug(string message)
{
System.Diagnostics.Debug.WriteLine("DEBUG: " + message);
}
public void Info(string message)
{
System.Diagnostics.Debug.WriteLine("INFO: ", message);
}
public void Warn(string message)
{
throw new NotImplementedException();
}
public void Error(string message)
{
throw new NotImplementedException();
}
}
class Program
{
static void Main()
{
var logger = new DebugLogger();
logger.Info("Just an informative message");
logger.Warn("This is warning");
}
}
}
FileNotFoundException
FileNotFoundException
vist lähemat selgitamist ei vaja. Tegemist on veaga, mis antakse siis kui vajaminevat faili ei leitud. Kõige lihtsam viis selle vea saamiseks on proovida avada faili, mida ei eksisteeri.
new FileStream("fail-mida-ei-ole.txt");
Selle sama vea võib anda ka mõni meie enda klass, millel on vaja mõnda kindlat faili, mis parajasti saadaval pole.
public class InvoiceImporter
{
public InvoiceImporter(string filePath)
{
if(!File.Exists(filePath))
{
throw new FileNotFoundException(null, nameof(filePath));
}
}
// Muud importimise meetodid
}
var importer = new InvoiceImporter("sellist-faili-pole.txt");
using System.IO;
namespace OOP
{
public class InvoiceImporter
{
public InvoiceImporter(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException(null, nameof(filePath));
}
}
// Muud importimise meetodid
}
class Program
{
static void Main()
{
var importer = new InvoiceImporter("sellist-faili-pole.txt");
}
}
}
Stack trace
Stack trace
on vea teekond läbi meetodite ja omaduste kuni kohani, kus viga tekkis. Oskus seda lugeda on objekt-orienteeritud programmeerimises täiesti elementaarne. Stack trace
abil saame me teada täpse teekonna alates programmi algusest kuni vea tekkekohani.
Vaatame näidet, kus viga tekib paar meetodit kaugemal programmi algusest.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Tulemuseks on järgnev stack trace:
at OOP.Program.Method2() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 19
at OOP.Program.Method1() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 14
at OOP.Program.Main() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 9
Stack trace annab meile kogu läbitud meetodite ja omaduste ahela alates neetodist või omadusest, milles viga tekkis. Näites oli throw
märksõna kasutatud real 19. Seega on see stack trace
esimene rida ja see rida tekkis meetodis Method2(). Kõik see info on olemas stack trace
esimesel real. Teisel real on meetod või omadus, mis käesolevat kutsus. Jällegi on kirjas meetodi või omaduse nimi ja selle rea number, kus eelmisel real toodud meetodi või omadust kutsuti. Selliselt jätkub stack trace
kuni programmi alguseni.
Probleem: Viga valel real
Kasutajatele anname me tavaliselt oma programmi release buildi
. Release build
korral lastakse kompilaatoril kompileerimise käigus optimeerida koodi, et see saaks efektiivsem, kiirem ja nõuaks vähem ressurse. Sellel võib olla ka kõrvalnähtusid.
Vaatleme järgnevat koodi. See on sarnane eelnevale, kuid on mõned erinevused. Method2() asemel on meil WriteStringLength() meetod, mis kirjutab antud stringi pikkuse konsoolile. Method1() defineerib string s, annab sellele väärtuseks nulli ja kutsub seejärel WriteStringLength() meetodit. Main() meetod on nagu enne - selles kustutakse välja Method1();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Kui me laseme selle programmi käima Debug buildiga
, siis kompilaator optimeerimisi läbi ei vii. Tulemuseks on järgnev stack trace
(jälgi hoolikalt ridade numbreid):
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at OOP.Program.WriteStringLenth(String s) in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 21
at OOP.Program.Method1() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 16
at OOP.Program.Main() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 9
Nüüd proovime järgi Release buildi
. Kompileerime programmi ja laseme selle käima. Ridade numbrid enam ei klapi nagu Debug build
puhul:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at OOP.Program.WriteStringLenth(String s) in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 21
at OOP.Program.Method1() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 14
at OOP.Program.Main() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 9
Kui enne oli Method1() korral probleem real 16, siis nüüd äkki on see real 14. Rida 14 on aga stringi definitsioon.
Mis juhtus? Release build
korral optimeerib kompilaator meie koodi ja rida 14 stringi definitsiooniga on üleliigne. Me omistame stringile väärtuseks konstandi ja konstandi hoidmiseks eraldi muutujat vaja ei ole. Kompilaator toodab meile veidi teistsuguse Method1().
static void Method1()
{
WriteStringLenth((string)null);
}
Arvestades sellega, et see tühi rida kaob ka stringi definitsiooni ja WriteStringLength() kutsumise vahelt ära, siis liigub kood meil kaks rida ülespoole. Seega WriteStringLength() meetodit kutsutakse real 14.
Ei juhtu tihti
Praktikas seda reanumbrite probleemi küll aeg-ajalt esineb, kuid mitte väga tihti. Tavaliselt on põhjuseks veidi ebaefektiivne kood nagu meiegi näites. Taolise koodi parandamisel saame hiljem ka õiged tulemused.
Probleem: Meetod on puudu
Veel radikaalsem näide tuleb koodi optimeerimise vallast. Kuigi .NET raamistik oskab programmi käivitades seda eelnevalt käigu pealt optimeerida, saame me ka ise seda protsessi mõjutada.
Lisame WriteStringLength() meetodile sellise atribuudi nagu MethodImpl
ja ütleme, et me soovime programmi käivitumisel sellist optimeerimist nagu method inlining
. Method inlining
tähendab seda, et kõik kohad koodis kus meetodi kutsutakse, asendatakse vastava meetodi enda koodiga.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Kui käivitame programmi on tulemuseks järgev stack trace
:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at OOP.Program.Method1() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 17
at OOP.Program.Main() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 10
WriteStringLength() meetod puudub nagu poleks seda olemas. Kuna me käskisime .NET raamistiku käitusmootoril asendada WriteStringLenth() meetodi kutsumised selle meetodi enda koodiga, siis on programmi käivitumisel real 17 selline kood:
Console.WriteLine(((string)null).Length);
WriteStringLength() meetod on kompileeritud koodis täiesti olemas ning see pole sealt kuhugi kadunud.
Vigade käsitlemine
Vigade käsitlemiseks kasutatakse try-catch
plokki.
try
{
string s = null;
var i = s.Length;
}
catch
{
// See kuulus veateade on maailmas väljavihastanud
// miljoneid tavakasutajaid
Console.WriteLine("There was an error in program!");
}
using System;
namespace OOP
{
class Program
{
static void Main()
{
try
{
string s = null;
var i = s.Length;
}
catch
{
// See kuulus veateade on maailmas väljavihastanud
// miljoneid tavakasutajaid
Console.WriteLine("There was an error in program!");
}
}
}
}
Me võime veaobjekti tuua catch
plokki selliselt:
try
{
string s = null;
var i = s.Length;
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
using System;
namespace OOP
{
class Program
{
static void Main()
{
try
{
string s = null;
var i = s.Length;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
Lisaks catch
plokile võime kasutada ka finally
plokki, mis lastakse alati käima - pole vahet kas try
plokis tekkis viga ja satuti catch
plokki või ei.
try
{
string s = null;
var i = s.Length;
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("Finally ...");
}
using System;
namespace OOP
{
class Program
{
static void Main()
{
try
{
string s = null;
var i = s.Length;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("Finally ...");
}
}
}
}
On üks erijuhtum, mille korral finally
plokk ei käivitu - kui catch
plokis tekib viga või kui catch
plokis kinni püütud viga uuesti teele põrgatatakse.
try
{
string s = null;
var i = s.Length;
}
catch(Exception ex)
{
Console.WriteLine(ex);
throw ex;
}
finally
{
Console.WriteLine("Finally ...");
}
using System;
namespace OOP
{
class Program
{
static void Main()
{
try
{
string s = null;
var i = s.Length;
}
catch (Exception ex)
{
Console.WriteLine(ex);
throw ex;
}
finally
{
Console.WriteLine("Finally ...");
}
}
}
}
Mitme veatüübi käsitlemine
try-catch
võib sisaldada ka mitut catch
plokki. Kui meil on veatüüpe, mille puhul me tahame midagi teisiti teha, siis saame nende jaoks kirjutada eraldi catch
plokid.
try
{
new FileStream("fail-mida-pole.txt", FileMode.Open);
}
catch (FileNotFoundException fnfex)
{
Console.WriteLine("File was not found: " + fnfex.FileName);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("Finally ...");
}
using System;
using System.IO;
namespace OOP
{
class Program
{
static void Main()
{
try
{
new FileStream("fail-mida-pole.txt", FileMode.Open);
}
catch (FileNotFoundException fnfex)
{
Console.WriteLine("File was not found: " + fnfex.FileName);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("Finally ...");
}
}
}
}
Sisemised vead
Nagu peatüki alguses sai mainitud, siis vigade sees võib olla veel vigu, need on nö. sisemised vead (inner exception
).
Oletame, et meil on kasutusel mõni valmis komponent, mis kasutab andmebaasi. Andmebaasi poole pöördumisel tekib viga, sest andmebaasi ei saa kätte parajasti. Esimese vea annab meile mõni andmebaasiga suhtlevatest komponentidest. Valmis komponent, mida meie kasutame, tahab anda oma enda vea, mis annab meile infot selle kohta, kus kohas ja mis kontekstis tekkis viga välises komponendis. Lõpuks võib ka meie kood tahta anda oma vea, kuid selliselt, et kõik eelnevad vead jäävad alles.
using System;
namespace OOP
{
class Program
{
static void Main()
{
Method1();
}
static void Method1()
{
string s = null;
try
{
WriteStringLenth(s);
}
catch(Exception ex)
{
throw new ApplicationException("Cannot find string length", ex);
}
}
static void WriteStringLenth(string s)
{
Console.WriteLine(s.Length);
}
}
}
Tulemuseks on järgnev stack trace
:
Unhandled exception. System.ApplicationException: Cannot find string length
---> System.NullReferenceException: Object reference not set to an instance of an object.
at OOP.Program.WriteStringLenth(String s) in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 28
at OOP.Program.Method1() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 18
--- End of inner exception stack trace ---
at OOP.Program.Method1() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 22
at OOP.Program.Main() in C:\projects\kustutamaha\OOP\OOP\Program.cs:line 9
.NET raamistik kirjutab meile nüüd välja ka sisemise vea, mis tekkis. WriteStringLength meetodi tulemus oli NullReferenceException
. Method1() püüdis selle vea kinni ja tegi uue ApplicationException
tüüpi vea, millesse eelnev kaasa pisteti. Seejärel andis Method()1 veaks ApplicationException
, mis catch
plokis loodi.
Jälgi sisemisi vigu!
Kui programm annab vigu, siis alati kontrolli kas tekkinud viga sisaldab sisemist viga või ei. Arvesta, et sisemisel veal võib olla oma sisemine viga. Kui on sisemisi vigu, siis nende abil saame tavaliselt jälile probleemi põhjusele. Mida kaugemale mööda sisemisi vigu liigume, seda lähemale jõuame vea algpõhjusele.
Vigade mõju jõudlusele
Oma olemuselt on vead kallid objektid. Nad nõuavad nii protsessorit kui mälu. Iseäranis hakkab see probleem silma kui vigu tekib tsüklite sees.
Kirjutame programmi, mis täidab stringide loendi stringidega ja nullidega. Loendisse lisame miljon elementi. Seejärel püüame leida stringide pikkused ja vigade vältimiseks kasutame try-catch
plokki, mis mitte midagi ei tee.
var list = new List<string>();
for(int i = 0; i < Math.Pow(10, 6); i++)
{
if(i == 1 || i % 2 != 0)
{
list.Add(i.ToString());
}
else
{
list.Add(null);
}
}
var watch = new Stopwatch();
watch.Start();
foreach(var s in list)
{
try
{
var i = s.Length;
}
catch(Exception ex)
{
var e = ex.Message;
}
}
watch.Stop();
Console.WriteLine(watch.Elapsed);
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace OOP
{
class Program
{
static void Main()
{
var list = new List<string>();
for(int i = 0; i < Math.Pow(10, 6); i++)
{
if(i == 1 || i % 2 != 0)
{
list.Add(i.ToString());
}
else
{
list.Add(null);
}
}
var watch = new Stopwatch();
watch.Start();
foreach(var s in list)
{
try
{
var i = s.Length;
}
catch(Exception ex)
{
var e = ex.Message;
}
}
watch.Stop();
Console.WriteLine(watch.Elapsed);
}
}
}
Masinas, kus seda näidet kirjutati, jooksis see kood lõpuni keskmiselt 3.32 sekundiga.
Nüüd teeme muudatuse foreach
tsüklis. Kui elemendi väärtus on null, siis me ei hakka mitte stringi pikkust leidma, vaid liigume järgmise elemendi juurde. Lisaks eemaldame try-catch
ploki.
var list = new List<string>();
for(int i = 0; i < Math.Pow(10, 6); i++)
{
if(i == 1 || i % 2 != 0)
{
list.Add(i.ToString());
}
else
{
list.Add(null);
}
}
var watch = new Stopwatch();
watch.Start();
foreach(var s in list)
{
if(s == null)
{
continue;
}
var i = s.Length;
}
watch.Stop();
Console.WriteLine(watch.Elapsed);
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace OOP
{
class Program
{
static void Main()
{
var list = new List<string>();
for(int i = 0; i < Math.Pow(10, 6); i++)
{
if(i == 1 || i % 2 != 0)
{
list.Add(i.ToString());
}
else
{
list.Add(null);
}
}
var watch = new Stopwatch();
watch.Start();
foreach(var s in list)
{
if(s == null)
{
continue;
}
var i = s.Length;
}
watch.Stop();
Console.WriteLine(watch.Elapsed);
}
}
}
See kontroll, mille me lisasime, kannab nimetust sanity check
ja see aitab meil vältida veaolukorda. Sama masina peal jooksutades lõpetas optimeeritud tsükkel oma töö keskmiselt 0.006 sekundiga ehk siis kuue millisekundiga.
Optimeeritud kood, mis vältis vigu, osutus seega 533 korda kiiremaks kui optimeerimata kood.