Jak wiadomo wszystkie dane platformy .NET są przechowywane w postaci obiektów (oraz struktur – szczególnego przypadku obiektów), więc siłą rzeczy sposób przechowywania tychże danych jest ukryty przed programistą (obiekty są wszak hermetyczne).

Jest to całkowicie odmienne podejście do przechowywania danych w stosunku do stosowanego przed erą .NET. Tam typy proste były zwyczajnie obszarem pamięci, który był odpowiednio interpretowany przez oprogramowanie (dbał o to kompilator). Tworząc strukturę, de facto umieszczało się kolejno w pamięci określone porcje bajtów, które były interpretowane stosownie do odpowiadającego im typu. Z punktu widzenia kodu programu, nic nie stało mu na drodze do struktury (w .NET staje na drodze właśnie kod obiektu).

Taka struktura (w pseudokodzie):

struct Product
{
	public int Identifier;
	public char[8] Symbol;
	public char[20] Name;
	public short Quantity;
}

ułożona była w pamięci w ten sposób, że pierwsze cztery bajty zajmowała wartość identyfikatora, następne 10 bajtów zajmowały znaki wchodzące w skład symbolu, kolejne 40 bajtów obejmowało znaki reprezentujące nazwę, a na końcu dwa bajty określały ilość. W przypadku tablicy 5 produktów, wyglądała on w pamięci mniej więcej tak:

indeks tablicy przesunięcie w pamięci pole struktury zawartość pamięci hex zinterpretowana zawartość pamięci
0 00 Identifier 00 01 01 00 65792
0 04 Symbol 53 79 6D 62 6F 6C 41 20 „SymbolA „
0 0C Name 4E 61 7A 77 61 20 70 69 65 72 77 73 7A 61 20 20 20 20 20 20 „Nazwa pierwsza      „
0 20 Quantity 01 00 256
1 22 Identifier 01 01 01 00 65793
1 26 Symbol 53 79 6D 62 6F 6C 42 20 „SymbolB „
1 2E Name 4E 61 7A 77 61 20 64 72 75 67 61 20 20 20 20 20 20 20 20 20 „Nazwa druga         „
1 42 Quantity 01 01 257
2 44 Identifier 02 01 01 00 65794
2 48 Symbol 53 79 6D 62 6F 6C 43 20 „SymbolC „
2 50 Name 4E 61 7A 77 61 20 74 72 7A 65 63 69 61 20 20 20 20 20 20 20 „Nazwa trzecia       „
2 64 Quantity 01 02 258
3 66 Identifier 03 01 01 00 65795
3 6A Symbol 53 79 6D 62 6F 6C 44 20 „SymbolD „
3 72 Name 4E 61 7A 77 61 20 63 7A 77 61 72 74 61 20 20 20 20 20 20 20 „Nazwa czwarta       „
3 86 Quantity 01 03 259
4 88 Identifier 04 01 01 00 65796
4 8C Symbol 53 79 6D 62 6F 6C 45 20 „SymbolE „
4 94 Name 4E 61 7A 77 61 20 70 69 A5 74 61 20 20 20 20 20 20 20 20 20 „Nazwa piąta         „
4 A8 Quantity 01 04 260

Tu mała dygresja. Ze względów wydajnościowych, tj. szybkiego dostępu do pamięci, bywało, że pomiędzy poszczególnymi polami były puste miejsca (niewykorzystana pamięć). Niemniej można było zażądać ścisłego upakowania, tak jak w powyższym przykładzie.

Niestety w .NET tak już nie jest, nie można traktować struktury jako spójnego obszaru pamięci. Rodzi to problemy, kiedy np. odczytujemy dane z pliku generowanego za pomocą oprogramowania nie napisanego w .NET (np. tabela w formacie dBase) lub uruchamiamy kod systemu operacyjnego, który oczekuje struktur w ściśle określonym układzie. O ile ten drugi przypadek został wzięty w .NET pod uwagę i istnieją odpowiednie mechanizmy, aby go obsłużyć (przetaczanie – marshaling), o tyle nie bardzo można zastosować je do interpretacji wspomnianych wcześniej plików.

Na czym polega problem w przypadku plików? Otóż kiedyś wystarczało następujące wywołanie (pseudokod):

Product[5] Products;
// ...
FileStream fs;
// ...
fs.Read(Products, sizeof(Products)); // sizeof(Products) zwracał rozmiar tablicy jako sumę rozmiaru jej elementów

aby wybrana zawartość pliku została umieszczona w tablicy Products. Równie łatwe było jej zapisywanie do pliku:

fs.Write(Products, sizeof(Products));

