Metodtips för undantag

En väl utformad app hanterar undantag och fel för att förhindra appkrascher. I den här artikeln beskrivs metodtips för att hantera och skapa undantag.

Använd try/catch/finally-block för att återställa från fel eller frigöra resurser

Använd try/catch block runt kod som potentiellt kan generera ett undantag, och koden kan återställas från det undantaget. I catch block beställer du alltid undantag från de mest härledda till de minst härledda. Alla undantag härleds från Exception klassen. Fler härledda undantag hanteras inte av en catch-sats som föregås av en catch-sats för en bas undantagsklass. När koden inte kan återställas från ett undantag ska du inte fånga det undantaget. Aktivera metoder längre upp i anropsstacken för att återställa om möjligt.

Rensa resurser som allokeras med antingen using instruktioner eller finally block. Föredrar using instruktioner för att automatiskt rensa resurser när undantag utlöses. Använd finally block för att rensa resurser som inte implementerar IDisposable. Kod i en finally sats körs nästan alltid även när undantag utlöses.

Hantera vanliga villkor utan att utlösa undantag

För villkor som sannolikt inträffar men som kan utlösa ett undantag bör du överväga att hantera dem på ett sätt som undviker undantaget. Om du till exempel försöker stänga en anslutning som redan är stängd får du en InvalidOperationException. Du kan undvika det genom att använda en if instruktion för att kontrollera anslutningstillståndet innan du försöker stänga det.

if (conn->State != ConnectionState::Closed)
{
    conn->Close();
}
if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

Om du inte kontrollerar anslutningstillståndet innan du stänger kan du fånga undantaget InvalidOperationException .

try
{
    conn->Close();
}
catch (InvalidOperationException^ ex)
{
    Console::WriteLine(ex->GetType()->FullName);
    Console::WriteLine(ex->Message);
}
try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

Vilken metod du ska välja beror på hur ofta du förväntar dig att händelsen ska inträffa.

  • Använd undantagshantering om händelsen inte inträffar ofta, dvs. om händelsen verkligen är exceptionell och indikerar ett fel, till exempel ett oväntat filslut. När du använder undantagshantering körs mindre kod under normala förhållanden.

  • Kontrollera om det finns feltillstånd i koden om händelsen inträffar rutinmässigt och kan betraktas som en del av normal körning. När du söker efter vanliga feltillstånd körs mindre kod eftersom du undviker undantag.

Utforma klasser så att undantag kan undvikas

En klass kan tillhandahålla metoder eller egenskaper som gör att du kan undvika att göra ett anrop som utlöser ett undantag. En klass innehåller till exempel FileStream metoder som hjälper dig att avgöra om slutet av filen har nåtts. Dessa metoder kan användas för att undvika undantaget som utlöses om du läser förbi slutet av filen. I följande exempel visas hur du läser till slutet av en fil utan att utlösa ett undantag:

class FileRead
{
public:
    void ReadAll(FileStream^ fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == nullptr)
        {
            throw gcnew System::ArgumentNullException();
        }

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead->Seek(0, SeekOrigin::Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead->Length; i++)
        {
            b = fileToRead->ReadByte();
            Console::Write(b.ToString());
            // Or do something else with the byte.
        }
    }
};
class FileRead
{
    public void ReadAll(FileStream fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == null)
        {
            throw new ArgumentNullException();
        }

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

Ett annat sätt att undvika undantag är att returnera null (eller standard) för de vanligaste felfallen i stället för att utlösa ett undantag. Ett vanligt felfall kan betraktas som ett normalt kontrollflöde. Genom att returnera null (eller standard) i dessa fall minimerar du prestandapåverkan för en app.

För värdetyper är det något att tänka på för din app om du vill använda Nullable<T> eller standard som felindikator. Genom att använda Nullable<Guid>blir default i null stället för Guid.Empty. Ibland kan tillägg Nullable<T> göra det tydligare när ett värde finns eller saknas. Andra gånger kan tillägg Nullable<T> skapa extra ärenden för att kontrollera att de inte är nödvändiga och endast användas för att skapa potentiella felkällor.

Generera undantag i stället för att returnera en felkod

Undantag säkerställer att felen inte går obemärkt förbi eftersom den anropande koden inte kontrollerade en returkod.

Använd de fördefinierade .NET-undantagstyperna

Introducera endast en ny undantagsklass när en fördefinierad klass inte gäller. Till exempel:

