piątek, 14 grudnia 2012

C# konwersja postaci bitowej tablicy bajtów do stringa

Ostatnio mam przyjemność pracować nad kodem, który pisał człowiek jeszcze studiujący. Zadziwia mnie czasem na jakie piękne rozwiązania natrafiam. Dosłownie przed chwilą przerobiłem metodę zwracającą ciąg znaków reprezentujących postać binarną pięciobajtowej ramki. Pozwolę sobie przedstawić jak nie powinno się czegoś takiego robić i jak można wygodnie przekonwertować tablicę bajtów do ciągu znaków w postaci binarnej.

Ramka jest reprezentowana przez klasę Frame, która zasadniczo głównie przechowuje binarną reprezentację i dostarcza metod i własności do pobierania informacji o ramce i pewnych na niej manipulacji. W pewnych warunkach ramki są szeregowane do plików tekstowych. W związku z tym istnieje parę metod do pozyskiwania ciągów znakowych ze składowych ramki.

Zastanawiać może czemu użyte zostało generowanie ciągu binarnego. Przecież można zapisać wartość dziesiętną bajtów, nie będzie to stwarzało kłopotów przy parsowaniu. Sam się nad tym chwilę czasu zastanawiałem. Okazuje się, że służy to ułatwieniu interpretacji danych człowiekowi. Część informacji jest zapisana na poziomie pól bitowych, więc zapis w postaci binarnej pozwala na interpretację tych danych bez przeliczania.

Kod który zastałem w metodzie ToBitString() co nieco mnie zaskoczył. Oto co zastałem:

            String tmp = "";
            foreach (byte b in _btFrame)
            {
                var flags = new BitArray(8);
                var counter = 8;
                var idx = 0;
                byte n = b;
                while (counter > 0)
                {
                    flags.Set(idx++, ((n & 1) == 1));
                    n >>= 1;
                    counter--;
                }

                for (int i = flags.Length - 1; i >= 0; i--)
                {
                    tmp += flags[i] ? 1 : 0;
                }
                tmp += "  ";
            }
            return tmp;


Nie trudno zauważyć co jest w tym miejscu źle. Po pierwsze złożoność kodu. Zastosowane zostały dwie pętle zagnieżdżone, więc można oczekiwać złożoności kwadratowej. Wiemy jednak, że ramka zawiera 5 bajtów, a każdy z bajtów posiada 8 bitów, czyli mamy dokładnie 40 przebiegów. Mimo wszystko cieszę się, że kolega nie pokusił się o rozwinięcie pętli dla pozbycia się hazardów. Chyba nie robi się takich rzeczy w .NET.

Druga kwestia, która mnie wprawiła w konsternację to użycie klasy BitArray. Aż musiałem poszukać jakie super moce klasa ta posiada i jakie tajne właściwości są tu użyte aby zrozumieć jej wykorzystanie. Tego niestety mi się nie udało. Została ona tu użyta, ponieważ operacje niższego poziomu, dostępne w .NET oczywiście, nie są zbyt znane i lubiane przez autora, najwyraźniej.

Ostatnia sprawa to długość i zawiłość kodu. To czego oczekujemy od tej metody jest stosunkowo proste i jasne. Chcemy tylko jeden ciąg znaków, którego długość stosunkowo łatwo określić. Łatwo postawione zadanie zostało stosunkowo mocno skomplikowane. Analiza takiego kodu może nastręczyć nieco kłopotu z rana, zanim kawa zrobi swoje.

Postanowiłem kod uprościć. Każdy bardziej ogarnięty programista zauważy, lub już po prostu wie, że zadania tego rodzaju można upchnąć w mniejszej liczbie linii kodu. W przypadku .NET da się to zrobić w jednej linii kodu. Wymaga to wykorzystania Linq, które osobiście bardzo polubiłem. Jedyna kwestia jest taka, że jako programista, a nie architekt baz danych, stosuję zapis nie przypominający popierdzielonego zapytania sql. Po poprawce ciało metody ToBitString() wygląda tak:

return string.Join(" ", _btFrame.Select(bt => Convert.ToString(bt, 2).PadLeft(8, '0')));
Jak wydać nie użyłem klasy StringBuilder. Wygląda na to, że Join jest stosunkowo szybki. W prawdzie wolniejszy niż operacje na StringBuilderze, jednak wolałem pozostać przy pojedynczej linii kodu, bo jest on niezauważalnie wolniejszy od zawierającego wykorzystanie klasy StringBuilder. Dla ciekawskich jeszcze kod z wykorzystaniem StringBuildera:

            StringBuilder sb = new StringBuilder();
            var tmp = _btFrame.Select(bt => Convert.ToString(bt, 2).PadLeft(8, '0'));
            foreach (var s in tmp)
                sb.AppendFormat("{0} ", s);
            sb.Remove(sb.Length - 1, 1);
            return sb.ToString();

Jedyny problem z powyższym kodem, to usunięcie ostatniego znaku spacji, który jest dodawany niepotrzebnie. Nie chciało mi się dodawać jakiś specjalnych warunków w pętli, albo specjalnie usuwać z kolekcji 'tmp' ostatniego elementu. Myślę, że Remove na obiekcie klasy StringBuilder jest dostatecznie szybki. Na koniec poniżej jeszcze kod ciała tej samej metody bez wykorzystania Linq:

            StringBuilder sb = new StringBuilder();
            foreach (byte bt in _btFrame)
                sb.AppendFormat("{0} ", Convert.ToString(bt, 2).PadLeft(8, '0'));

            sb.Remove(sb.Length - 1, 1);
            return sb.ToString();

Jak łatwo zauważyć brak wykorzystania Linq nie wydłużył kodu jako takiego. Ponownie konieczne jest usunięcie ostatniej zbędnej spacji, ale już mi się nie chce tego rozwijać. Języki takie jak C# powstały dla przyspieszenia pracy i dla jej ułatwienia. Oznacza to także ułatwienie przy czytaniu kodu, który powstał wcześniej i był napisany przez inną osobę. Wystarczy tylko stosować najprostsze rozwiązania i nie bać się szukać ich w sieci. Na prawdę warto ułatwiać sobie pracę.

Czytaj też:

Brak komentarzy:

Prześlij komentarz