W .NET takie podejście jest niemożliwe. Jedyne co można odczytać za pomocą strumienia z pliku to tablica bajtów. A potrzebny jest odczyt struktury (lub ciągu struktur). Oczywiście jest na to sposób, w przeciwnym wypadku .NET miałoby mniejszą funkcjonalność niż jego poprzednicy. Z pomocą przychodzi klasa BitConverter. Posiada ona metody, które pozwalają przeprowadzać szatkowanie tablicy bajtów na odpowiednie typy danych. Wystarczy zatem pobrać zawartość pliku do tablicy, a następnie dokonać konwersji na właściwe typy (takimi metodami jak: ToBoolean, ToByte, ToChar, ToInt??, ToUInt??, ToString, ToDouble, ToSingle), wskazując w tablicy indeksy odpowiadające położeniu poszczególnych pól struktury (będą one zgodne wartościowo z podanymi w powyższej tabeli przesunięciami w pamięci). Innym wyjściem jest czytanie pliku w blokach o rozmiarze równym rozmiarowi (liczonym wg starych zasad) zajmowanym przez strukturę w pamięci. Wówczas podczas wyłuskiwania pól z takiej tablicy wystarczy podawać jako indeksy ich przesunięcia wewnątrz struktury (czyli trzymając się powyższego przykładu, będą to indeksy odpowiadające przesunięciom w pamięci dla pierwszych pięciu wierszy – odpowiadających indeksowi 0 przykładowej tablicy).

Niestety okazuje się, że sama klasa BitConverter jest niewystarczająca, nie potrafi bowiem przetransformować tekstów na typ string. Jak wiadomo przechowuje on teksty w postaci Unicode (gdzie jeden znak reprezentują dwa bajty), a w rozpatrywanych plikach teksty najczęściej przechowywane są w postaci jednobajtowej, zaś znaki narodowe przyjmują wybrane wartości z zakresu od 128 do 255, w zależności od wybranej do ich interpretacji strony kodowej (np. dla strony kodowej polskiego Windows kod ASCII odpowiadająca znakowi ‚Ą’ to 165).

W tym przypadku z pomocą przychodzi kolejna klasa tj. Encoding, a dokładnie jej metoda GetString, która jako jeden z argumentów przyjmuje tablicę bajtów mającą podlegać konwersji na tekst. Dodatkowo konieczne jest wybranie takiej instancji klasy Encoding, aby używała ona strony kodowej, wg której mają być interpretowane znaki powyżej kodu ASCII 127. Na szczęście klasa Encoding posiada metodę GetEncoding, która zwraca odpowiednią instancję, obsługującą kodowanie zgodnie z podaną w jej parametrze, tekstową nazwą strony kodowej (np. dla kodowania polskich znaków w Windows jest to „windows-1250”).

A oto przykład, demonstrujący pobieranie pliku zawierającego obraz pamięci i wykorzystującego wspomniane klasy:

class BinaryIO
{
	class Product // klasa odzwierciedlająca pobieraną strukturę
	{
		public int Identifier { get; set; }
		public string Symbol { get; set; }
		public string Name { get; set; }
		public short Quantity { get; set; }
		public Product(int aIdentifier, string aSymbol, string aName, short aQuantity)
		{
			Identifier = aIdentifier;
			Symbol = aSymbol;
			Name = aName;
			Quantity = aQuantity;
		}
	}

	private List<Product> Load(string aFileName)
	{
		List<Product> Products = new List<Product>();
		Encoding CurrentEncoding = Encoding.GetEncoding("windows-1250"); // instancja kodowania dla Windows PL
		byte[] Sizes = { sizeof(int), 8, 20, sizeof(short) }; // długości poszczególnych pól struktury starego typu
		int 
			BlockSize = Sizes[0] + Sizes[1] + Sizes[2] + Sizes[3]; // rozmiar struktury starego typu - rozmiar bloku pamięci
		byte[] bytes = new byte[BlockSize]; // tablica na blok pamięci odpowiadający strukturze starego typu
		using (FileStream Reader = new FileStream(aFileName, FileMode.Open, FileAccess.Read))
		{
			while (Reader.Read(bytes, 0, BlockSize) > 0)
				Products.Add(
					new Product(
						BitConverter.ToInt32(bytes, 0),
						CurrentEncoding.GetString(bytes, Sizes[0], Sizes[1]).Trim(),
						CurrentEncoding.GetString(bytes, Sizes[0] + Sizes[1], Sizes[2]).Trim(),
						BitConverter.ToInt16(bytes, Sizes[0] + Sizes[1] + Sizes[2])
					)
				);
		}
		return Products;
	}

	public void Presentation()
	{
		List<Product> Products = Load(@"d:\memory.bin");
		foreach (Product item in Products)
		{
			Console.WriteLine(item.Identifier.ToString() + '|' + item.Symbol + '|' + item.Name + '|' + item.Quantity.ToString());
		}
	}
}

Jak widać, tym razem kodu jest o wiele więcej – taki niestety urok tego, że jest on zarządzany ;).

Dla chcących pobawić się powyższym przykładem udostępniam plik, który zawiera dokładnie takie dane, jak ujęte w powyższej tabeli. Oczywiście należy zmienić ścieżkę do niego w wywołaniu metody Load ;).

W następnym wpisie pokażę, jak zapisywać dane, aby wyglądały tak, jakby utworzyło je oprogramowanie niezarządzane (czyli spreparuję plik z obrazem pamięci tak, aby takie oprogramowanie potrafiło go odczytać).

Tym którzy zastanawiają się po co w ogóle robić takie, rzeczy śpieszę wyjaśnić, że wbrew pozorom wciąż używane jest mnóstwo oprogramowania, które zapisuje i odczytuje dane w takiej postaci. Podobne pliki eksportują także różnego rodzaju urządzenia pomiarowe. Zatem prędzej czy później (u mnie zdecydowanie prędzej) przyjdzie się zetknąć z tego typu formatem danych.