Ošetření výjimek při procházení sekvence

Tomáš Holan       10. 10. 2011             5666 zobrazení

Jak víme například z tohoto příspěvku z dřívějška, pokud je při zpracovávání sekvenčních dat (například objektů generovaných LINQ dotazem) vyhozena výjimka, celé zpracování se tím ukončí. Pokud budeme ale takto zpracovávat například řádky při importu ze vstupního CSV souboru, můžeme například chtít v případě chybného řádku zalogovat na jakém řádku chyba vznikla nebo dokonce chybný řádek pouze přeskočit a pokračovat v importu řádků dalších. Celá situace může vypadat například takto:

static void Main()
{
    var lines = new string[] { "Item 1;1", "Item 2;2", "Item 3;bad value", "Item 4;4" };

    int i = 0;
    foreach (var row in from line in lines
                        let values = line.Split(';')
                        select new
                        {
                            Name = values[0],
                            Value = Int32.Parse(values[1])  //<--FormatException pro "Item 3"
                        })
    {
        //TODO: Zajistit vynechání chybných řádků
        //try
        //{
            //Process row
            Console.WriteLine("Name: {0}, Value: {1}", row.Name, row.Value);
        //}
        //catch (FormatException)
        //{
        //    Console.WriteLine("Chybná hodnota na řádku {0}.", i + 1);
        //}
        i++;
    }
}

Co s tím? Ještě jednou v čem je tedy problém – ten je v tom, že výjimka je vyhozena z volání MoveNext() na zdrojovém enumerátoru, které je voláno interně z použitého foreach tj. mimo blok, který zpracovává již jednotlivé (a pouze korektně získané) prvky.

Jedno řešení je nepoužít foreach a přistupovat k enumerátoru napřímo. Toto řešení má ale dost nevýhod a navíc je principiálně špatné (vysvětlím pod ukázkou). Přesto zde uvedu jak by to celé vypadalo:

(POZOR: Tento kód nepoužívejte.)

static void Main()
{
    var lines = new string[] { "Item 1;1", "Item 2;2", "Item 3;bad value", "Item 4;4" };

    var source = from line in lines
                 let values = line.Split(';')
                 select new
                 {
                     Name = values[0],
                     Value = Int32.Parse(values[1]) //<--FormatException pro "Item 3"
                 };

    var enumerator = source.GetEnumerator();
    try
    {
        int i = 0;
        while (true)
        {
            try
            {
                if (!enumerator.MoveNext())
                {
                    break;
                }

                var row = enumerator.Current;

                //Process row
                Console.WriteLine("Name: {0}, Value: {1}", row.Name, row.Value);
            }
            catch (FormatException)
            {
                Console.WriteLine("Chybná hodnota na řádku {0}.", i + 1);
            }
            i++;
        }
    }
    finally
    {
        ((IDisposable)enumerator).Dispose();
    }
}

Hlavní nevýhody jsou tyto:

  • Kód je složitý, nepřehledný, dlouhý i velmi těžce udržovatelný. Kód obsahuje více logiky na ošetření přístupu k enumerátoru než logiky vlastního zpracování řádků, která se v něm navíc naprosto ztrácí.
  • Navíc logika přístupu k enumerátoru není (nebo je alespoň velmi těžko) oddělitelná od vlastní logiky importu. Pokud by jsme potřebovali napsat více podobných importů museli by jsme vždy vyjít z tohoto kódu, což by jsme rozhodně nechtěli.
  • Kód nedodržuje základní kontrakt (konkrétně bod 4) pro interface IEnumerable<T>. To že výjimku vyhozenou z metody MoveNext() ignorujeme a pokračujeme ve zpracování není jistě z tohoto pohledu správné. Zde je možná “validní” pouze v případě našeho konkrétního enumerátoru a pouze protože chytáme specifickou FormatException, obecně si ale nemůžeme být jistí tím, že je po zachycení výjimky stav enumerátoru stále platný.

Toto řešení tedy z výše uvedených důvodů nedoporučuji.

Jiným řešení je ze zdrojového enumerátoru, tj. uvnitř našeho LINQ dotazu, vytvářet a vracet korektní objekty i v případě očekávaných chyb parsování apod. Toho obecně docílíme použitím nějakého “wrapperu” nad vlastním prvkem sekvence. Ten by nám pak musel daný prvek zpřístupnit nebo zpřístupnit pouze výjimku, vzniklou při pokusu o konstrukci prvku. Jako tento wrapper můžeme napsat konkrétní třídu pro data tohoto konkrétního importu nebo nějakou obecnou třídu zaobalující takovýto způsob přístupu k vnitřnímu prvku, nejvýhodnější a zároveň i nejjednodušší je však použít nějakou třídu standardní – zde konkrétně použijeme třídu Lazy<T> z .NET Frameworku 4.0.

Aby šlo konstruovat instance třídy Lazy<T> i v našem případě, kde je pro vlastní hodnotu prvku použit anonymní typ, tj. aby nebylo nutné uvádět generický argument pro typ T, musíme si nejprve ještě udělat malou přípravu v podobě pomocné třídy (tento známý trik je vysvětlen např. zde):

namespace System
{
    public static class Lazy
    {
        #region action methods
        public static Lazy<T> Create<T>(Func<T> valueFactory)
        {
            return new Lazy<T>(valueFactory);
        }
        #endregion
    }
}

S použitím této pomocné třídy nám již nic nebrání upravit původní příklad do finálního řešení:

static void Main()
{
    var lines = new string[] { "Item 1;1", "Item 2;2", "Item 3;bad value", "Item 4;4" };

    int i = 0;
    foreach (var row in from line in lines
                        let values = line.Split(';')
                        select Lazy.Create(() => new
                        {
                            Name = values[0],
                            Value = Int32.Parse(values[1])  //<--FormatException pro "Item 3"
                        }))
    {
        try
        {
            //Process row
            Console.WriteLine("Name: {0}, Value: {1}", row.Value.Name, row.Value.Value);
        }
        catch (FormatException)
        {
            Console.WriteLine("Chybná hodnota na řádku {0}.", i + 1);
        }
        i++;
    }
}

Pro úplnost ještě doplním i výstup z tohoto příkladu:

Name: Item 1, Value: 1
Name: Item 2, Value: 2
Chybná hodnota na řádku 3.
Name: Item 4, Value: 4

Výhody tohoto řešení jsou hned patrné: Kód je přehledný a jednoduchý, obsahuje pouze logiku vlastního importu a jedná se o velmi lehce a obecně aplikovatelný způsob.

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback