Az F# programozási nyelv

Kivételkezelés



A .NET keretrendszerben a hibák kezelésének szabványos módja a kivételkezelési mechanizmuson alapszik. Számos .NET nyelv támogatja ezt az eljárást, beleértve az F#-ot is.

Kivételek típusa

Az F#-ban a kivételeknek két csoportját különböztethetjük meg: beszélhetünk .NET-kivételtípusokról, illetve F#-kivételtípusokról. Ebben a részben az F#-kivételtípusokról lesz szó. A kivételek definiálásának általános szintaxisa alább látható.

exception exception-type of argument-type

Az elobbi definícióban az exception-type az új F#-kivétel neve, az argument-type pedig azt a típust reprezentálja, amelynek megfelelo típusú kivételt a késobbiek folyamán szeretnénk kiváltani. Egy konkrét példa kivétel definiálására:

exception MyError of string

Az elobbi típusú kivételek kiváltására használható a raise függvény:

raise (MyError("Error message"))

Az F#-kivételek közvetlenül felhasználhatók egy try...with kifejezésben, ahogy ezt az alábbi példakód is mutatja:

exception Error1 of string
// Rendezett pár használata argumentumként
exception Error2 of string * int

let function1 x y =
    try
        if x = y then raise (Error1("x"))
        else raise (Error2("x", 10))
    with
        | Error1(str) -> printfn "Error1 %s" str
        | Error2(str, i) -> printfn "Error2 %s %d" str i

function1 10 10
function1 9 2

Az F#-ban az exception kulcsszóval definiált új kivételtípusok az Exception osztály leszármazottai.

A try...with kifejezés

A try...with kifejezések általános szintaxisa a következoképpen néz ki:

try
    expression1
with
  | pattern1 -> expression2
  | pattern2 -> expression3
  ...

Az elobbi kifejezés használható F#-ban a kivételek kezelésére, amely egyébként hasonló a C#-ban szereplo try...catch utasításhoz. A fenti kódrészletben az expression1 az a kifejezés, ami kivételt válthat ki. Maga a teljes try...with kifejezés egy meghatározott értékkel tér vissza. Amennyiben a védett kódrészletben nem lép fel kivétel, úgy a teljes kifejezés értékét az expression1 kifejezés szolgáltatja. Egy kivétel kiváltódása esetén azonban a fellépo kivétel a with után megadott mintákkal kerül összehasonlításra, és az elso egyezés esetén a mintának megfelelo kifejezés hajtódik végre (ez felel meg a kivételkezelo résznek), így tehát az egész try...with kifejezés értékét a kivételkezelo kifejezés adja meg. Ha a with utáni minták közül egyikre sem sikeres a mintaillesztés, akkor a végrehajtási veremben szereplo hívási lánc szerint terjed tovább felfelé a kivétel egészen addig, amíg a megfelelo kivételkezelo megtalálásra nem kerül. Fontos kiemelni, hogy a kivételkezelokhöz tartozó kifejezések által visszaadott értékek típusának meg kell egyeznie a try-blokkban szereplo kifejezés visszatérési értékének típusával. Gyakran elofordul, hogy egy hiba fellépése során nem tudunk értelmes értéket visszaadni egy kivételkezelo kifejezésben. Ilyenkor általában azt érdemes csinálni, hogy a try-blokk kifejezése által visszaadott érték típusát Option típusúnak választjuk. Az alábbi kódrészlet erre mutat egy példát:

let divide1 x y =
    try
        Some (x / y)
    with
      | :? System.DivideByZeroException -> printfn "Division by zero!"; None

let result1 = divide1 100 0

Mint már korábban említésre került, a kivételek lehetnek .NET-kivételek, illetve F#-kivételek. A kivételkezelokhöz tartozó minták megírásához számos lehetoség kínálkozik, ezeket foglalja össze a következo táblázat.