  • Om en egenskapsuppsättning eller ett metodanrop inte är lämpligt med tanke på objektets aktuella tillstånd utlöser du ett InvalidOperationException undantag.
  • Om ogiltiga parametrar skickas utlöser du ett ArgumentException undantag eller någon av de fördefinierade klasserna som härleds från ArgumentException.

Avsluta undantagsklassnamn med ordet Exception

När ett anpassat undantag krävs namnger du det på rätt sätt och härleder det från Exception klassen. Till exempel:

public ref class MyFileNotFoundException : public Exception
{
};
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

Inkludera tre konstruktorer i anpassade undantagsklasser

Använd minst tre vanliga konstruktorer när du skapar dina egna undantagsklasser: den parameterlösa konstruktorn, en konstruktor som tar ett strängmeddelande och en konstruktor som tar ett strängmeddelande och ett inre undantag.

Ett exempel finns i Så här skapar du användardefinierade undantag.

Se till att undantagsdata är tillgängliga när koden körs via fjärranslutning

När du skapar användardefinierade undantag kontrollerar du att metadata för undantagen är tillgängliga för kod som körs via fjärranslutning.

Om till exempel .NET-implementeringar som stöder appdomäner kan undantag inträffa mellan appdomäner. Anta att appdomän A skapar appdomän B, som kör kod som utlöser ett undantag. För att appdomän A ska kunna fånga och hantera undantaget korrekt måste den kunna hitta sammansättningen som innehåller undantaget som genereras av appdomän B. Om appdomän B utlöser ett undantag som finns i en sammansättning under dess programbas, men inte under appdomänen A:s programbas, kommer appdomän A inte att kunna hitta undantaget, och den vanliga språkkörningen utlöser ett FileNotFoundException undantag. För att undvika den här situationen kan du distribuera sammansättningen som innehåller undantagsinformationen på något av två sätt:

  • Placera sammansättningen i en gemensam programbas som delas av båda appdomänerna.
  • Om domänerna inte delar en gemensam programbas signerar du sammansättningen som innehåller undantagsinformationen med ett starkt namn och distribuerar sammansättningen till den globala sammansättningscacheminnet.

Använda grammatiskt korrekta felmeddelanden

Skriv tydliga meningar och inkludera avslutande skiljetecken. Varje mening i strängen som tilldelats egenskapen Exception.Message ska sluta i en period. "Loggtabellen har till exempel svämmat över." skulle vara en lämplig meddelandesträng.

Inkludera ett lokaliserat strängmeddelande i varje undantag

Felmeddelandet som användaren ser härleds från Exception.Message egenskapen för undantaget som utlöstes och inte från namnet på undantagsklassen. Vanligtvis tilldelar du ett värde till Exception.Message egenskapen genom att skicka meddelandesträngen message till argumentet för en undantagskonstruktor.

För lokaliserade program bör du ange en lokaliserad meddelandesträng för varje undantag som programmet kan utlösa. Du använder resursfiler för att ange lokaliserade felmeddelanden. Information om hur du lokaliserar program och hämtar lokaliserade strängar finns i följande artiklar:

I anpassade undantag anger du ytterligare egenskaper efter behov

Ange ytterligare egenskaper för ett undantag (utöver den anpassade meddelandesträngen) endast när det finns ett programmatiskt scenario där ytterligare information är användbar. Till FileNotFoundException exempel tillhandahåller FileName egenskapen .

Placera throw-instruktioner så att stackspårningen kan vara till hjälp

Stackspårningen börjar vid -instruktionen där undantaget genereras och slutar vid -instruktionen catch som fångar undantaget.

Använda undantagsverktygets metoder

Det är vanligt att en klass genererar samma undantag från olika platser i implementeringen. Undvik överdriven kod genom att använda hjälpmetoder som skapar undantaget och returnerar det. Till exempel:

ref class FileReader
{
private:
    String^ fileName;

public:
    FileReader(String^ path)
    {
        fileName = path;
    }

    array<Byte>^ Read(int bytes)
    {
        array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes);
        if (results == nullptr)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException^ NewFileIOException()
    {
        String^ description = "My NewFileIOException Description";

        return gcnew FileReaderException(description);
    }
};
class FileReader
{
    private string fileName;

    public FileReader(string path)
    {
        fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(fileName, bytes);
        if (results == null)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

I vissa fall är det mer lämpligt att använda undantagets konstruktor för att skapa undantaget. Ett exempel är en global undantagsklass som ArgumentException.

Återställningstillstånd när metoderna inte slutförs på grund av undantag

Anropare bör kunna anta att det inte finns några biverkningar när ett undantag genereras från en metod. Om du till exempel har kod som överför pengar genom att ta ut från ett konto och sätta in på ett annat konto, och ett undantag utlöses när insättningen utförs, vill du inte att uttaget ska gälla.

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

Föregående metod utlöser inga undantag direkt. Du måste dock skriva metoden så att uttagen återförs om insättningsåtgärden misslyckas.

Ett sätt att hantera den här situationen är att fånga upp eventuella undantag som utlöses av insättningstransaktionen och återställa uttagen.

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

Det här exemplet illustrerar användningen av throw för att återväxa det ursprungliga undantaget, vilket gör det lättare för anropare att se den verkliga orsaken till problemet utan att behöva undersöka InnerException egenskapen. Ett alternativ är att utlösa ett nytt undantag och inkludera det ursprungliga undantaget som det inre undantaget.

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

Samla in undantag för återväxt senare

Om du vill samla in ett undantag och bevara dess anropsstack för att kunna återväxa det senare använder du System.Runtime.ExceptionServices.ExceptionDispatchInfo klassen. Den här klassen innehåller följande metoder och egenskaper (bland annat):

I följande exempel visas hur ExceptionDispatchInfo klassen kan användas och hur utdata kan se ut.

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

Om filen i exempelkoden inte finns skapas följande utdata:

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

Se även