Onko olemassa nyrkkisääntöä sille, milloin Javassa kannattaa heittää poikkeus ja milloin luoda olio, jolla on attribuutti erilaisille virhetiloille tehtynä vaikkapa dynaamisena tietorakenteena?
Poikkeus kannattaa heittää, jos kyseisen funktion suoritus loogisesti katkeaa siihen. Muuta ratkaisua on loogista käyttää lähinnä silloin, kun suoritus ei katkea vaan virhe annetaan vain tiedoksi (ikään kuin lokitieto tai varoitus).
Jaska kirjoitti:
Onko olemassa nyrkkisääntöä sille, milloin Javassa kannattaa heittää poikkeus ja milloin luoda olio, jolla on attribuutti erilaisille virhetiloille tehtynä vaikkapa dynaamisena tietorakenteena?
Tämän tietorakenteen voi tietenkin lisätä myös poikkeus luokkaan ja sitä käyttää Metabolix ohjeen mukaan.
Poikkeus tulee tehdä, kun ohjelman suorituksessa tapahtuu poikkeuksellinen virhe, joka keskeyttää sen, mitä virhettä ennen yritettiin tehdä. Yleensä virhe on fataali eli ohjelma ei voi suorittaa loppuohjelmaa virheen takia.
Attribuuttia käytetään vertaamaan sitä, että ovatko jotkin tilat voimassa. Sitä voisi käyttää poikkeustenomaisesti, mutta tämä on epästandardia ja siksi ei-suositeltavaa. Poikkeuksilla on tietty käyttökohde, joka ei ole sama kuin attribuuttien. Kts. alempana.
TÄRKEÄÄ:
Jos poikkeukselle on standarditoteutus, kuten ArithmeticException, niin tulee käyttää standardipoikkeusta, eikä jotain omatekemää. Tämä takaa, että muiden nähdessä virheet, he voivat olettaa, että se on standarditoteutus.
Jos luokan Foo konstruktori voi epäonnistua, tämän voi ilmaista ainakin kolmella eri tavalla:
1) Lisätään luokkaan kenttä boolean valid;
, joka asetetaan falseksi.
2) Heitetään poikkeus.
3) Tehdään konstruktorin sijaan staattinen metodi, esimerkiksi static Optional<Foo> newFoo();
Tapa (1) on minusta lähes aina huono, koska konstruktoria kutsuva koodi voi unohtaa tarkistaa valid-kentän, eikä tämä johda käännösaikaiseen virheeseen.
Tapa (2) on usein tyypillinen.
Jos epäonnistuminen on melko yleistä ja useimmat kutsut joutuvat heti reagoimaan siihen, (3) voi johtaa selkeämpään koodiin kuin (2). Varsinkin, jos virheen syytä ei ole tarvetta raportoida kutsujalle. (Optionalin sijaan voi myös palauttaa jotain muuta, vaikka objektin joka sisältää joko poikkeustyylisen objektin tai Foon.)
mavavilj kirjoitti:
Yleensä virhe on fataali eli ohjelma ei voi suorittaa loppuohjelmaa virheen takia.
Perustuuko tämä väite mihinkään?
jlaire kirjoitti:
mavavilj kirjoitti:
Yleensä virhe on fataali eli ohjelma ei voi suorittaa loppuohjelmaa virheen takia.
Perustuuko tämä väite mihinkään?
Poikkeuksen määritelmään, ja esim. siihen, miten poikkeuksia käytetään alimmilla tasoilla.
"an exception breaks the normal flow of execution"
Jos käytetään poikkeusta, se ilmaisee, että ohjelmassa tapahtui virhe, joka ei ole normaalin suorituksen mukainen. Jos näin ei ole, niin poikkeusta on käytetty tapauksessa, jossa pitäisi olla muunlainen case-tarkastelu.
On totta, että korkean tason ohjelma voi jatkaa suoritusta, mutta ohjelman suoritus ei ole ollut normaali. On mahdollista, että poikkeusten olettaminen "välivirheiksi" johtaa erittäin vaikeasti ennustettavaan koodiin.
Yleensä poikkeuksen tapahtuminen tuottaa "graceful exit":n. Esimerkiksi ValueError tarkoittaa, että on saatu lukuarvo, jota ohjelma ei käytä eli se ei ole ohjelman mukainen.
mavavilj kirjoitti:
Yleensä poikkeuksen tapahtuminen tuottaa "graceful exit":n.
Olet kyllä ymmärtänyt jotain todella pahasti väärin. Ohjelman sulkeutuminen poikkeuksen takia on etenkin interaktiivisissa tai muuten pitkäkestoisiksi tarkoitetuissa ohjelmissa aika harvinaista. Yleensä ohjelman käyttöä halutaan jatkaa monenlaisista virheistä huolimatta. Monet poikkeukset ovat aivan odotettuja ja johtavat vain siihen, että ohjelma tekee jotakin muuta kuin ensisijaisen "normaalin" toiminnon.
HTTP-palvelin ei sammu jossain yksittäisessä virhetilanteessa vaan jatkaa muiden asiakkaiden palvelua. Poikkeus voi tulla vaikka siitä, että yksittäinen asiakas lähettää viallista dataa, eikä tämä tietenkään vaikuta muiden asiakkaiden palveluun.
Nettiselain ei sulkeudu, vaikka palvelimen osoite olisi virheellinen, vaan poikkeus on ihan odotettavissa ja raportoidaan käyttäjälle, joka voi sitten korjata osoitteen tai muuten jatkaa käyttöä.
Käyttäjän syöttämää lukua odottava ohjelma ei sammu virheellisen syötteen takia vaan pyytää paremman syötteen tai ehkä yksinkertaisesti värittää syöttökentän huomiovärillä ja estää etenemisen ennen syötteen korjausta.
Jos olet jostain näistä kohdista eri mieltä, voisit vaikka kuvailla, (a) millä perusteella ohjelman pitäisi yleensä päättyä kuvatunnlaisiin poikkeuksiin tai (b) miten ja miksi itse ratkaisisit nämä kohdat tehokkaasti ja luotettavasti ilman poikkeusten käyttöä.
Virhetilanteessa koko ohjelman sammuttaminen koskee lähinnä yhtä asiaa tekeviä komentoriviohjelmia, joita käytetään yksittäisenä komentona tai skriptin osana eikä interaktiivisesti. Muut ohjelmat sammuvat virheeseen vain ihan poikkeuksellisen vakavassa virhetilanteessa tai esimerkiksi käynnistyksen yhteydessä havaittavan asetusvirheen vuoksi, kun voi odottaa, että ohjelman käynnistäjä on vielä valvomassa tilannetta.
Olisi usein kuvaavampaa, jos ohjelmointikielten sanan except tilalla olisi expect.
Jäin miettimään, että tarkoittikohan mavavilj käsittelmätöntä poikkeusta, jolloin se usein johtaa jonkinlaiseen ohjelman loppumiseen, joskaan ei välttämättä hirveän "graceful".
No siis poikkeuksen määritelmä + käytetyn prosessin ilmentymä. Eli yhden asiakkaan prosessi stallaa. Yhden välilehden prosessi stallaa jne.
Metabolix kirjoitti:
mavavilj kirjoitti:
Yleensä poikkeuksen tapahtuminen tuottaa "graceful exit":n.
Nettiselain ei sulkeudu, vaikka palvelimen osoite olisi virheellinen, vaan poikkeus on ihan odotettavissa ja raportoidaan käyttäjälle, joka voi sitten korjata osoitteen tai muuten jatkaa käyttöä.
Käyttäjän syöttämää lukua odottava ohjelma ei sammu virheellisen syötteen takia vaan pyytää paremman syötteen tai ehkä yksinkertaisesti värittää syöttökentän huomiovärillä ja estää etenemisen ennen syötteen korjausta.
Näihin ei tarvita poikkeuksia. Ja niiden käyttäminen näihin on väärinkäyttöä.
Kts. Alempana, riippuu, missä kohdin koodia ollaan ja miten modularisoidaan.
Grez kirjoitti:
(03.03.2024 12:29:29): ”– –” Joo se on kyllä aika paha tilanne missä tahansa...
Ei vaan poikkeuksen tarkoitus on:
Tuli poikkeus.
Printaa debuggeriin/logiin ongelma.
Poistu siististi osasta, joka on nyt rikki, aiempaan osaan (caller), koska ohjelman jatko osassa on undefined behavior.
mavavilj kirjoitti:
Näihin ei tarvita poikkeuksia. Ja niiden käyttäminen näihin on väärinkäyttöä.
Poikkeuksen käyttäminen esimerkiksi sen ilmoittamiseen, että annettu teksti ei ole kelvollinen kokonaisluku tai että yhteys palvelimeen ei onnistu, on täysin oikein ja hyväksyttävää. Jos väität muuta, esitä jokin kelpo perustelu.
Metabolix kirjoitti:
(03.03.2024 15:06:58): ”– –” Poikkeuksen käyttäminen esimerkiksi sen...
Riippuu siitä, miten paljon virhe vaikuttaa. Oleellista on, että poikkeus on vahvempi ilmiö kuin vain tarkistaminen, eikä poikkeuksia tulisi käyttää pienempien tarkastusten riittäessä.
Myös esim. onko virheen lähde oma koodi vai syöte tai muu ohjelma.
Esim.
"Do not use throw to indicate a coding error in usage of a function. Use assert or other mechanism to either send the process into a debugger or to crash the process and collect the crash dump for the developer to debug."
Eli mitä, jos float:ia odottavaan funktioon yritettiin int:iä?
"Do not use throw if you discover unexpected violation of an invariant of your component, use assert or other mechanism to terminate the program. Throwing an exception will not cure memory corruption and may lead to further corruption of important user data."
https://isocpp.org/wiki/faq/exceptions#why-not-exceptions
Eri kielissä voi olla eri konventioita.
Pyöritän nyt esim. GeckoView -koodia (Kotlin), jossa tulee erroreita logcatiin, mutta fataaleja ne eivät minun osassa ole. Eräs niistä on file not found.
Jos nämä olisivat exceptioneita, niin minun pitäisi debugata jotain muuta osaa kuin omaani, mutta nämä ovatkin kirjastonlaatijan puolella.
Olet löytänyt wikistä erinomaisia kohtia, jotka tukevat minun kertomaani ja kumoavat sinun väitteitäsi. Ihmeellistä, että olet tulkinnut ne aivan päinvastoin. Noissahan siis sanotaan, että tietyissä vakavissa virhetilanteissa, joissa ohjelmassa on vakava virhe ja ohjelma ei pysty jatkamaan, ei tulisi käyttää poikkeuksia vaan sulkea ohjelma muulla tavalla. Eli tilanteessa, johon itse tarjosit poikkeuksia (ohjelman lopetus fataaliin virheeseen), niitä juuri ei tulisi käyttää tuon ohjeen mukaan.
Samalla sivulla pari riviä ylempänä lukee: "Use throw only to signal an error (which means specifically that the function couldn’t do what it advertised, and establish its postconditions)."
Eli aivan kuten sanoin: hyviä aiheita poikkeukselle ovat esimerkiksi, että annettu teksti ei ole kelvollinen kokonaisluku (eli funktio yritti tulkita tekstin lukuna mutta ei pystynyt) tai että yhteys palvelimeen ei onnistu (eli funktio yritti yhdistää mutta ei pystynyt).
mavavilj kirjoitti:
Ei vaan poikkeuksen tarkoitus on:
Tuli poikkeus.
Printaa debuggeriin/logiin ongelma.
Poistu siististi osasta, joka on nyt rikki, aiempaan osaan (caller), koska ohjelman jatko osassa on undefined behavior.
Sinulla on tässä logiikka ihan sekaisin.
Ensin lokitetaan omalla tasolla ja prioriteetilla, sitten tarvittaessa siivotaan omat asiat, ja viimeiseksi heitetään poikkeus. Nimenomaan poikkeus on se tapa viestiä siististi ylemmälle tasolle, että nyt ei onnistunut vaan homma jäi kesken.
mavavilj kirjoitti:
(03.03.2024 16:17:32): Pyöritän nyt esim. GeckoView -koodia...
Kyllä ne voivat olla poikkeuksia siellä kirjaston sisällä (oletko tutkinut asiaa?), mutta koska kirjasto pystyy käsittelemään ne sisäisesti, tietenkään niitä ei ole pakko heittää ulos. Eli ihan kuten sanoin, poikkeus voi olla pieni käsiteltävä asia eikä sen tarvitse keskeyttää koko ohjelman toimintaa. GeckoViewin kohdalla voisi ajatella, että kyseessä on integroitu selain, ja niin kauan kuin sen toiminta pysyy selaimen rajoissa (sisältäen puuttuvista tiedostoista selviytymisen, virhesivun näytön jne.), ei ole tarvetta heittää poikkeusta ulos.
Metabolix kirjoitti:
Olet löytänyt wikistä erinomaisia kohtia, jotka tukevat minun kertomaani ja kumoavat sinun väitteitäsi. Ihmeellistä, että olet tulkinnut ne aivan päinvastoin. Noissahan siis sanotaan, että tietyissä vakavissa virhetilanteissa, joissa ohjelmassa on vakava virhe ja ohjelma ei pysty jatkamaan, ei tulisi käyttää poikkeuksia vaan sulkea ohjelma muulla tavalla. Eli tilanteessa, johon itse tarjosit poikkeuksia (ohjelman lopetus fataaliin virheeseen), niitä juuri ei tulisi käyttää tuon ohjeen mukaan.
Tuo on advanced use case tietää muistikorruptiosta, eikä edes relevanttia Java:lle?
Metabolix kirjoitti:
Samalla sivulla pari riviä ylempänä lukee: "Use throw only to signal an error (which means specifically that the function couldn’t do what it advertised, and establish its postconditions)."
Eli aivan kuten sanoin: hyviä aiheita poikkeukselle ovat esimerkiksi, että annettu teksti ei ole kelvollinen kokonaisluku (eli funktio yritti tulkita tekstin lukuna mutta ei pystynyt) tai että yhteys palvelimeen ei onnistu (eli funktio yritti yhdistää mutta ei pystynyt).
Se riippuu virheiden lähteestä, ja siitä miten modularisoidaan. Tai kenelle virhe on relevantti. Lisäksi nuo eivät välttämättä ole exceptionaaleja eventejä, vaan ne voivat olla odotettavia.
https://softwareengineering.stackexchange.com/a/
Tai
"For example, programmers often place assertions at the beginning of functions to check if the input is valid (preconditions)."
https://realpython.com/python-assert-statement/#what-are-assertions-good-for
Jos tuo olisi exception, niin ei tarvitsisi tehdä assert:eja.
Huom. Java:ssa assertit pitää enabloida, tai niitä ei tule käännettyyn ohjelmaan. Siten Java:ssa yleensä kaikki exception-kaltaiset ovat exceptioneita.
Huom. C:ssä ei ole poikkeuksia.
-> Lähtökohtaisesti on muita vaihtoehtoja strukturoida ohjelma kuin poikkeusten avulla.
Tässä GeckoView-ohjelmassani tulee esim. paljon nulleja returneina. Exceptioneja ei näy. Virheet ovat exception-kaltaisia. Syötteet ovat virheellisiä.
Nämä ovat public facing, joten jonkun mielestä pitäisi näkyä exceptionia clientille, eikö? Miksi ei näy?
mavavilj kirjoitti:
Tuo on advanced use case tietää muistikorruptiosta, eikä edes relevanttia Java:lle?
Mikä ihmeen "advanced use case"? Joka tapauksessa itsehän sinä olet tässä C++-linkkejä tuonut keskusteluun.
mavavilj kirjoitti:
Lisäksi nuo eivät välttämättä ole exceptionaaleja eventejä, vaan ne voivat olla odotettavia.
En tiedä, mitä yrität tällä nyt perustella. Enkö nimenomaan sanonut, että poikkeuksilla viestitään myös aivan odotetuista tilanteista, jotka kuitenkin poikkeavat ohjelman ”normaalista” kulusta. Eli vaikka nyt se tekstin parsiminen kokonaisluvuksi ”normaalisti” tuottaa tekstistä luvun mutta ”poikkeus” on se, että teksti ei ollut kelvollinen.
mavavilj kirjoitti:
Lähtökohtaisesti on muita vaihtoehtoja strukturoida ohjelma kuin poikkeusten avulla.
Toki on, mutta monissa kielissä on poikkeukset, koska ne ovat selvempi ja vähemmän virhealtis tapa strukturoida ohjelma. Voisit vaikka lukea tuon linkittämäsi C++-sivun kokonaan oikein ajatuksella, sehän alkaa selityksestä, miksi poikkeuksia tulisi monissa tilanteissa käyttää.
Tässä on nyt vielä esimerkki, miten poikkeuksia voi käyttää Javassa.
public static int parseInt(String s) throws NumberFormatException { if (s.length() == 0) { throw new NumberFormatException("empty string"); } int result = 0, sign = 1; for (int i = 0; i < s.length(); ++i) { if (i == 0 && (s.charAt(i) == '-' || s.charAt(i) == '+')) { // etumerkki sign = s.charAt(i) == '-' ? -1 : 1; } else if ('0' <= s.charAt(i) && s.charAt(i) <= '9') { // numero int digit = (int)(s.charAt(i) - '0'); int prev = result; result = 10 * result + (digit * sign); // outo muutos luvussa = overflow if (result / 10 != prev) { throw new NumberFormatException("int overflow"); } } else { // väärä merkki throw new NumberFormatException("not a number"); } } return result; }
Funktion normaali toiminta on palauttaa tekstiä vastaava kokonaisluku. Funktion heittämä poikkeus on, että teksti ei kelpaa luvuksi. Tämä on tavallinen ja aivan oikea tapa käyttää poikkeuksia.
Funktio voi tuottaa esimerkiksi seuraavat tulokset:
"": exception: empty string "0": 0 "-0": 0 "+0": 0 "0-": exception: not a number "0-0": exception: not a number "123x": exception: not a number "123456789": 123456789 "+123456789": 123456789 "-123456789": -123456789 "+2147483647": 2147483647 "-2147483647": -2147483647 "+2147483648": exception: int overflow "-2147483648": -2147483648 "+2147483649": exception: int overflow "-2147483649": exception: int overflow
Tietenkin tämä on teknisesti mahdollista viestiä muillakin tavoin. Paluuarvo voisi olla jotain muuta kuin pelkkä luku, vaikka olio, jossa olisi kaksi arvoa (luku ja virhe), tai Optional, tai muuta vastaavaa. Parametrina voisi olla muuttuja, johon virhe tai luku tallennetaan (C:ssä osoitin, Javassa vaatisi taas erillisen olion). Parametrina voisi olla vaikka kielestä riippuen callback-funktio tai jokin käsittelijäolio, ParsedNumberHandler, jolle luku tai virhe välitettäisiin, ja funktion ei tarvitsisi palauttaa mitään. Silti poikkeus on tähän tilanteeseen aivan oikea ratkaisu, muiden ratkaisujen olemassaolo ei tee ainakaan poikkeuksesta väärää.
On aika typerää väittää, että poikkeuksia ei kuuluisi käyttää siihen tai tähän (erityisesti Javassa, sitähän tämä keskustelu käsitteli). Käy katsomassa Javan dokumentaatiota, mistä kaikesta tulee poikkeuksia. Todella suuri osa Javan metodeista voi tuottaa poikkeuksen, ja niitä käytetään juuri yllä kuvaamallani tavalla. Oletko sitä mieltä, että koko Javan standardikirjasto on tehty Javan käyttöohjeiden vastaisesti ja väärin?
Metabolix kirjoitti:
mavavilj kirjoitti:
Lisäksi nuo eivät välttämättä ole exceptionaaleja eventejä, vaan ne voivat olla odotettavia.
En tiedä, mitä yrität tällä nyt perustella. Enkö nimenomaan sanonut, että poikkeuksilla viestitään myös aivan odotetuista tilanteista, jotka kuitenkin poikkeavat ohjelman ”normaalista” kulusta. Eli vaikka nyt se tekstin parsiminen kokonaisluvuksi ”normaalisti” tuottaa tekstistä luvun mutta ”poikkeus” on se, että teksti ei ollut kelvollinen.
https://stackoverflow.com/a/409842
https://stackoverflow.com/a/409810
Nämä ovat relevanttia siihen, että milloin toisenlainen case-tarkastelu voisi riittävä.
Ajattelin, että C/C++ avaisi, mitkä asiat sopivat muuten kuin exceptioneilla tehtäviksi.
Kaikki yo. koodissa olevat kohdat ovat graceful exit -tilanteita. Empty string ja not a number ovat vähän rajatapauksia, koska sellaisia ei kannata odottaa, kun nimi on parseInt. Eli kukaan idiootti ei laittaisi niitä sinne. Riittäisi palauttaa null ja olla hiljaa.
Opin joskus myös, että exceptioneita ei pitäisi viljellä, koska se tarkoittaa kutsujalle, että pitää catchata kaikenlaista. Kutsujaa yleensä kiinnostaa vain tietyt asiat, eikä turhat pienet viestit. Jos siis exceptionoidaan, niin viestin pitää olla tärkeä ja viestejä ei saa olla liikaa.
mavavilj kirjoitti:
Nämä ovat relevanttia siihen, että milloin toisenlainen case-tarkastelu voisi riittävä.
Nyt on pakko kysyä, että luitko noita linkkejäsi taas ollenkaan ja tiedätkö mitään assertin käytöstä, vai missä vika, että sotket assertin tähän keskusteluun?
Rinnastus Javan poikkeuksista juuri C:n asserttiin on kyllä äärimmäisen epäonnistunut. C:ssä assert abortoi koko ohjelman (nimenomaan abort eikä exit), joten se ei sovellu minkäänlaiseen virheenkäsittelyyn, ainoastaan fataalien virheiden tunnistamiseen debuggausvaiheessa (koska tuotantovaiheen käännösasetus NDEBUG eli "no debug" poistaa assertit ohjelmasta). Joten pysytään nyt vaikka Javassa mieluummin.
Assert ei ole ratkaisu, joka yleensäkään ”voisi [olla?] riittävä” poikkeusten tilalle. Assert on kehitysvaiheeseen tarkoitettu tarkastus, jolla etsitään tilanteita, joita ei pitäisi ikinä tapahtua eli jotka ovat ohjelmointivirheitä. Kuten tuolla Stack Overflow'n keskustelussa lukee: ”Assert the stuff that you know cannot happen (i.e. if it happens, it's your fault for being incompetent).”
Assertin idea on, että assertista tuleva virheilmoitus on aina bugi omassa koodissa ja että kun bugit on korjattu, assertit voi turvallisesti poistaa käytöstä. Riippuu tilanteesta ja käännös- tai suoritusasetuksista, tekeekö assert edes mitään, joten sen varaan ei pidä laittaa mitään valmiin koodin suoritukseen, käyttäjän syötteeseen tai ajoympäristön yllätyksiin liittyvää tarkastusta, eikä sitä pidä käyttää valmiin ohjelman lokifunktiona.
Monissa kielissä uusilla ominaisuuksilla on vähennetty assertin käytön tarvetta, kun erilaisia vaatimuksia voi selvemmin esittää jo syntaksin tasolla. Esimerkiksi PHP:ssä aiemmin ei voinut merkitä funktioiden argumenttien tyyppejä, ja assert olisi voinut sopia silloin tyyppien tarkastukseen.
mavavilj kirjoitti:
Empty string ja not a number ovat vähän rajatapauksia, koska sellaisia ei kannata odottaa, kun nimi on parseInt. Eli kukaan idiootti ei laittaisi niitä sinne.
Mitä ihmettä selität? :D :D :D
On ihan tavallista käyttää parseInt-tyyppisiä funktioita nimenomaan näin:
try { int number = parseInt(str); } catch (NumberFormatException e) { // käsittele virhe }
Jos tässä ei käytettäisi poikkeuksia, jouduttaisiin tekemään erillinen tarkastus:
if (isValidInt(str)) { int number = parseValidInt(str); } else { // käsittele virhe }
Erillistä tarkastusta käyttävässä ratkaisussa on monta selvää huonoa puolta: Tarkastus pitää aina joka kerta muistaa tehdä erikseen. Tarkastusta varten pitää kirjoittaa ylimääräinen funktio, joka lähes mutta ei aivan samaa koodia kuin varsinaisen työn tekevä funktio. Jos funktioiden toimintaa muokataan jälkikäteen, muutoksetkin pitää muistaa tehdä molempiin funktioihin. Jos tarkastuksen unohtaa ja funktio ei käytä poikkeuksia, tulos voi olla virheellinen ja ongelman tunnistaminen ohjelmasta voi olla perin vaikeaa. Pahimmillaan funktioihin tulee pieniä eroja niin, että jokin syöte tunnistetaan kelvolliseksi mutta se ei sitten oikeasti kelpaakaan.
Voit kokeeksi kirjoittaa nämä funktiot isValidInt ja parseValidInt ja verrata, miten koodin pituus ja selkeys suhtautuu tuohon poikkeuksia käyttävään versioon. Testaa toki myös, mitä tuloksia funktio antaa erilaisilla virheellisillä syötteillä.
mavavilj kirjoitti:
Opin joskus myös, että exceptioneita ei pitäisi viljellä, koska se tarkoittaa kutsujalle, että pitää catchata kaikenlaista.
Tässä on enemmänkin kyse siitä, että ei pidä tuottaa tarpeettoman yksityiskohtaisia poikkeuksia. Esimerkiksi vaikka parseInt voi ihan hyvin ilmoittaa yhdellä poikkeustyypillä, että luku ei kelpaa. Olisi yleensä tarpeetonta ja ärsyttävää, jos tulisi eri poikkeus kaikista eri virheistä (esim. NullException, EmptyStringException, IllegalWhiteSpaceException, IllegalCharacterException, TooBigNegativeException, TooBigPositiveException).
Tai vastaavasti jossain matalalla tasolla voi kiinnostaa, miksi verkkoyhteys meni poikki, mutta jossain kohdassa matkalla korkeammalle tasolle tämä poikkeus kannattaisi muuttaa yksinkertaisempaan muotoon, että poikki on.
Monissa kielissä onkin mahdollista merkitä poikkeuksen syyksi toinen poikkeus, jolloin debuggaus ei kärsi mutta ohjelmointi helpottuu. Eli poikkeus voi sisältää tiedon, että nettisivun lataus ei onnistu, koska yhteys katkesi, koska
mavavilj kirjoitti:
Jos siis exceptionoidaan, niin viestin pitää olla tärkeä ja viestejä ei saa olla liikaa.
Poikkeuksella on aina yksi viesti ja se on aina vähintäänkin yhdellä tavalla tärkeä: funktion suoritus jäi kesken ja funktion tulosta ei voi käyttää. Jos poikkeuksen jättäisi heittämättä, mistä käyttäjä sitten tietäisi, että suoritus jäi kesken ja tulosta ei voi käyttää (ellei siis ole käytetty jotain mainituista muista virheilmoituskeinoista)? Jos virhe ei estä funktion jatkumista, silloin se ei tietenkään voi johtaa poikkeukseen, eli poikkeuksia ei voi tulla siinä mielessä ”turhaan” tai ”liikaa”.
No siis, että assert:in käyttö C-kielissä sivuaa exception:eja. Ja toisaalta käyttämällä assert:eja voi ymmärtää paremmin virheenkäsittelytarpeita.
Metabolix kirjoitti:
mavavilj kirjoitti:
Opin joskus myös, että exceptioneita ei pitäisi viljellä, koska se tarkoittaa kutsujalle, että pitää catchata kaikenlaista.
Tässä on enemmänkin kyse siitä, että ei pidä tuottaa tarpeettoman yksityiskohtaisia poikkeuksia. Esimerkiksi vaikka parseInt voi ihan hyvin ilmoittaa yhdellä poikkeustyypillä, että luku ei kelpaa. Olisi yleensä tarpeetonta ja ärsyttävää, jos tulisi eri poikkeus kaikista eri virheistä (esim. NullException, EmptyStringException, IllegalWhiteSpaceException, IllegalCharacterException, TooBigNegativeException, TooBigPositiveException).
Niin paitsi, että pitää osata päättää, että mitä tekee, koska jos kaikki on yhden tyypin alla, niin jos joku toinen käyttäjä haluaakin branchata exception-tyypin mukaan, niin tätä ei edistä se, että kaikilla exceptioneilla on sama tyyppi.
-> Pitää miettiä, mikä on tarpeellista, eikä vain roiskia.
Metabolix kirjoitti:
mavavilj kirjoitti:
Jos siis exceptionoidaan, niin viestin pitää olla tärkeä ja viestejä ei saa olla liikaa.
Poikkeuksella on aina yksi viesti ja se on aina vähintäänkin yhdellä tavalla tärkeä: funktion suoritus jäi kesken ja funktion tulosta ei voi käyttää. Jos poikkeuksen jättäisi heittämättä, mistä käyttäjä sitten tietäisi, että suoritus jäi kesken ja tulosta ei voi käyttää (ellei siis ole käytetty jotain mainituista muista virheilmoituskeinoista)? Jos virhe ei estä funktion jatkumista, silloin se ei tietenkään voi johtaa poikkeukseen, eli poikkeuksia ei voi tulla siinä mielessä ”turhaan” tai ”liikaa”.
Pitää tietää, mikä on tarpeellista. Esimerkiksi ovatko parseInt:iin syötteestä tulevat virheet relevantteja parseInt:n ohjelmoijalle vai syöttäjälle? Entä mikä on virheiden esiintymistodennäköisyys? Milloin return null tai false riittää?
Täällä oli tällainen essee:
https://learn.microsoft.com/en-gb/archive/blogs/ericlippert/vexing-exceptions
"Vexing exceptions are the result of unfortunate design decisions. Vexing exceptions are thrown in a completely non-exceptional circumstance, and therefore must be caught and handled all the time.
The classic example of a vexing exception is Int32.Parse, which throws if you give it a string that cannot be parsed as an integer. But the 99% use case for this method is transforming strings input by the user, which could be any old thing, and therefore it is in no way exceptional for the parse to fail."
https://stackoverflow.com/a/4946774
"coding by exception": https://stackoverflow.com/a/4945774
Metabolix kirjoitti:
Oletko sitä mieltä, että koko Javan standardikirjasto on tehty Javan käyttöohjeiden vastaisesti [Ei] ja väärin [Ehkä]?
https://stackoverflow.com/a/8392060
.NET:ssä on muuten kaksi "parse int" funktiota:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/how-to-convert-a-string-to-a-number#call-parse-or-tryparse-methods
En tiedä riittäisikö tähän nyrkkisäännöksi, että tee kaikki muuten kuin exception:eilla ja liitä exception:t asioihin, joista joku oikeasti haluaa tietää ja jotka ovat poikkeuksellisia eli epäodotettavia eli esiintyvät jokseenkin harvoin.
Tai sitten, kuten Metabolix ilmeisesti haluaa: seuraa kirjaston tai kielenlaatijan esimerkkiä, jotta olette edes konsistentteja. Ongelma on, että Java on vanha ripuli, jossa on jotain varhaisia arvauksia C/C++:n kehittämisestä. Varmaan joku Apache Commons on epävirallinen standardikirjasto, jolla Java:sta saa modernimman, koska nykyään C# on oikeampi Java. Ja kannattaa esim. katsoa, miten asia tehdään C#:ssa.
mavavilj kirjoitti:
(03.03.2024 21:01:51): Opin joskus myös, että exceptioneita ei...
Se on juurikin täsmälleen poikkeusten tarkoitus. Jos suoritusvirhe ilmoitetaan esimerkiksi funktion paluuarvolla, niin funktiota kutsuta koodari voi aika usein unohtaa tai jopa "koodin siistiyden nimissä" tarkoituksella jättää tarkistamatta eri virhetilanteiden varalta. Tämä voi johtaa kaikenlaisiin hölmöihin bugeihin silloin, kun ne virheet oikeasti tapahtuvat.
Poikkeuksen heittäminen pakottaa funktiota kutsuvan koodarin tekemään asiat oikein. Se on siis hyvä asia.
Joskus paluuarvolla on myös mahdotonta ilmoittaa virhettä. Mitä jos funktio palauttaa booleanin ja sekä TRUE että FALSE ovat sallittuja lopputuloksia. Millä arvolla voit ilmoittaa virheestä? Et millään.
mavavilj kirjoitti:
Poikkeus tulee tehdä, kun ohjelman suorituksessa tapahtuu poikkeuksellinen virhe, joka keskeyttää sen, mitä virhettä ennen yritettiin tehdä. Yleensä virhe on fataali eli ohjelma ei voi suorittaa loppuohjelmaa virheen takia.
Nyt et ole ymmärtänyt lukemaasi / kuulemaasi / oppimaasi.
Poikkeus todellakin tulee heittää silloin, kun suoritus tulee keskeyttää, mutta se ei tarkoita koko sovelluksen suoritusta vaan kyseisen "proseduurin" suoritusta; proseduuri voi olla vaikka funktion sisällä oleva koodi.
mavavilj kirjoitti:
Jos poikkeukselle on standarditoteutus, kuten ArithmeticException, niin tulee käyttää standardipoikkeusta, eikä jotain omatekemää. Tämä takaa, että muiden nähdessä virheet, he voivat olettaa, että se on standarditoteutus.
Liian geneeristen poikkeusten heittäminen on huono tapa, koska silloin on mahdotonta napata vain kyseinen virhe ja ohittaa loput (ts. antaa niiden "kuplia" ylemmille tasoille, missä ne mahdollisesti käsitellään jollain toisella tavalla).
Oma poikkeusluokka voi myös periä jonkin ns. standardipoikkeuksen.
Poikkeuksilla on helppoa suoraviivaistaa koodin suorittamista.
Yksi esimerkki ilman poikkeuksia; virheet ilmaistaan funktion paluuarvoilla:
let foo = doWork() if (foo) { let bar = doMoreWork(foo) if (bar) { finish(foo, bar) } }
Sama esimerkki mutta poikkeuksia käyttäen:
try { let foo = doWork() let bar = doMoreWork(foo) finish(foo, bar) } catch (DoWorkException | DoMoreWorkException) { }
Yllä oleva esimerkki on ehkä se kaikista yleisin syy poikkeusten käyttämiseen: syvät if-else-rakenteet on helppo korvata yhdellä catch-lohkolla ja koodikin on selkeämpää.
Poikkeuksia voi myös käyttää useilla eri tavoilla. Viime kädessä tarkoitus on kuitenkin vain tehdä koodista siistimpää ja vähentää huolimattomuusvirheiden mahdollisuutta. Taitava koodari voi käyttää poikkeuksia usealla eri tavalla tehden koodistaan helpompaa lukea ja ylläpitää.
Eri ohjelmointikieliin liittyy erilaisia rajoituksia ja nyansseja, joten on vaikeaa antaa seikkaperäisiä vinkkejä parhaisiin kikkoihin. Niitä pitää vain opetella käyttämään ja miettiä aina, että mikä on paras lähestymistapa.
Poikkeuksen heittämisen ei aina tarvitse tarkoittaa suoritusvirhettä; se voi myös toimia signaalina suorituksen päättymiselle.
function doRandomWork(j) { try { let i = 0; while (true) { i = foo(i) if (timeToStop(j)) { throw new FinishedException(i) } if (alternativeTimeToStop(i)) { /** * Erikoisehdon täyttyessä keskeytetään suoritus ja palautetaan arvo 0. */ throw new FinishedException(0) } } } catch (FinishedException result) { return result.value + j } }
muuskanuikku kirjoitti:
Poikkeuksilla on helppoa suoraviivaistaa koodin suorittamista.
Yksi esimerkki ilman poikkeuksia; virheet ilmaistaan funktion paluuarvoilla:
let foo = doWork() if (foo) { let bar = doMoreWork(foo) if (bar) { finish(foo, bar) } }Sama esimerkki mutta poikkeuksia käyttäen:
try { let foo = doWork() let bar = doMoreWork(foo) finish(foo, bar) } catch (DoWorkException | DoMoreWorkException) { }Yllä oleva esimerkki on ehkä se kaikista yleisin syy poikkeusten käyttämiseen: syvät if-else-rakenteet on helppo korvata yhdellä catch-lohkolla ja koodikin on selkeämpää.
Tämähän on ihan kusetusesimerkki, koska et tehnyt poikkeusten yksittäistarkasteluja eli mitä kullakin poikkeuksella tehdään. Lisäksi näissä näkyy yllämainittua coding by exception. Erityisesti paluuarvoa sotkettaessa poikkeuksiin. Näissä lainatuissa koodeissasi on 7 vs 6 tai 5 vs 5 riviä, joten tarkastusten kanssa poikkeuskoodista tulee pitempi. Etkä tarkastanut, voiko ylempää koodia tiivistää.
Entä kun pitää selvittää, että missä poikkeus tapahtui? Entä teetkö joskus DoMoreLogicError123xyzversionbExceptionin, kun pokkeuksia pitää voida erottaa?
Entä jos scopessa osa heittää ja osa ei?
Muihin huomioihisi on vastaus viestissä https://www.ohjelmointiputka.net/keskustelu/
mavavilj kirjoitti:
Tämähän on ihan kusetusesimerkki, koska et tehnyt poikkeusten yksittäistarkasteluja eli mitä kullakin poikkeuksella tehdään. Lisäksi näissä näkyy yllämainittua coding by exception. Erityisesti paluuarvoa sotkettaessa poikkeuksiin. Näissä lainatuissa koodeissasi on 7 vs 6 tai 5 vs 5 riviä, joten tarkastusten kanssa poikkeuskoodista tulee pitempi. Etkä tarkastanut, voiko ylempää koodia tiivistää.
Kyse ei ole koodin pituudesta vaan selkeydestä. Esimerkki havainnollistaa vain sitä, että jos meillä on peräkkäin useampi funktiokutsu ja myöhempien funktioiden kutsuminen riippuu edellisen kutsun onnistumisesta, niin koodista tulee nopeasti syvä if-elsejen pyramidi.
Mitä enemmän tällaisia redundantteja if-elsejä tarvitaan, sitä todennäköisemmin yhdessä niistä on bugi. (Tarkistetaan väärää ehtoa tai unohdetaan tarkistaa jokin reunaehto.)
Esimerkki ei tee virheillä yhtään mitään, koska se on ihan oma ongelmansa. Joskus virheen tapahtuessa on mahdollista käyttää jotain oletusarvoa ja jatkaa suoritusta, joskus taas suoritus on sillä tasolla keskeytettävä ja raportoitava virheestä edelliselle kutsujalle (omasta funktiosta heitettävä uusi poikkeus, jonka omaa funktiota kutsunut taho joutuu käsittelemään).
"Paluuarvon sotkeminen poikkeuksiin" tarkoittaa siis mitä? Poikkeuksethan yleisesti ottaen siirtävät virheraportoinnin pois paluuarvoista.
Mainitsemasi "coding by exception" on kohtalaisen humoristinen keissi. Ilmeisesti yksi kaveri on jättänyt karvaan postauksen nettiin joskus 20 vuotta sitten, ja myöhemmin sen ympärille on kerääntynyt pieni poikkeuksia verisesti vihaava kultti.
mavavilj kirjoitti:
Entä kun pitää selvittää, että missä poikkeus tapahtui? Entä teetkö joskus DoMoreLogicError123xyzversionbExceptionin, kun pokkeuksia pitää voida erottaa?
No millä ihmeellä sinä selvität sen, mistä jokin "-1" on peräisin? Virheen alkuperän selvittäminen on sitä paitsi debuggaamista eikä liity ohjelman toimintaan mitenkään.
Ohjelman toiminnan aikana meitä kiinnostaa vain virheiden tyyppi. "Jos tapahtuu virhe A, niin meidän täytyy toimia tavalla X, mutta jos tapahtuukin virhe B, niin meidän täytyy toimia tavalla Y."
Niin – jos meidän pitää voida erottaa kaksi virhettä toisistaan, niin silloin ne selkeästi vaativat eri poikkeusluokatkin.
mavavilj kirjoitti:
Entä jos scopessa osa heittää ja osa ei?
Jos poikkeuksia ei lentele, niin niitä ei voi napatakaan. Sitten on tilannekohtaisesti päätettävä, että haluaako kirjoittaa ylimääräistä käärekoodia, jotta sen loppuosankin koodista saa mukautettua poikkeuksia heittäväksi koodiksi, vai että päätyykö käsittelemään virheet eri tavalla.
Tämän varmasti tiesitkin jo mutta pitihän päästä nillittämään.
Vielä tuosta "coding by exceptionista", niin koodin kirjoittaminen väärin on aina synti. Ei sillä ole pahemmin väliä, millä tavalla koodi on paskaa, jos se on paskaa.
Poikkeusten käyttö ei automaattisesti tee koodista parempaa, jos niitä käyttää väärin, mutta ihan yhtälailla poikkeusten maaninen välttely voi tehdä koodista huonompaa.
Liian kirjavien poikkeusten heittäminen on selkeästi väärin, mutta niin on myös liian monen virhekoodin käyttö klassisessa ohjelmoinnissa. On aivan sama, heittääkö funktio neljää erilaista poikkeusta vai palauttaako se virhekoodit -1, -2, -3 ja -4. Kaikki ne on kuitenkin käsiteltävä.
Ongelma ei millään tavalla ole uusi eikä sillä ole mitään tekemistä poikkeusten käytön kanssa: huono ohjelmointi on aina huonoa ohjelmointia.
Ehkä pitäisi luottaa tekijän suosituksiin
https://docs.oracle.com/javase/tutorial/
Vähän parempia esimerkkejä tuolla. Mutta myös Oracle käyttää termiä "exceptional" eli ikäänkuin exceptioneita ei ole tarkoitus räiskiä sinne tänne.
mavavilj kirjoitti:
Ehkä pitäisi luottaa tekijän suosituksiin
https://docs.oracle.com/javase/tutorial/
essential/exceptions/advantages.html Vähän parempia esimerkkejä tuolla. Mutta myös Oracle käyttää termiä "exceptional" eli ikäänkuin exceptioneita ei ole tarkoitus räiskiä sinne tänne.
Olisi erittäin yllättävää(poikkeuksellista), mikäli poikkeuksista puhuttaessa ei käytettäisi sanaa poikkeuksellinen. (confirmation bias)
Omasta mielestäni linkkaamasi artikkeli havainnollistaa juuri sitä asiaa, mitä itsekin yritin havainnollistaa, mutten puppukoodillani onnistunut konkretisoimaan hyötyjä yhtä tehokkaasti kuin oikealla koodilla pystyy.
Poikkeuksella ja poikkeuksellisella on minusta hieman eri semantiikka. Muuten tiedoston avaamattomuus ei olisi poikkeuksellista, koska niin tapahtuu aina kun ei ole lukuoikeuksia. Poikkeuksellisuus viittaa siis myös harvinaisuuteen. Ei vain siihen, että se poikkeaa säännöistä.
https://thecontentauthority.com/blog/exception-vs-exceptional
muuskanuikku kirjoitti:
mavavilj kirjoitti:
Entä kun pitää selvittää, että missä poikkeus tapahtui? Entä teetkö joskus DoMoreLogicError123xyzversionbExceptionin, kun pokkeuksia pitää voida erottaa?
No millä ihmeellä sinä selvität sen, mistä jokin "-1" on peräisin? Virheen alkuperän selvittäminen on sitä paitsi debuggaamista eikä liity ohjelman toimintaan mitenkään.
Siis siitä, että virhe tuotetaan samalle riville kuin epäonnistuva kutsu. Exceptioneissa pitää katsoa, mistä se tuli ja missä se ratkaistaan.
Aihe on jo aika vanha, joten et voi enää vastata siihen.