class ReadOnlyFieldExample
{ private readonly static int staticData; // Możemy tu ustawić to pole static ReadOnlyFieldExample()
{
// Pole staticData możemy również ustawić tutaj
}
private readonly int instanceData; // Możemy tu ustawić to pole
public ReadOnlyFieldExample()
{
// Pole instanceData możemy również ustawić
// w jednym z konstruktorów ReadOnlyFieldExample
}
}
Nie należy jednak mylić funkcji tylko do odczytu z niezmiennością. Jeśli na przykład pole przeznaczone tylko do odczytu odwołuje się do zmiennej struktury danych, zawartość tej struktury można zmieniać. „Tylko do odczytu” oznacza po prostu, że nie można zaktuali-zować referencji tak, aby wskazywała nową instancję struktury danych. Oto przykład: class ReadOnlyFieldBadExample
{ public static readonly int[] ConstantNumbers = new int[] {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
};
}
// Jakiś szkodliwy kod może nadal zmieniać liczby w tablicy;
// nie może tylko ustawić zmiennej ConstantNumbers na nową tablicę!
ReadOnlyFieldBadExample.ConstantNumbers[0] = 9;
ReadOnlyFieldBadExample.ConstantNumbers[1] = 8;
// …
Ci, którzy mają dostęp do tablicy ConstantNumbers (wszyscy, którzy mają dostęp do ReadOn-lyFieldBadExamSle, ponieważ pole jest oznaczone jako Sublic), mogą zmieniać jej elementy.
Bywa to dość zaskakujące, kiedy pozornie niezmienne wartości w kodzie zostają zmodyfikowane przez użytkowników, co prowadzi do dziwnych błędów (które nie zostały wychwy-cone podczas testów).
Pola stałe
Można również tworzyć pola stałe lub literalne. Są one oznaczone jako static literal w IL i definiowane z wykorzystaniem modyfikatora const w C#. Stałe muszą być inicjalizowane w kodzie programu, na przykład przez użycie wplecionego inicjalizatora pola w C#, i mogą zawierać tylko wartości podstawowe takie jak liczby całkowite, liczby zmiennopozycyjne, literały łańcuchowe itd. Można oczywiście używać prostych wyrażeń matematycznych, pod warunkiem że kompilator będzie mógł zredukować je do stałej wartości podczas kompilacji programu.
Rozdział 2. n Wspólny system typów
51
class UsefulConstants
{ public const double PI = 3.1415926535897931;
// Poniższe wyrażenie jest na tyle proste, że kompilator może zredukować je do 4: public const int TwoPlusTwo = 2 + 2;
}
Zauważmy, że C# emituje stałe jako wartości statyczne, czyli związane z typem. Ma to sens, ponieważ wartość nie może zależeć od instancji. Bądź co bądź, jest to stała.
Użycie pola stałego pozwala na efektywne reprezentowanie wartości stałych, ale może prowadzić do subtelnych problemów związanych z kontrolą wersji. Typowy kompilator w razie wykrycia pola const osadza w kodzie literalną wartość stałej. Właśnie tak postępuje kompilator C#. Jeśli stała pochodzi z innego podzespołu, zmiana jej wartości w tamtym podzespole może spowodować problemy, mianowicie skompilowany uprzednio kod IL będzie nadal używał starej wartości. Trzeba będzie go ponownie skompilować, aby korzystał z nowej wartości stałej. Oczywiście, problem ten nie występuje w przypadku wewnętrznych stałych danego podzespołu.
Kontrolowanie układu pól struktur
W dotychczasowej dyskusji przyjmowaliśmy nieco naiwny model układu struktur; dotyczy to nawet punktu poświęconego właśnie temu zagadnieniu! Choć model ten jest prawidłowy w 99 procentach przypadków, istnieje mechanizm, który pozwala przedefiniować domyślny układ struktury. W szczególności można zmieniać kolejność i przesunięcia pól w typach wartościowych. Mechanizm ten zwykle wykorzystuje się do współpracy z kodem niezarzą-
dzanym (rozdział 11.), ale bywa również przydatny w zastosowaniach zaawansowanych, na przykład w przypadku unii albo pakowania bitów.
W C# układ jest kontrolowany przez atrybut System.Runtime.InteroSServices.StructLayout-Attribute. Istnieją trzy podstawowe tryby wskazywane przez wyliczeniową wartość LayoutKind przekazywaną do konstruktora atrybutu. Są one kompilowane do odpowiednich słów kluczowych IL (wymienionych poniżej):
n LayoutKind.Automatic — ta opcja zezwala CLR na dowolne rozmieszczenie pól typu wartościowego, co jest wskazywane przez słowo kluczowe autolayout języka IL i umożliwia optymalizację wyrównania pól. Gdybyśmy na przykład mieli trzy pola, 1-bajtowe, 2-bajtowe i znów 1-bajtowe, struktura danych prawdopodobnie zostałaby ułożona tak, aby pole 2-bajtowe znajdowało się na granicy słowa.
Jest to opcja domyślna. Uniemożliwia ona jednak szeregowanie wartości przez granicę z kodem niezarządzanym, ponieważ układ jest nieprzewidywalny.
n LayoutKind.SeLuential — w tym trybie wszystkie pola są rozmieszczone dokładnie tak, jak określono to w kodzie IL. Jest to wskazywane przez słowo kluczowe layoutseLuential języka IL. Choć nie pozwala to środowisku
uruchomieniowemu na optymalizowanie układu, to gwarantuje przewidywalną kolejność pól podczas współpracy z kodem niezarządzanym.
n LayoutKind.ExSlicit — ta ostatnia opcja obarcza autora struktury pełną odpowiedzialnością za jej układ i jest wskazywana przez słowo kluczowe
exSlicitlayout języka IL. Do każdego pola struktury trzeba dołączyć atrybut
52
Część I n Podstawowe informacje o CLR
FieldOffsetAttribute, który określa, gdzie ma występować pole w ogólnym układzie struktury. Opcja ta wymaga dużej ostrożności, ponieważ przypadkowo można utworzyć nakładające się pola, ale umożliwia ona realizację pewnych zaawansowanych scenariuszy. Przykład przedstawię poniżej.
Podczas definiowania układu można dostarczyć trzech dodatkowych informacji. Pierwszą jest bezwzględny rozmiar (Size, w bajtach) całej struktury. Musi on być równy sumie rozmiarów pól w określonym układzie lub większy od niej. Można to wykorzystać na przykład do poszerzenia rozmiaru struktury danych w celu jej wyrównania albo wtedy, kiedy kod niezarządzany zapisuje jakieś informacje w bajtach poza zarządzanymi danymi struktury.