Spis treści:

Kategoria:C#


Kompresja danych w C#

Pojemności dysków twardych i pamięci operacyjnej mogą przyprawić o zawrót głowy. Megabajty, gigabajty, terabajty, a wkrótce pewnie petabajty zadomowią się na dobre w nazwach kolejnych urządzeń. Nie zmienia to jednak faktu, że kompresja danych jest badzo często stosowana i nie wydaje się, by się to kiedykolwiek zmieniło.

Kompresja tablicy bajtów

Zacznę od metody najbardziej ogólnej. Parametrem wejściowym będzie zmienna reprezentująca tablicę bajtów, a rezultatem... również tablica bajtów. Uniwersalne rozwiązanie i jak się potem okaże - najbardziej przydatne.

public static byte[] Pack(byte[] bytes)
{
  using (MemoryStream msOut = new MemoryStream())
  {
    using (GZipStream zippedStream = new GZipStream(msOut, CompressionMode.Compress))
      zippedStream.Write(bytes, 0, bytes.Length);
    return msOut.ToArray();
  }
}

Metoda jest bardzo krótka. Klasa GZipStream (także Deflate) wymaga użycia strumienia i na tym polega cała trudność kompresji. Druga, nie mniej istotna rzecz: kompresja uruchamiana jest dopiero z chwilą zamknięcia strumienia GZipStream. Wymaga to zatem umieszczenia strumienia GZipStream w oddzielnym bloku using, a już przynajmniej wywołania metody Close tego strumienia. Dopiero wtedy komplet spakowanych danych zostanie przeniesiony do leżącego pod spodem strumienia MemoryStream.

Konieczność zamknięcia strumienia wydaje się początkowo dość dziwna. Nie działa nawet znana z implementacji innych strumieni metoda Flush. Powód jest dość prosty - kompresja nie jest uruchamiana sekwencyjnie dla każdego wstawionego w strumień bajtu - plik traktowany jest jako całość, pewien spójny blok. Dopiero pełny plik poddawany jest pod działanie algorytmu, wtedy też liczone są sumy kontrolne i inne wymagane pola z nagłówków, które, co oczywiste, powinny znaleźć się w tablicy bajtów będącej rezultatem operacji.

Dekompresja tablicy bajtów

Kompresja do niczego by się nam nie przydała, gdyby nie możliwość późniejszej dekompresji. Przyjrzyjmy się poniższemu listingowi:

private const int BufferLength = 64 * 4096;
public static byte[] Unpack(byte[] bytes)
{
    using (MemoryStream ms = new MemoryStream(bytes))
    using (MemoryStream msOut = new MemoryStream())
    using (GZipStream zippedStream = new GZipStream(ms, CompressionMode.Decompress))
    {
        byte[] buf = new byte[BufferLength];
        int read;
        while ((read = zippedStream.Read(buf, 0, BufferLength)) > 0)
            msOut.Write(buf, 0, read);
        return msOut.ToArray();
    }
}

Klasa GZipStream wymaga do działania strumienia. Najprostszym strumieniem, a także najbardziej wydajnym, jest MemoryStream. W tym przypadku potrzebujemy dwóch takich strumieni: jeden wejściowy, który będzie dekompresowany, i drugi, wyjściowy, który będzie przyjmował rezultat operacji. Warto zwrócić jeszcze uwagę na wewnętrzną pętlę. Po co ona tam jest? Dlatego, że nie znamy wynikowego rozmiaru obiektu po dekompresji. O ile w przypadku kompresji znaliśmy rozmiar tablicy wejściowej i mogliśmy tę wartość przekazać do metody Write, o tyle w tym przypadku rozmiar jest niewiadomą.

Kompresja (prawie) dowolnych obiektów

Kompresja tablicy bajtów nie zawsze jest tym, czego oczekujemy. Zdarza się, że w aplikacji korzystamy z ogrmonych struktur, które warto skompresować przed zapisaniem na dysku, w bazie danych czy też przed wysłaniem ich przez sieć. Rozpatrzmy kolejny przykład:

public static byte[] Pack(object objectToPack)
{
    using (MemoryStream msOut = new MemoryStream())
    {
        using (GZipStream zippedStream = new GZipStream(msOut, CompressionMode.Compress))
        {
            BinaryFormatter f = new BinaryFormatter();
            f.Serialize(zippedStream, objectToPack);
        }
        return msOut.ToArray();
    }
}

Tym razem kod uzupełniony został o jeden ważny element: klasę BinaryFormatter. Jej zadaniem jest serializacja (zamiana na postać binarną) obiektów. Co nas bardzo cieszy, klasa do serializacji może zapisywać wynik operacji bezpośrednio w strumieniu. Tym strumieniem jest GZipStream. To w zasadzie tyle. Prawie tyle. Należy jeszcze pamiętać o oznaczeniu kompresowanego obiektu atrybutem Serializable.

Aby mechanizmy .NET mogły przeprowadzić serializację, należy użyć atrybutu pokazanego poniżej:

[Serializable]
public class TestClass
{
    public int[] Nums;
    public string Str;
}
Najczęściej stosuje się serializację binarną i XML, ale nic nie stoi na przeszkodzie, aby samodzielnie przeprowadzić taką serializację. Własna serializacja jest najczęściej lepiej dostosowana do charakterystyki obiektu i pozwala go zapisać w sposób bardziej zwięzły. Skąd .NET ma wiedzieć, że chcemy sami przeprowadzić serializację? Służą do tego atrybuty OnDeserializedAttribute, OnDeserializingAttribute, OnSerializedAttribute oraz OnSerializingAttribute. Szczegółowy opis tej techniki wykracza poza ramy tego artykułu.

Skoro skompresowaliśmy obiekt, to wypadałoby go po jakimś czasei zdekompresować.

Dekompresja obiektów

Wydaje mi się, że sposób postępowania jest łatwy do przewidzenia. Najpierw należy dane zdekompresować, a następnie przeprowadzić operację odwrotną do serializacji - deserializację. Przykład pokazany jest na poniższym listingu:

public static T Unpack<T>(byte[] bytes)
{
    using (MemoryStream ms = new MemoryStream(bytes))
    using (MemoryStream msOut = new MemoryStream())
    using (GZipStream zippedStream = new GZipStream(ms, CompressionMode.Decompress))
    {
        byte[] buf = new byte[BufferLength];
        int read;
        while ((read = zippedStream.Read(buf, 0, BufferLength)) > 0)
            msOut.Write(buf, 0, read);
        BinaryFormatter f = new BinaryFormatter();
        msOut.Position = 0;
        return (T)f.Deserialize(msOut); ;
    }
}

Kod różni się od zwykłej dekompresji tylko tym, że zamiast zwracać tablicę bajtów, konwertuje te bajty na docelowy obiekt. Ważne jest tutaj przestawienie bieżącej pozycji w strumieniu msOut. Po dekompresji wskaźnik ustawiony jest na końcu, czyli w miejscu, w którym mogłyby być wpisywane dodatkowe dane. My dane chcemy odczytać, więc przestawiamy ten wskaźnik na sam początek.

Druga rzecz - deserializacja, choć jest w stanie rozpoznać typ obiektu, zwraca ogólny typ object. Aby metoda była silnie typowana, zastosowałem mechanizm typów ogólnych. Dzięki temu otrzymujemy dokładnie taki obiekt, jakiego oczekujemy. Jeżeli komuś nie odpowiada takie rozwiązanie, zawsze może zamienić typ zwracany przez metodę na object i pominąć rzutowanie w ostatniej linijce.

Kategoria:C#

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?