Felhantering och Debugging i Lua

I denna lektion lär du dig hur du kan hantera fel, undantag och felsöka dina Lua-program.

Förstå fel i Lua

När du skriver kod kommer du oundvikligen att stöta på fel. Det finns flera typer av fel i programmering:

  • Syntaxfel - Kod som bryter mot språkets grammatik och förhindrar programmet från att starta
  • Körningsfel - Fel som upptäcks medan programmet körs (t.ex. division med noll)
  • Logiska fel - Programmet kraschar inte, men ger fel resultat på grund av felaktig logik

I Lua är felhantering viktig för att skapa robusta program som inte kraschar vid oväntade situationer.

Generera fel

Du kan medvetet generera ett fel i Lua med funktionen error():

function divide(a, b)
    if b == 0 then
        error("Division med noll är inte tillåtet")
    end
    return a / b
end

-- Använda funktionen
print(divide(10, 2))  -- Fungerar: 5
print(divide(10, 0))  -- Genererar ett fel: "Division med noll är inte tillåtet"

Du kan även ange en nivå som andra parameter till error() för att påverka var i anropsstacken felet visas:

function validateAge(age)
    if age < 0 then
        error("Ålder kan inte vara negativ", 2)  -- Nivå 2 pekar på den som anropar funktionen
    end
    return true
end
Tips: Använd informativa felmeddelanden som förklarar vad som gick fel och vad användaren kan göra för att rätta till problemet.

Fånga fel med pcall

För att förhindra att ditt program kraschar när fel uppstår kan du använda pcall (protected call):

-- Försöker köra en funktion som kan generera fel
local success, result = pcall(divide, 10, 0)

if success then
    print("Resultatet är: " .. result)
else
    print("Ett fel uppstod: " .. result)
end

pcall returnerar två värden:

  1. En boolean som indikerar om funktionen kördes utan fel (success)
  2. Antingen resultatet av funktionen (om lyckades) eller felmeddelandet (om misslyckades)

Detta är liknande try/catch i andra språk:

-- Använda pcall för att fånga fel vid filöppning
local function readFile(filename)
    local file = assert(io.open(filename, "r"))
    local content = file:read("*all")
    file:close()
    return content
end

local success, content = pcall(readFile, "finns_inte.txt")
if success then
    print("Filinnehåll: " .. content)
else
    print("Kunde inte läsa filen: " .. content)
end

Använda xpcall med felhanterare

För mer kontroll över felhanteringen kan du använda xpcall, som låter dig ange en felhanteringsfunktion:

-- Definiera en felhanteringsfunktion som visar felmeddelande och stack trace
local function errorHandler(err)
    print("ERROR: " .. tostring(err))
    print("Stack trace:")
    print(debug.traceback("", 2)) -- Visa stack trace, hoppa över denna funktion
    return "Felhantering slutförd"
end

-- Funktionen som kan generera fel
local function riskyFunction(n)
    print("Startar riskfylld funktion...")
    assert(type(n) == "number", "Parameter måste vara ett nummer")
    print("n = " .. n)
    print(n / 0) -- Detta genererar ett fel
    print("Detta körs aldrig") -- Kod efter fel körs aldrig
end

-- Använd xpcall med vår felhanterare
local success, result = xpcall(riskyFunction, errorHandler, 10)

print("xpcall resultat: " .. tostring(success))
print("Returvärde: " .. tostring(result))
Obs! debug.traceback() kräver debug-biblioteket som kanske inte är tillgängligt i alla Lua-miljöer.

Assert-funktionen

assert är en praktisk funktion för att kontrollera villkor och generera fel om villkoret inte uppfylls:

-- Verifierar att ett villkor är sant, annars genererar det ett fel
function sqrt(x)
    assert(x >= 0, "Kan inte beräkna roten ur ett negativt tal")
    return x^0.5
end

-- Exempel på användning
print(sqrt(16))   -- 4
print(sqrt(-1))   -- Fel: "Kan inte beräkna roten ur ett negativt tal"

assert tar två parametrar:

  1. Ett villkor som utvärderas till sant eller falskt
  2. Ett valfritt felmeddelande som visas om villkoret är falskt

Dette är ett enkelt sätt att validera indata och parametrar:

function processUser(user)
    assert(type(user) == "table", "User måste vara en tabell")
    assert(user.name, "User saknar namnfält")
    assert(type(user.age) == "number", "User.age måste vara ett nummer")
    assert(user.age >= 0, "User.age kan inte vara negativ")
    
    -- Om alla assertions passerar, fortsätt med funktionen
    print("Bearbetar användare: " .. user.name)
end

Debugging-tekniker

När du behöver felsöka Lua-kod finns det flera tillvägagångssätt:

1. Print-debugging

Den enklaste formen av debugging är att lägga till print-uttalanden för att se värden och programmets flöde:

function komplexFunktion(a, b)
    print("komplexFunktion startad med a=" .. a .. ", b=" .. b)
    
    local resultat = a * b
    print("Efter multiplikation: " .. resultat)
    
    if resultat > 100 then
        print("Resultat > 100, ökar med 50")
        resultat = resultat + 50
    else
        print("Resultat <= 100, minskar med 10")
        resultat = resultat - 10
    end
    
    print("Slutligt resultat: " .. resultat)
    return resultat
end

2. Använda debug-biblioteket

Lua har ett inbyggt debug-bibliotek för mer avancerad felsökning:

-- Visa lokal variabelinformation
function debugLocals()
    local variables = {}
    local i = 1
    
    while true do
        local name, value = debug.getlocal(2, i)
        if not name then break end
        variables[name] = value
        i = i + 1
    end
    
    return variables
end

function testFunction()
    local a = 10
    local b = 20
    local c = "test"
    
    local vars = debugLocals()
    for name, value in pairs(vars) do
        print(name .. " = " .. tostring(value))
    end
end

testFunction()

3. Stack Trace

När fel uppstår kan du generera en stack trace för att se var i koden felet inträffade:

function skrivStackTrace()
    print("Stack trace:")
    print(debug.traceback())
end

function funktionA()
    print("I funktionA")
    funktionB()
end

function funktionB()
    print("I funktionB")
    funktionC()
end

function funktionC()
    print("I funktionC")
    skrivStackTrace()
end

funktionA()

Tips för bättre felhantering

  1. Validera indata - Kontrollera alltid användarinmatning och funktionsparametrar
  2. Använd beskrivande felmeddelanden - Felmeddelanden bör vara tydliga om vad som gick fel
  3. Återställ resurser - Se till att filer stängs och resurser frigörs även om fel uppstår
  4. Loggning - Implementera felloggar så att du kan spåra problem i produktionsmiljö
  5. Testa gränsfall - Skriv tester för oväntade eller extrema indatavärden
-- Exempel på bra felhantering med resursfrigivning
function readAndProcessFile(filename)
    -- Validera indata
    assert(type(filename) == "string", "Filnamnet måste vara en sträng")
    
    -- Försök öppna filen med pcall
    local success, file = pcall(io.open, filename, "r")
    if not success then
        return nil, "Kunde inte öppna filen: " .. file
    end
    
    -- Använd pcall för att läsa filen och fånga eventuella fel
    local success, content = pcall(function()
        local data = file:read("*all")
        file:close()  -- Stäng filen när vi är klara
        return data
    end)
    
    -- Om fel uppstod vid läsning, se till att filen fortfarande stängs
    if not success then
        pcall(file.close, file)  -- Försök stänga filen även om ett fel uppstod
        return nil, "Fel vid läsning av filen: " .. content
    end
    
    -- Bearbeta innehållet (fiktiv funktion)
    local success, result = pcall(processContent, content)
    if not success then
        return nil, "Fel vid bearbetning av innehåll: " .. result
    end
    
    return result
end

Övningar

Övning 1: Robust Division

Skapa en funktion safeDivide som utför division men hanterar fel på ett robust sätt:

  1. Ta emot två parametrar: täljare och nämnare
  2. Kontrollera att båda parametrarna är nummer
  3. Kontrollera att nämnaren inte är noll
  4. Returnera resultatet om allt är OK, annars returnera nil och ett felmeddelande
  5. Skapa ett testprogram som anropar funktionen med olika värden, både giltiga och ogiltiga