Minta Leírás
:? kivétel-típus Adott .NET-kivétel típusra illeszkedik.
:? kivétel-típus as azonosító Adott .NET-kivétel típusra illeszkedik, de a kivétel névhez van kötve.
kivétel-név(argumentumok) Adott F#-kivétel típusra illeszkedik, és az argumentumok is kötésre kerülnek.
azonosító Akármilyen kivételre illeszkedik, maga a kivételobjektum ehhez a névhez lesz hozzákötve.
A következovel ekvivalens:
:? System.Exception as azonosító
azonosító when feltétel Akármilyen kivételre illeszkedik, ha a feltétel igaz.

A következő kódrészlet a különböző kivételkezelési mintákra mutat példát:

// Az as kulcsszó használatával egy nevet köthetünk egy .NET-kivételhez
let divide2 x y =
    try
        Some( x / y )
    with
      | :? System.DivideByZeroException as ex -> printfn "Exception! %s " (ex.Message); None

// Egy feltétel használatával ugyanahhoz a kivételhez többféle kivételkezelőt is írhatunk
let divide3 x y flag =
    try
        x / y
    with
      | ex when flag -> printfn "TRUE: %s" (ex.ToString()); 0
      | ex when not flag -> printfn "FALSE: %s" (ex.ToString()); 1

// let result = divide2 100 0
let result = divide3 100 0 true

Fontos megjegyezni, hogy a try...with konstrukció egy önálló kifejezés, így különbözik a try...finally kifejezéstől. Amennyiben mind a with, mind a finally-blokkra szükségünk van, úgy a két kifejezést össze kell illesztenünk.

A try...finally kifejezés

A try...finally kifejezés segítségével olyan kódrészletet írhatunk, amely mindenképpen végrehajtódik, így tehát akkor is, amikor egy blokkban kivétel lépett fel. Az általános szintaxis a következő:

try
    expression1
finally
    expression2

Ez a kifejezés tehát arra használható, hogy az expression2 kifejezésnek megfelelő kód mindenképpen végrehajtódjon függetlenül attól, hogy az expression1 kifejezés futása során keletkezett-e kivétel vagy sem. Az expression2 kifejezés típusa nem vesz részt a teljes kifejezés értékének meghatározásában, vagyis ha nem lép fel kivétel, akkor az expression1 utolsó értéke határozza meg a típust. Ha mégis fellép egy kivétel, akkor nem adódik vissza semmilyen érték, hanem átkerül a vezérlés a hívási lánc szerinti első egyező kivételkezelőhöz. Amennyiben nincs megfelelő kivételkezelő, úgy a program terminál. Mielőtt azonban a megfelelő kivételkezelő lefutna vagy a program terminálna, végrehajtásra kerül a finally-blokk.
A következő kódrészlet a try...finally kifejezés használatára mutat egy példát:

let divide x y =
    let stream : System.IO.FileStream = System.IO.File.Create("test.txt")
    let writer : System.IO.StreamWriter = new System.IO.StreamWriter(stream)
    try
        writer.WriteLine("test1");
        Some( x / y )
    finally
        writer.Flush()
        printfn "Closing stream"
        stream.Close()

let result =
    try
        divide 100 0
    with
      | :? System.DivideByZeroException -> printfn "Exception handled."; None

A konzolra az alábbi szöveg íródik ki:

Closing stream
Exception handled.

Látható, hogy a stream még azelőtt lezárásra került, hogy a (külső) kivétel lekezelődött volna. Így a test.txt nevű fájl a „test1” szöveget tartalmazza, ez pedig azt jelzi, hogy a buffer még az előtt ürítődött ki (és íródott a tartalma a lemezre), hogy a vezérlés átadódott volna a külső kivételkezelő résznek.
A következőkben arra láthatunk példát, hogy hogyan kell a try...with és a try...finally szerkezeteket egyszerre használni (azaz, amikor a with és a finally-blokkokra egyszerre van szükségünk):

exception InnerError of string
exception OuterError of string

let function1 x y =
    try
        try
            if x = y then raise (InnerError("inner"))
            else raise (OuterError("outer"))
        with
          | InnerError(str) -> printfn "Error1 %s" str
    finally
        printfn "Always print this."