Lösningsförslag:

-- Robust division med felhantering
function safeDivide(numerator, denominator)
    -- Kontrollera att båda parametrarna är nummer
    if type(numerator) ~= "number" then
        return nil, "Täljaren måste vara ett nummer"
    end
    
    if type(denominator) ~= "number" then
        return nil, "Nämnaren måste vara ett nummer"
    end
    
    -- Kontrollera division med noll
    if denominator == 0 then
        return nil, "Division med noll är inte tillåtet"
    end
    
    -- Utför divisionen säkert
    return numerator / denominator
end

-- Testprogram
function testSafeDivide(a, b)
    print("Försöker dela " .. tostring(a) .. " med " .. tostring(b))
    
    local result, error_msg = safeDivide(a, b)
    
    if result then
        print("Resultat: " .. result)
    else
        print("Fel: " .. error_msg)
    end
    print("---")
end

-- Testa med giltiga värden
testSafeDivide(10, 2)     -- Ska ge 5
testSafeDivide(-10, 5)    -- Ska ge -2
testSafeDivide(7, 3.5)    -- Ska ge 2

-- Testa med ogiltiga värden
testSafeDivide(10, 0)     -- Division med noll
testSafeDivide("10", 5)   -- Fel täljare
testSafeDivide(10, "5")   -- Fel nämnare
testSafeDivide({}, true)  -- Båda parametrarna är fel

Övning 2: Filhanterare med felhantering

Skapa ett program för filhantering som använder felhantering på ett robust sätt:

  1. Skapa en funktion copyFile som kopierar innehållet från en fil till en annan
  2. Implementera felhantering för att hantera problem som:
    • Källfilen finns inte
    • Kan inte skapa målfilen
    • Läsfel eller skrivfel
  3. Visa beskrivande felmeddelanden som talar om vad som gick fel
  4. Se till att alla filer stängs korrekt, även om fel uppstår

Lösningsförslag:

-- En robust filkopieringsfunktion med felhantering
function copyFile(sourceFile, targetFile)
    -- Validera indata
    if type(sourceFile) ~= "string" or type(targetFile) ~= "string" then
        return false, "Filnamnen måste vara strängar"
    end
    
    -- Öppna källfilen
    local sourceHandle, sourceError = io.open(sourceFile, "rb")
    if not sourceHandle then
        return false, "Kunde inte öppna källfilen: " .. sourceError
    end
    
    -- Använd pcall för att garantera att källfilen stängs
    local sourceContent, contentError = (function()
        local content = sourceHandle:read("*all")
        sourceHandle:close()
        return content
    end)()
    
    if not sourceContent then
        return false, "Kunde inte läsa från källfilen: " .. tostring(contentError)
    end
    
    -- Öppna målfilen
    local targetHandle, targetError = io.open(targetFile, "wb")
    if not targetHandle then
        return false, "Kunde inte öppna målfilen för skrivning: " .. targetError
    end
    
    -- Använd pcall för att garantera att målfilen stängs
    local success, writeError = pcall(function()
        targetHandle:write(sourceContent)
        targetHandle:close()
    end)
    
    if not success then
        -- Försök stänga filen om den fortfarande är öppen
        pcall(function() targetHandle:close() end)
        return false, "Kunde inte skriva till målfilen: " .. tostring(writeError)
    end
    
    return true, "Filen kopierades framgångsrikt"
end

-- Testprogram
function testCopyFile(source, target)
    print("Försöker kopiera '" .. source .. "' till '" .. target .. "'")
    
    local success, message = copyFile(source, target)
    
    if success then
        print("Framgång: " .. message)
    else
        print("Fel: " .. message)
    end
    print("---")
end

-- Testa med olika scenarier
testCopyFile("exempeltext.txt", "kopia.txt")       -- Antar att denna fil finns
testCopyFile("finns_inte.txt", "kopia.txt")        -- Fil som inte finns
testCopyFile("exempeltext.txt", "/otillåten/sökväg/kopia.txt")  -- Ogiltig sökväg
testCopyFile(123, "kopia.txt")                     -- Ogiltigt filnamn