let function2 x y =
    try
        function1 x y
    with
      | OuterError(str) -> printfn "Error2 %s" str

function2 100 100
function2 100 10

A kimenet alább látható:

Error1 inner
Always print this.
Always print this.
Error2 outer

A raise függvény

A raise függvény valamilyen hiba vagy kivétel fellépése esetén használható, és a hibával kapcsolatos információt a kivételobjektum tartalmazza. Az általános szintaxis a következő:

raise (expression)

Amikor a raise függvényt meghívjuk, akkor egy kivételobjektum generálódik, és egyúttal megkezdődik a végrehajtási verem visszagörgetésének folyamata. Ezt a folyamatot a .NET futtatókörnyzete, a CLR (Common Language Runtime) felügyeli, ezért a viselkedés is ugyanaz lesz, mint más .NET-es nyelvek esetében. A verem visszagörgetése tulajdonképpen egy olyan kivételkezelő keresését jelenti, amely illeszkedik a fellépő kivételre. Így tehát a keresés az aktuális try...with kifejezésben kezdőik (ha van ilyen). Ekkor a with-blokk összes mintája sorban megvizsgálásra kerül, és sikeres illeszkedés esetén a mintához tartozó kivételkezelő kifejezés hajtódik végre, különben folytatódik tovább a visszagörgetés a hívási lánc szerint addig, amíg meg nem lesz a megfelelő kivételkezelő. Eközben az összes olyan finally-blokk is végrehajtódik, amely a visszagörgetés során előfordult.
A raise függvény ekvivalens a C#-ból ismert throw utasítással. Amennyiben ugyanazt a kivételt még egyszer ki akarjuk váltani egy kivételkezelőben, akkor ehhez a reraise függvényt használhatjuk.
A raise függvénnyel .NET-es kivételek is kiválthatók, ezt mutatja az alábbi példa:

let divide x y =
    if (y = 0) then raise (System.ArgumentException("Divisor cannot be zero!"))
    else
        x / y

A failwith függvény

A failwith függvény egy F#-kivételt vált ki, használata a következő formában történik:

failwith error-message-string

Az error-message-string vagy egy string literál vagy egy string típusú érték, és ez képezi majd a keletkező kivételobjektum Message property-jét.
A failwith függvénnyel kiváltott kivétel típusa Microsoft.FSharp.Core.FailureException, amelyre az F#-kódban Failure névvel hivatkozhatunk. A következő kódrészlet bemutatja, hogy hogyan kell a failwith függvénnyel kivételt kiváltani:

let divideFailwith x y =
    if (y = 0) then failwith "Divisor cannot be zero."
    else
        x / y

let testDivideFailwith x y =
    try
        divideFailwith x y
    with
      | Failure(msg) -> printfn "%s" msg; 0

let result1 = testDivideFailwith 100 0

Az invalidArg függvény

Az invalidArg függvény valamilyen hibás argumentummal kapcsolatos kivételt vált ki, használata a következőképpen történik:

invalidArg parameter-name error-message-string

A parameter-name egy string, amely a hibás paraméter nevére vonatkozik. Az error-message-string pedig szintén egy string, amely a keletkező kivételobjektum Message property-jét képezi.
Az invalidArg függvénnyel kiváltott kivétel típusa System.ArgumentException. Az alábbi kódrészletben láthatjuk, hogy hogyan kell az invalidArg függvénnyel kivételt kiváltani:

let months = [| "January"; "February"; "March"; "April";
    "May"; "June"; "July"; "August"; "September";
    "October"; "November"; "December" |]

let lookupMonth month =
    if (month > 12 || month < 1)
        then invalidArg "month" (sprintf "Value passed in was %d." month)
    months.[month - 1]

printfn "%s" (lookupMonth 12)
printfn "%s" (lookupMonth 1)
printfn "%s" (lookupMonth 13)

Az eredmény a következő (amit a stack trace követ):

December
January
Unhandled Exception: System.ArgumentException: Value passed in was 13.
Parameter name: month