23 Niebezpieczny kod
23.1 Ogólne
Implementacja, która nie obsługuje niebezpiecznego kodu, jest wymagana do zdiagnozowania dowolnego użycia reguł składniowych zdefiniowanych w tej klauzuli.
Pozostała część tej klauzuli, w tym wszystkie jej podklasy, jest warunkowo normatywna.
Uwaga: podstawowy język C# zdefiniowany w poprzednich klauzulach różni się w szczególności od języka C i C++ w pomijaniu wskaźników jako typu danych. Zamiast tego język C# udostępnia odwołania i możliwość tworzenia obiektów zarządzanych przez moduł odśmieceń pamięci. Ten projekt, w połączeniu z innymi funkcjami, sprawia, że język C# jest znacznie bezpieczniejszy niż C lub C++. W podstawowym języku C# po prostu nie można mieć niezainicjowanej zmiennej, wskaźnika zwisającego ani wyrażenia, które indeksuje tablicę poza jej granicami. W ten sposób wyeliminowane są całe kategorie błędów, które rutynowo nękają programy C i C++.
Mimo że praktycznie każda konstrukcja typu wskaźnika w języku C lub C++ ma odpowiednik typu referencyjnego w języku C#, istnieją sytuacje, w których dostęp do typów wskaźników staje się koniecznością. Na przykład interfacing z bazowym systemem operacyjnym, uzyskiwanie dostępu do urządzenia mapowanego w pamięci lub implementowanie algorytmu krytycznego czasowo może nie być możliwe lub praktyczne bez dostępu do wskaźników. Aby rozwiązać ten problem, język C# zapewnia możliwość pisania niebezpiecznego kodu.
W niebezpiecznym kodzie można deklarować wskaźniki i wykonywać operacje na wskaźnikach w celu przeprowadzania konwersji między wskaźnikami i typami całkowitymi, aby pobrać adres zmiennych itd. W sensie pisanie niebezpiecznego kodu jest podobne do pisania kodu C w programie C#.
Niebezpieczny kod jest w rzeczywistości "bezpieczną" funkcją z perspektywy zarówno deweloperów, jak i użytkowników. Niebezpieczny kod musi być wyraźnie oznaczony modyfikatorem
unsafe
, dzięki czemu deweloperzy nie mogą przypadkowo używać niebezpiecznych funkcji, a aparat wykonywania działa w celu zapewnienia, że nie można wykonać niebezpiecznego kodu w niezaufanym środowisku.notatka końcowa
23.2 Niebezpieczne konteksty
Niebezpieczne funkcje języka C# są dostępne tylko w niebezpiecznych kontekstach. Niebezpieczny kontekst jest wprowadzany przez uwzględnienie unsafe
modyfikatora w deklaracji typu, elementu członkowskiego lub funkcji lokalnej albo zastosowanie unsafe_statement:
- Deklaracja klasy, struktury, interfejsu lub delegata może zawierać
unsafe
modyfikator, w takim przypadku cały tekstowy zakres deklaracji tego typu (w tym treść klasy, struktury lub interfejsu) jest uważany za niebezpieczny kontekst.Uwaga: jeśli type_declaration jest częściowa, tylko ta część jest niebezpiecznym kontekstem. notatka końcowa
- Deklaracja pola, metody, właściwości, zdarzenia, indeksatora, operatora, konstruktora wystąpienia, finalizatora, konstruktora statycznego lub funkcji lokalnej może zawierać
unsafe
modyfikator, w takim przypadku cały tekstowy zakres tej deklaracji elementu członkowskiego jest uważany za niebezpieczny kontekst. - Unsafe_statement umożliwia korzystanie z niebezpiecznego kontekstu w bloku. Cały zakres tekstowy skojarzonego bloku jest uważany za niebezpieczny kontekst. Funkcja lokalna zadeklarowana w niebezpiecznym kontekście jest niebezpieczna.
Skojarzone rozszerzenia gramatyczne są wyświetlane poniżej i w kolejnych podklasach.
unsafe_modifier
: 'unsafe'
;
unsafe_statement
: 'unsafe' block
;
Przykład: w poniższym kodzie
public unsafe struct Node { public int Value; public Node* Left; public Node* Right; }
unsafe
modyfikator określony w deklaracji struktury powoduje, że cały tekstowy zakres deklaracji struktury staje się niebezpiecznym kontekstem. W związku z tym można zadeklarowaćLeft
pola iRight
jako typu wskaźnika. Powyższy przykład można również napisaćpublic struct Node { public int Value; public unsafe Node* Left; public unsafe Node* Right; }
W tym miejscu modyfikatory w deklaracjach pól powodują,
unsafe
że deklaracje te będą traktowane jako niebezpieczne konteksty.przykład końcowy
Poza ustanawianiem niebezpiecznego kontekstu, co pozwala na używanie typów wskaźników, unsafe
modyfikator nie ma wpływu na typ lub element członkowski.
Przykład: w poniższym kodzie
public class A { public unsafe virtual void F() { char* p; ... } } public class B : A { public override void F() { base.F(); ... } }
niebezpieczny modyfikator metody
F
wA
programie po prostu powoduje, że tekstowy zakresF
staje się niebezpiecznym kontekstem, w którym mogą być używane niebezpieczne funkcje języka. W przesłonięcieF
wB
pliku nie ma potrzeby ponownegounsafe
określania modyfikatora — chyba żeF
sama metodaB
wymaga dostępu do niebezpiecznych funkcji.Sytuacja jest nieco inna, gdy typ wskaźnika jest częścią podpisu metody
public unsafe class A { public virtual void F(char* p) {...} } public class B: A { public unsafe override void F(char* p) {...} }
W tym miejscu podpis
F
zawiera typ wskaźnika, dlatego może być zapisywany tylko w niebezpiecznym kontekście. Jednak niebezpieczny kontekst można wprowadzić, tworząc całą klasę niebezpieczną, tak jak w przypadku ,A
lub przez dołączenieunsafe
modyfikatora w deklaracji metody, tak jak w przypadku .B
przykład końcowy
unsafe
Gdy modyfikator jest używany w deklaracji typu częściowego (§15.2.7), tylko ta konkretna część jest uważana za niebezpieczny kontekst.
23.3 Typy wskaźników
W niebezpiecznym kontekście typ (§8.1) może być pointer_type, a także value_type, reference_type lub type_parameter. W niebezpiecznym kontekście pointer_type może być również typem elementu tablicy (§17). Pointer_type można również używać w wyrażeniu typeof (§12.8.18) poza niebezpiecznym kontekstem (na przykład użycie nie jest niebezpieczne).
Pointer_type jest zapisywana *
jako unmanaged_type (§8.8) lub słowo kluczowe void
, a następnie token:
pointer_type
: value_type ('*')+
| 'void' ('*')+
;
Typ określony przed *
typem wskaźnika jest nazywany typem odwołania typu wskaźnika. Reprezentuje typ zmiennej, do której znajduje się wartość punktów typu wskaźnika.
Pointer_type można używać tylko w array_type w niebezpiecznym kontekście (§23.2). Non_array_type to dowolny typ, który nie jest sam array_type.
W przeciwieństwie do odwołań (wartości typów odwołań) wskaźniki nie są śledzone przez moduł odśmiecanie pamięci — moduł odśmiecanie pamięci nie ma wiedzy na temat wskaźników i danych, do których wskazują. Z tego powodu wskaźnik nie może wskazywać odwołania lub struktury zawierającej odwołania, a referencyjny typ wskaźnika jest unmanaged_type. Same typy wskaźników są typami niezarządzanych, więc typ wskaźnika może być używany jako typ odwołania dla innego typu wskaźnika.
Intuicyjna reguła mieszania wskaźników i odwołań polega na tym, że odwołania do odwołań (obiektów) mogą zawierać wskaźniki, ale odwołania do wskaźników nie mogą zawierać odwołań.
Przykład: niektóre przykłady typów wskaźników podano w poniższej tabeli:
Przykład Opis byte*
Wskaźnik do byte
char*
Wskaźnik do char
int**
Wskaźnik do wskaźnika do int
int*[]
Jednowymiarowa tablica wskaźników do int
void*
Wskaźnik do nieznanego typu przykład końcowy
W przypadku danej implementacji wszystkie typy wskaźników mają taki sam rozmiar i reprezentację.
Uwaga: W przeciwieństwie do języka C i C++, gdy wiele wskaźników jest zadeklarowanych w tej samej deklaracji, w języku C#
*
element jest zapisywany wraz tylko z typem bazowym, a nie jako znaki interpunkcyjne prefiksu dla każdej nazwy wskaźnika. Na przykład:int* pi, pj; // NOT as int *pi, *pj;
notatka końcowa
Wartość wskaźnika o typie T*
reprezentuje adres zmiennej typu T
. Operator *
pośredni wskaźnika (§23.6.2) może służyć do uzyskiwania dostępu do tej zmiennej.
Przykład: Biorąc pod uwagę zmienną
P
typuint*
, wyrażenie*P
oznaczaint
zmienną znajdującą się pod adresem zawartym wP
elem. przykład końcowy
Podobnie jak odwołanie do obiektu, wskaźnik może mieć wartość null
. Zastosowanie operatora pośredniego null
do wskaźnika -valued powoduje zachowanie zdefiniowane przez implementację (§23.6.2). Wskaźnik z wartością null
jest reprezentowany przez all-bits-zero.
Typ void*
reprezentuje wskaźnik do nieznanego typu. Ponieważ typ odwołania jest nieznany, operator pośredni nie może być stosowany do wskaźnika typu void*
, ani nie można wykonać żadnej arytmetyki na takim wskaźniku. Wskaźnik typu void*
można jednak rzutować do dowolnego innego typu wskaźnika (i odwrotnie) i porównać z wartościami innych typów wskaźników (§23.6.8).
Typy wskaźników są oddzielną kategorią typów. W przeciwieństwie do typów odwołań i typów wartości, typy wskaźników nie dziedziczą i object
nie istnieją konwersje między typami wskaźników i object
. W szczególności boks i rozpałkanie (§8.3.13) nie są obsługiwane w przypadku wskaźników. Jednak konwersje są dozwolone między różnymi typami wskaźników i między typami wskaźników a typami całkowitymi. Jest to opisane w §23.5.
Nie można użyć pointer_type jako argumentu typu (§8.4) i wnioskowania typu (§12.6.3) kończy się niepowodzeniem w wywołaniach metod ogólnych, które wywnioskowałyby argument typu jako typ wskaźnika.
Nie można użyć pointer_type jako typu podrażenia operacji dynamicznie powiązanej (§12.3.3).
Nie można użyć pointer_type jako typu pierwszego parametru w metodzie rozszerzenia (§15.6.10).
Pointer_type może być używany jako typ pola lotnego (§15.5.4).
Dynamiczne wymazywanie typu E*
jest typem wskaźnika z typem odwołania dynamicznego wymazywania elementu E
.
Nie można użyć wyrażenia z typem wskaźnika, aby podać wartość w member_declarator w anonymous_object_creation_expression (§12.8.17.7).
Wartość domyślna (§9.3) dla dowolnego typu wskaźnika to null
.
Uwaga: Mimo że wskaźniki mogą być przekazywane jako parametry odwołania, może to spowodować niezdefiniowane zachowanie, ponieważ wskaźnik może być również ustawiony tak, aby wskazywał zmienną lokalną, która już nie istnieje, gdy wywołana metoda zwraca lub stały obiekt, do którego był używany do wskazywania, nie jest już stały. Na przykład:
class Test { static int value = 20; unsafe static void F(out int* pi1, ref int* pi2) { int i = 10; pi1 = &i; // return address of local variable fixed (int* pj = &value) { // ... pi2 = pj; // return address that will soon not be fixed } } static void Main() { int i = 15; unsafe { int* px1; int* px2 = &i; F(out px1, ref px2); int v1 = *px1; // undefined int v2 = *px2; // undefined } } }
notatka końcowa
Metoda może zwracać wartość typu, a ten typ może być wskaźnikiem.
Przykład: Gdy wskaźnik do ciągłej sekwencji
int
s, liczba elementów tej sekwencji i innaint
wartość, następująca metoda zwraca adres tej wartości w tej sekwencji, jeśli wystąpi dopasowanie; w przeciwnym razie zwracanull
wartość :unsafe static int* Find(int* pi, int size, int value) { for (int i = 0; i < size; ++i) { if (*pi == value) { return pi; } ++pi; } return null; }
przykład końcowy
W niebezpiecznym kontekście dostępnych jest kilka konstrukcji do obsługi wskaźników:
- Operator jednoargumentowy
*
może służyć do wykonywania pośredniego wskaźnika (§23.6.2). - Operator
->
może służyć do uzyskiwania dostępu do elementu członkowskiego struktury za pośrednictwem wskaźnika (§23.6.3). - Operator
[]
może służyć do indeksowania wskaźnika (§23.6.4). - Operator jednoargumentowy
&
może służyć do uzyskania adresu zmiennej (§23.6.5). - Operatory
++
i--
mogą służyć do przyrostowania i dekrementacji wskaźników (§23.6.6). - Operatory binarne
+
i-
mogą służyć do wykonywania arytmetyki wskaźnika (§23.6.7). - Operatory , , ,
<=
<
>
, i>=
mogą służyć do porównywania wskaźników (§23.6.8).!=
==
- Operator
stackalloc
może służyć do przydzielania pamięci ze stosu wywołań (§23.9). - Instrukcja
fixed
może służyć do tymczasowego naprawienia zmiennej, aby można było uzyskać jej adres (§23.7).
23.4 Stałe i przenoszone zmienne
Operator address-of (§23.6.5) i fixed
instrukcja (§23.7) dzielą zmienne na dwie kategorie: zmienne stałe i zmienne przenoszone.
Stałe zmienne znajdują się w lokalizacjach przechowywania, których nie dotyczy operacja modułu odśmiecywania pamięci. (Przykłady zmiennych stałych obejmują zmienne lokalne, parametry wartości i zmienne utworzone przez wskaźniki wyłudania). Z drugiej strony zmienne przenoszone znajdują się w lokalizacjach przechowywania, które podlegają relokacji lub usuwania przez moduł odśmiecywania pamięci. (Przykłady zmiennych, które można przenosić, obejmują pola w obiektach i elementach tablic).
Operator &
(§23.6.5) zezwala na uzyskanie adresu stałej zmiennej bez ograniczeń. Jednak ze względu na to, że zmienna przenosząca podlega relokacji lub usuwania przez moduł odśmiecenia pamięci, adres zmiennej przenoszonej można uzyskać tylko przy użyciu fixed statement
(§23.7), a adres ten pozostaje ważny tylko przez czas trwania tego fixed
oświadczenia.
Mówiąc dokładnie, zmienna stała jest jedną z następujących wartości:
- Zmienna wynikająca z simple_name (§12.8.4), która odwołuje się do zmiennej lokalnej, parametru wartości lub tablicy parametrów, chyba że zmienna jest przechwytywana przez funkcję anonimową (§12.19.6.2).
- Zmienna wynikająca z member_access (§12.8.7) formularza
V.I
, gdzieV
jest zmienną stałą struct_type. - Zmienna wynikająca z pointer_indirection_expression (§23.6.2) formularza
*P
, pointer_member_access (§23.6.3) formularzaP->I
lub pointer_element_access (§23.6.4) formularzaP[E]
.
Wszystkie inne zmienne są klasyfikowane jako zmienne przenoszone.
Pole statyczne jest klasyfikowane jako zmienna z możliwością przenoszenia. Ponadto parametr by-reference jest klasyfikowany jako zmienna przenosząca, nawet jeśli argument podany dla parametru jest zmienną stałą. Na koniec zmienna wygenerowana przez wyłudanie wskaźnika jest zawsze klasyfikowana jako zmienna stała.
Konwersje wskaźników 23.5
23.5.1 Ogólne
W niebezpiecznym kontekście zestaw dostępnych niejawnych konwersji (§10.2) jest rozszerzony w celu uwzględnienia następujących niejawnych konwersji wskaźników:
- Z dowolnego pointer_type do typu
void*
. - Od literału
null
(§6.4.5.7) do dowolnego pointer_type.
Ponadto w niebezpiecznym kontekście zestaw dostępnych jawnych konwersji (§10.3) jest rozszerzony w celu uwzględnienia następujących jawnych konwersji wskaźników:
- Od dowolnego pointer_type do innych pointer_type.
- Z
sbyte
, ,byte
,short
,int
uint
long
ushort
lubulong
do dowolnego pointer_type. - Z dowolnego pointer_type do
sbyte
,byte
, ,short
ushort
,int
,uint
long
lubulong
.
Na koniec w niebezpiecznym kontekście zestaw standardowych niejawnych konwersji (§10.4.2) obejmuje następujące konwersje wskaźników:
- Z dowolnego pointer_type do typu
void*
. - Od literału
null
do dowolnego pointer_type.
Konwersje między dwoma typami wskaźników nigdy nie zmieniają rzeczywistej wartości wskaźnika. Innymi słowy, konwersja z jednego typu wskaźnika na inny nie ma wpływu na adres bazowy podany przez wskaźnik.
Jeśli jeden typ wskaźnika jest konwertowany na inny, jeśli wynikowy wskaźnik nie jest poprawnie wyrównany dla typu wskazywanego, zachowanie jest niezdefiniowane, jeśli wynik jest wyłuszczone. Ogólnie rzecz biorąc, koncepcja "poprawnie wyrównana" jest przechodnia: jeśli wskaźnik do typu A
jest poprawnie wyrównany do wskaźnika do typu B
, który z kolei jest poprawnie wyrównany dla wskaźnika do typu C
, to wskaźnik do typu jest poprawnie wyrównany dla wskaźnika do typu A
C
.
Przykład: rozważmy następujący przypadek, w którym zmienna o jednym typie jest uzyskiwana za pośrednictwem wskaźnika do innego typu:
unsafe static void M() { char c = 'A'; char* pc = &c; void* pv = pc; int* pi = (int*)pv; // pretend a 16-bit char is a 32-bit int int i = *pi; // read 32-bit int; undefined *pi = 123456; // write 32-bit int; undefined }
przykład końcowy
Gdy typ wskaźnika jest konwertowany na wskaźnik na byte
wartość , wynik wskazuje najniższy adres byte
zmiennej. Kolejne przyrosty wyniku, do rozmiaru zmiennej, zwracają wskaźniki do pozostałych bajtów tej zmiennej.
Przykład: Następująca metoda wyświetla każdą z ośmiu bajtów w
double
postaci wartości szesnastkowej:class Test { static void Main() { double d = 123.456e23; unsafe { byte* pb = (byte*)&d; for (int i = 0; i < sizeof(double); ++i) { Console.Write($" {*pb++:X2}"); } Console.WriteLine(); } } }
Oczywiście generowane dane wyjściowe zależą od endianness. Jedną z możliwości jest
" BA FF 51 A2 90 6C 24 45"
.przykład końcowy
Mapowania między wskaźnikami i liczbami całkowitymi są definiowane przez implementację.
Uwaga: jednak w architekturze 32-bitowej i 64-bitowej procesora CPU z liniową przestrzenią adresową konwersje wskaźników do lub z typów całkowitych zwykle zachowują się dokładnie tak, jak konwersje
uint
lubulong
wartości, odpowiednio, do lub z tych typów całkowitych. notatka końcowa
23.5.2 Tablice wskaźników
Tablice wskaźników można tworzyć przy użyciu array_creation_expression (§12.8.17.5) w niebezpiecznym kontekście. Tylko niektóre konwersje stosowane do innych typów tablic są dozwolone w tablicach wskaźników:
- Niejawna konwersja odwołania (§10.2.8) z dowolnego array_type do
System.Array
interfejsów, które implementuje, ma również zastosowanie do tablic wskaźników. Jednak każda próba uzyskania dostępu do elementów tablicy za pośrednictwemSystem.Array
lub implementowane interfejsy może spowodować wyjątek w czasie wykonywania, ponieważ typy wskaźników nie są konwertowane naobject
. - Niejawne i jawne konwersje odwołań (§10.2.8, §10.3.5) z typu
S[]
tablicy jednowymiarowej doSystem.Collections.Generic.IList<T>
i jego ogólnych interfejsów podstawowych nigdy nie mają zastosowania do tablic wskaźników. - Jawna konwersja odwołania (§10.3.5) z
System.Array
i interfejsy, które implementuje do dowolnego array_type dotyczy tablic wskaźników. - Jawne konwersje odwołań (§10.3.5) z
System.Collections.Generic.IList<S>
i jego interfejsów podstawowych do jednowymiarowego typuT[]
tablicy nigdy nie mają zastosowania do tablic wskaźników, ponieważ typy wskaźników nie mogą być używane jako argumenty typu i nie ma konwersji z typów wskaźników do typów innych niż wskaźniki.
Te ograniczenia oznaczają, że rozszerzenie instrukcji dla foreach
tablic opisanych w §9.4.4.17 nie może być stosowane do tablic wskaźników. Zamiast tego instrukcja foreach
formularza
foreach (V v in x)
embedded_statement
gdzie typ x
jest typem tablicy formularza T[,,...,]
, n jest liczbą wymiarów minus 1 i T
lub V
jest typem wskaźnika, jest rozszerzany przy użyciu zagnieżdżonych pętli for-loop w następujący sposób:
{
T[,,...,] a = x;
for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++)
{
for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++)
{
...
for (int in = a.GetLowerBound(n); in <= a.GetUpperBound(n); in++)
{
V v = (V)a[i0,i1,...,in];
*embedded_statement*
}
}
}
}
Zmienne a
, i0
, i1
, ... in
nie są widoczne ani dostępne dla x
lub embedded_statement ani żadnego innego kodu źródłowego programu. Zmienna v
jest tylko do odczytu w instrukcji embedded. Jeśli nie ma jawnej konwersji (§23.5) z T
(typu elementu) na V
, zostanie wygenerowany błąd i nie zostaną podjęte żadne dalsze kroki. Jeśli x
ma wartość null
, System.NullReferenceException
element jest zgłaszany w czasie wykonywania.
Uwaga: Mimo że typy wskaźników nie są dozwolone jako argumenty typu, tablice wskaźników mogą być używane jako argumenty typu. notatka końcowa
23.6 Wskaźniki w wyrażeniach
23.6.1 Ogólne
W niebezpiecznym kontekście wyrażenie może zwracać wynik typu wskaźnika, ale poza niebezpiecznym kontekstem jest to błąd czasu kompilacji dla wyrażenia o typie wskaźnika. Mówiąc dokładnie, poza niebezpiecznym kontekstem występuje błąd czasu kompilacji, jeśli wystąpi jakikolwiek simple_name (§12.8.4), member_access (§12.8.7), invocation_expression (§12.8.10) lub element_access (§12.8.12) jest typem wskaźnika.
W niebezpiecznym kontekście primary_no_array_creation_expression (§12.8) i unary_expression (§12.9) produkcji zezwalają na dodatkowe konstrukcje, które są opisane w poniższych podklasach.
Uwaga: Pierwszeństwo i kojarzenie niebezpiecznych operatorów jest implikowane przez gramatykę. notatka końcowa
23.6.2 Pośredni wskaźnik
Pointer_indirection_expression składa się z gwiazdki (*
), a następnie unary_expression.
pointer_indirection_expression
: '*' unary_expression
;
Operator jednoargumentowy *
określa pośredni wskaźnik i służy do uzyskiwania zmiennej, do której wskazuje wskaźnik. Wynik oceny *P
, gdzie P
jest wyrażeniem typu T*
wskaźnika , jest zmienną typu T
. Jest to błąd czasu kompilacji, aby zastosować operator jednoargumentowy *
do wyrażenia typu void*
lub wyrażenia, które nie jest typem wskaźnika.
Efekt zastosowania operatora jednoargumentowego *
do wskaźnika -valued jest zdefiniowany przez implementację null
. W szczególności nie ma gwarancji, że ta operacja zgłasza błąd System.NullReferenceException
.
Jeśli do wskaźnika przypisano nieprawidłową wartość, zachowanie operatora jednoargumentowego *
jest niezdefiniowane.
Uwaga: Wśród nieprawidłowych wartości wyłuszczenia wskaźnika przez operatora jednoargumentowego
*
jest adres niewłaściwie wyrównany dla typu wskazanego (zobacz przykład w §23.5) i adres zmiennej po zakończeniu okresu istnienia.
Do celów określonej analizy przypisania zmienna wygenerowana przez ocenę wyrażenia formularza *P
jest uważana za początkowo przypisaną (§9.4.2).
23.6.3 Dostęp do elementu członkowskiego wskaźnika
Pointer_member_access składa się z primary_expression, a następnie tokenu "->
", a następnie identyfikatora i opcjonalnego type_argument_list.
pointer_member_access
: primary_expression '->' identifier type_argument_list?
;
W dostępie do elementu członkowskiego wskaźnika formularza P->I
P
musi być wyrażeniem typu wskaźnika i I
określa dostępny element członkowski typu, do którego P
wskazuje.
Dostęp do elementu członkowskiego wskaźnika formularza P->I
jest oceniany dokładnie jako (*P).I
. Aby uzyskać opis operatora pośredniego wskaźnika (*
), zobacz §23.6.2. Aby uzyskać opis operatora dostępu do składowych (.
), zobacz §12.8.7.
Przykład: w poniższym kodzie
struct Point { public int x; public int y; public override string ToString() => $"({x},{y})"; } class Test { static void Main() { Point point; unsafe { Point* p = &point; p->x = 10; p->y = 20; Console.WriteLine(p->ToString()); } } }
->
operator służy do uzyskiwania dostępu do pól i wywoływania metody struktury za pomocą wskaźnika. Ponieważ operacjaP->I
jest dokładnie równoważna(*P).I
metodzie ,Main
metoda może być równie dobrze napisana:class Test { static void Main() { Point point; unsafe { Point* p = &point; (*p).x = 10; (*p).y = 20; Console.WriteLine((*p).ToString()); } } }
przykład końcowy
23.6.4 Dostęp do elementu wskaźnika
Pointer_element_access składa się z primary_no_array_creation_expression, po którym następuje wyrażenie ujęte w znaki "[
" i "]
".
pointer_element_access
: primary_no_array_creation_expression '[' expression ']'
;
W przypadku elementu wskaźnika dostęp do formularza P[E]
musi być wyrażeniem typu wskaźnika innego niż void*
, i E
musi być wyrażeniem, które może być niejawnie konwertowane na int
, , uint
long
lub ulong
. P
Dostęp do elementu wskaźnika formularza P[E]
jest obliczany dokładnie jako *(P + E)
. Aby uzyskać opis operatora pośredniego wskaźnika (*
), zobacz §23.6.2. Aby uzyskać opis operatora dodawania wskaźnika (+
), zobacz §23.6.7.
Przykład: w poniższym kodzie
class Test { static void Main() { unsafe { char* p = stackalloc char[256]; for (int i = 0; i < 256; i++) { p[i] = (char)i; } } } }
dostęp do elementu wskaźnika służy do inicjowania buforu znaków w
for
pętli. Ponieważ operacjaP[E]
jest dokładnie równoważna*(P + E)
, przykład może równie dobrze zostać napisany:class Test { static void Main() { unsafe { char* p = stackalloc char[256]; for (int i = 0; i < 256; i++) { *(p + i) = (char)i; } } } }
przykład końcowy
Operator dostępu elementu wskaźnika nie sprawdza błędów poza granicami, a zachowanie podczas uzyskiwania dostępu do elementu poza granicami jest niezdefiniowane.
Uwaga: jest to samo co C i C++. notatka końcowa
23.6.5 Operator adresu
Addressof_expression składa się z ampersand (&
), a następnie unary_expression.
addressof_expression
: '&' unary_expression
;
Biorąc pod uwagę wyrażenie E
, które jest typem T
i jest klasyfikowane jako zmienna stała (§23.4), konstrukcja &E
oblicza adres zmiennej podanej przez E
. Typ wyniku to T*
i jest klasyfikowany jako wartość. Błąd czasu kompilacji występuje, jeśli E
nie jest klasyfikowany jako zmienna, jeśli E
jest klasyfikowany jako zmienna lokalna tylko do odczytu, lub jeśli E
oznacza zmienną ruchomą. W ostatnim przypadku można użyć stałej instrukcji (§23.7), aby tymczasowo "naprawić" zmienną przed uzyskaniem adresu.
Uwaga: Zgodnie z opisem w §12.8.7 poza konstruktorem wystąpienia lub konstruktorem statycznym dla struktury lub klasy, która definiuje
readonly
pole, to pole jest uznawane za wartość, a nie zmienną. W związku z tym nie można podjąć jego adresu. Podobnie nie można podjąć adresu stałej. notatka końcowa
Operator &
nie wymaga, aby jego argument był zdecydowanie przypisany, ale po &
operacji zmienna, do której jest stosowany operator, jest uznawana za zdecydowanie przypisaną w ścieżce wykonywania, w której występuje operacja. Obowiązkiem programisty jest upewnienie się, że w tej sytuacji rzeczywiście odbywa się poprawne inicjowanie zmiennej.
Przykład: w poniższym kodzie
class Test { static void Main() { int i; unsafe { int* p = &i; *p = 123; } Console.WriteLine(i); } }
i
jest uznawany za zdecydowanie przypisany po operacji użytej&i
do zainicjowaniap
. Przypisanie*p
do w efekcie inicjujei
element , ale włączenie tej inicjalizacji jest obowiązkiem programisty i nie wystąpi błąd czasu kompilacji, jeśli przypisanie zostało usunięte.przykład końcowy
Uwaga: można uniknąć nadmiarowego inicjowania zmiennych lokalnych przez reguły określonego przypisania operatora
&
. Na przykład wiele zewnętrznych interfejsów API wprowadza wskaźnik do struktury wypełnionej przez interfejs API. Wywołania takich interfejsów API zwykle przekazują adres lokalnej zmiennej struktury, a bez reguły wymagane byłoby nadmiarowe inicjowanie zmiennej struktury. notatka końcowa
Uwaga: jeśli zmienna lokalna, parametr wartości lub tablica parametrów jest przechwytywana przez funkcję anonimową (§12.8.24), ta zmienna lokalna, parametr lub tablica parametrów nie jest już uważana za zmienną stałą (§23.7), ale jest zamiast tego uważana za zmienną, która można przenosić. W związku z tym jest to błąd dotyczący dowolnego niebezpiecznego kodu, który pobiera adres zmiennej lokalnej, parametru wartości lub tablicy parametrów przechwyconej przez funkcję anonimową. notatka końcowa
23.6.6 Zwiększanie i dekrementacja wskaźnika
W niebezpiecznym kontekście ++
operatory i --
(§12.8.16 i §12.9.6) mogą być stosowane do zmiennych wskaźników wszystkich typów z wyjątkiem void*
. W związku z tym dla każdego typu T*
wskaźnika następujące operatory są niejawnie zdefiniowane:
T* operator ++(T* x);
T* operator --(T* x);
Operatory generują takie same wyniki jak x+1
i x-1
, odpowiednio (§23.6.7). Innymi słowy, dla zmiennej wskaźnika typu T*
operator ++
dodaje sizeof(T)
do adresu zawartego w zmiennej, a --
operator odejmuje sizeof(T)
od adresu zawartego w zmiennej.
Jeśli operacja inkrementacji lub dekrementacji wskaźnika przepełni domenę typu wskaźnika, wynik jest zdefiniowany przez implementację, ale nie są generowane żadne wyjątki.
23.6.7 Arytmetyka wskaźnika
W niebezpiecznym kontekście operator (§12.10.5) i -
operator (§12.10.6) mogą być stosowane do wartości wszystkich typów wskaźników z wyjątkiem void*
.+
W związku z tym dla każdego typu T*
wskaźnika następujące operatory są niejawnie zdefiniowane:
T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);
T* operator +(int x, T* y);
T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);
T* operator –(T* x, int y);
T* operator –(T* x, uint y);
T* operator –(T* x, long y);
T* operator –(T* x, ulong y);
long operator –(T* x, T* y);
Biorąc pod uwagę wyrażenie P
typu T*
wskaźnika i wyrażenie N
typu int
, uint
, long
lub ulong
, wyrażenia P + N
i N + P
oblicz wartość wskaźnika typu T*
, który wynika z dodania N * sizeof(T)
do adresu podanego przez P
. Podobnie wyrażenie P – N
oblicza wartość wskaźnika typu T*
, który wynika z odejmowania N * sizeof(T)
z adresu podanego przez P
element .
Biorąc pod uwagę dwa wyrażenia P
i Q
, typu T*
wskaźnika , wyrażenie P – Q
oblicza różnicę między adresami podanymi przez P
, Q
a następnie dzieli różnicę przez sizeof(T)
. Typ wyniku to zawsze long
. W efekcie P - Q
parametr jest obliczany jako ((long)(P) - (long)(Q)) / sizeof(T)
.
Przykład:
class Test { static void Main() { unsafe { int* values = stackalloc int[20]; int* p = &values[1]; int* q = &values[15]; Console.WriteLine($"p - q = {p - q}"); Console.WriteLine($"q - p = {q - p}"); } } }
co generuje dane wyjściowe:
p - q = -14 q - p = 14
przykład końcowy
Jeśli operacja arytmetyczna wskaźnika przepełni domenę typu wskaźnika, wynik zostanie obcięty w sposób zdefiniowany przez implementację, ale nie są tworzone żadne wyjątki.
Porównanie wskaźników 23.6.8
W niebezpiecznym kontekście operatory ==
, , <
!=
, >
<=
, i >=
(§12.12) mogą być stosowane do wartości wszystkich typów wskaźników. Operatory porównania wskaźników to:
bool operator ==(void* x, void* y);
bool operator !=(void* x, void* y);
bool operator <(void* x, void* y);
bool operator >(void* x, void* y);
bool operator <=(void* x, void* y);
bool operator >=(void* x, void* y);
Ponieważ konwersja niejawna istnieje z dowolnego typu wskaźnika do void*
typu, operandy dowolnego typu wskaźnika można porównać przy użyciu tych operatorów. Operatory porównania porównują adresy podane przez dwa operandy tak, jakby były niepodpisane liczby całkowite.
23.6.9 Operator sizeof
W przypadku niektórych wstępnie zdefiniowanych typów (§12.8.19sizeof
) operator zwraca stałą int
wartość. W przypadku wszystkich innych typów wynik sizeof
operatora jest definiowany przez implementację i jest klasyfikowany jako wartość, a nie stała.
Kolejność, w której składowe są pakowane w strukturę, nie jest określona.
W celach wyrównania na początku struktury na końcu struktury może znajdować się nienazwana dopełnienie. Zawartość bitów używanych jako dopełnienie jest nieokreślona.
Po zastosowaniu do operandu, który ma typ struktury, wynik jest całkowitą liczbą bajtów w zmiennej tego typu, w tym dopełnienie.
23.7 Stała instrukcja
W niebezpiecznym kontekście embedded_statement (§13.1) produkcja zezwala na dodatkową konstrukcję, stałą instrukcję, która służy do "naprawiania" zmiennej przenoszonej, tak aby jego adres pozostał stały przez czas trwania instrukcji.
fixed_statement
: 'fixed' '(' pointer_type fixed_pointer_declarators ')' embedded_statement
;
fixed_pointer_declarators
: fixed_pointer_declarator (',' fixed_pointer_declarator)*
;
fixed_pointer_declarator
: identifier '=' fixed_pointer_initializer
;
fixed_pointer_initializer
: '&' variable_reference
| expression
;
Każda fixed_pointer_declarator deklaruje zmienną lokalną danego pointer_type i inicjuje zmienną lokalną z adresem obliczonym przez odpowiedni fixed_pointer_initializer. Zmienna lokalna zadeklarowana w stałej instrukcji jest dostępna we wszystkich fixed_pointer_initializerwystępujących po prawej stronie deklaracji tej zmiennej, a w embedded_statement stałej instrukcji. Zmienna lokalna zadeklarowana przez stałą instrukcję jest traktowana jako tylko do odczytu. Błąd czasu kompilacji występuje, jeśli osadzona instrukcja próbuje zmodyfikować tę zmienną lokalną (za pomocą przypisania lub ++
operatorów i --
) lub przekazać ją jako odwołanie lub parametr wyjściowy.
Jest to błąd podczas używania przechwyconej zmiennej lokalnej (§12.19.6.2), parametru wartości lub tablicy parametrów w fixed_pointer_initializer. Fixed_pointer_initializer może być jedną z następujących czynności:
- Token "
&
", po którym następuje variable_reference (§9.5) do zmiennej przenoszonej (§23.4) typu niezarządzanegoT
, pod warunkiem, że typT*
jest niejawnie konwertowany do typu wskaźnika podanego w instrukcjifixed
. W takim przypadku inicjator oblicza adres danej zmiennej, a zmienna ma gwarancję pozostania na stałym adresie przez czas trwania stałej instrukcji. - Wyrażenie array_type z elementami typu
T
niezarządzanego , pod warunkiem, że typ jest niejawnie konwertowany na typT*
wskaźnika podany w stałej instrukcji. W tym przypadku inicjator oblicza adres pierwszego elementu w tablicy, a cała tablica ma gwarancję pozostania na stałym adresie przez czas trwaniafixed
instrukcji. Jeśli wyrażenie tablicy jestnull
lub tablica ma zero elementów, inicjator oblicza adres równy zero. - Wyrażenie typu
string
, pod warunkiem, że typchar*
jest niejawnie konwertowany na typ wskaźnika podany w instrukcjifixed
. W takim przypadku inicjator oblicza adres pierwszego znaku w ciągu, a cały ciąg musi pozostać na stałym adresie przez czas trwaniafixed
instrukcji . Zachowanie instrukcjifixed
jest definiowane przez implementację, jeśli wyrażenie ciągu tonull
. - Wyrażenie typu innego niż array_type lub , pod warunkiem, że istnieje dostępna metoda lub dostępna metoda rozszerzenia zgodna z podpisem
ref [readonly] T GetPinnableReference()
, gdzieT
jest unmanaged_type iT*
jest niejawnie konwertowany na typ wskaźnika podany wfixed
instrukcji.string
W tym przypadku inicjator oblicza adres zwracanej zmiennej, a zmienna ta ma gwarancję pozostania na stałym adresie przez czas trwaniafixed
instrukcji.GetPinnableReference()
Metoda może być używana przez instrukcjęfixed
, gdy rozdzielczość przeciążenia (§12.6.4) tworzy dokładnie jeden element członkowski funkcji i że składowy funkcji spełnia powyższe warunki. MetodaGetPinnableReference
powinna zwrócić odwołanie do adresu równego zero, na przykład zwrócone zSystem.Runtime.CompilerServices.Unsafe.NullRef<T>()
momentu, gdy nie ma danych do przypinania. - Simple_name lub member_access odwołujący się do elementu buforowego o stałym rozmiarze zmiennej przenoszonej, pod warunkiem, że typ składowej buforu o stałym rozmiarze jest niejawnie konwertowany na typ wskaźnika podany w instrukcji
fixed
. W tym przypadku inicjator oblicza wskaźnik do pierwszego elementu buforu o stałym rozmiarze (§23.8.3), a bufor o stałym rozmiarze ma gwarancję pozostania na stałym adresie przez czas trwaniafixed
instrukcji.
Dla każdego adresu obliczonego przez fixed_pointer_initializer fixed
instrukcja gwarantuje, że zmienna, do której odwołuje się adres, nie podlega relokacji ani usuwania przez moduł odśmiecający śmieci przez czas trwania fixed
instrukcji.
Przykład: Jeśli adres obliczony przez fixed_pointer_initializer odwołuje się do pola obiektu lub elementu wystąpienia tablicy, stała instrukcja gwarantuje, że zawierające wystąpienie obiektu nie zostanie przeniesione ani usunięte w okresie istnienia instrukcji. przykład końcowy
Jest to odpowiedzialność programisty za zapewnienie, że wskaźniki utworzone przez stałe instrukcje nie przetrwają poza wykonywaniem tych instrukcji.
Przykład: gdy wskaźniki utworzone przez
fixed
instrukcje są przekazywane do zewnętrznych interfejsów API, obowiązkiem programisty jest zapewnienie, że interfejsy API nie zachowują pamięci tych wskaźników. przykład końcowy
Stałe obiekty mogą powodować fragmentację sterty (ponieważ nie można ich przenosić). Z tego powodu obiekty powinny być stałe tylko wtedy, gdy jest to absolutnie konieczne, a następnie tylko przez najkrótszy możliwy czas.
Przykład: przykład
class Test { static int x; int y; unsafe static void F(int* p) { *p = 1; } static void Main() { Test t = new Test(); int[] a = new int[10]; unsafe { fixed (int* p = &x) F(p); fixed (int* p = &t.y) F(p); fixed (int* p = &a[0]) F(p); fixed (int* p = a) F(p); } } }
demonstruje kilka zastosowań instrukcji
fixed
. Pierwsza instrukcja naprawia i uzyskuje adres pola statycznego, drugą instrukcję naprawia i uzyskuje adres pola wystąpienia, a trzecia instrukcja naprawia i uzyskuje adres elementu tablicy. W każdym przypadku błędem byłoby użycie operatora regularnego&
, ponieważ wszystkie zmienne są klasyfikowane jako zmienne przenoszone.Trzecie i czwarte
fixed
instrukcje w powyższym przykładzie dają identyczne wyniki. Ogólnie rzecz biorąc, w przypadku wystąpieniaa
a[0]
tablicy określenie wfixed
instrukcji jest takie samo, jak po prostu określenie elementua
.przykład końcowy
W niebezpiecznym kontekście elementy tablic jednowymiarowych są przechowywane w rosnącej kolejności indeksów, począwszy od indeksu 0
i kończąc na indeksie Length – 1
. W przypadku tablic wielowymiarowych elementy tablic są przechowywane tak, aby indeksy wymiaru z prawej strony zostały najpierw zwiększone, a następnie następny wymiar po lewej stronie i tak dalej po lewej stronie.
W instrukcji fixed
, która uzyskuje wskaźnik p
do wystąpienia a
tablicy , wartości wskaźnika od p
do p + a.Length - 1
reprezentowania adresów elementów w tablicy. Podobnie zmienne, począwszy od p[0]
, aby reprezentować p[a.Length - 1]
rzeczywiste elementy tablicy. Biorąc pod uwagę sposób przechowywania tablic, tablica dowolnego wymiaru może być traktowana tak, jakby była liniowa.
Przykład:
class Test { static void Main() { int[,,] a = new int[2,3,4]; unsafe { fixed (int* p = a) { for (int i = 0; i < a.Length; ++i) // treat as linear { p[i] = i; } } } for (int i = 0; i < 2; ++i) { for (int j = 0; j < 3; ++j) { for (int k = 0; k < 4; ++k) { Console.Write($"[{i},{j},{k}] = {a[i,j,k],2} "); } Console.WriteLine(); } } } }
co generuje dane wyjściowe:
[0,0,0] = 0 [0,0,1] = 1 [0,0,2] = 2 [0,0,3] = 3 [0,1,0] = 4 [0,1,1] = 5 [0,1,2] = 6 [0,1,3] = 7 [0,2,0] = 8 [0,2,1] = 9 [0,2,2] = 10 [0,2,3] = 11 [1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15 [1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19 [1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23
przykład końcowy
Przykład: w poniższym kodzie
class Test { unsafe static void Fill(int* p, int count, int value) { for (; count != 0; count--) { *p++ = value; } } static void Main() { int[] a = new int[100]; unsafe { fixed (int* p = a) Fill(p, 100, -1); } } }
instrukcja
fixed
służy do naprawiania tablicy, aby jej adres mógł zostać przekazany do metody, która przyjmuje wskaźnik.przykład końcowy
char*
Wartość wygenerowana przez naprawienie wystąpienia ciągu zawsze wskazuje ciąg zakończony o wartości null. W stałej instrukcji, która uzyskuje wskaźnik p
do wystąpienia s
ciągu , wartości wskaźnika od p
do p + s.Length ‑ 1
reprezentowania adresów znaków w ciągu, a wartość p + s.Length
wskaźnika zawsze wskazuje na znak null (znak z wartością "\0").
Przykład:
class Test { static string name = "xx"; unsafe static void F(char* p) { for (int i = 0; p[i] != '\0'; ++i) { System.Console.WriteLine(p[i]); } } static void Main() { unsafe { fixed (char* p = name) F(p); fixed (char* p = "xx") F(p); } } }
przykład końcowy
Przykład: Poniższy kod przedstawia fixed_pointer_initializer z wyrażeniem typu innym niż array_type lub
string
:public class C { private int _value; public C(int value) => _value = value; public ref int GetPinnableReference() => ref _value; } public class Test { unsafe private static void Main() { C c = new C(10); fixed (int* p = c) { // ... } } }
Typ
C
ma dostępnąGetPinnableReference
metodę z poprawnym podpisem. W instrukcjifixed
zwracany z tej metody,ref int
gdy jest wywoływanyc
, jest używany do inicjowaniaint*
wskaźnikap
. przykład końcowy
Modyfikowanie obiektów typu zarządzanego za pomocą stałych wskaźników może spowodować niezdefiniowane zachowanie.
Uwaga: na przykład ze względu na to, że ciągi są niezmienne, obowiązkiem programisty jest zapewnienie, że znaki, do których odwołuje się wskaźnik do stałego ciągu, nie są modyfikowane. notatka końcowa
Uwaga: automatyczne kończenie ciągów o wartości null jest szczególnie wygodne podczas wywoływania zewnętrznych interfejsów API, które oczekują ciągów "styl C". Należy jednak pamiętać, że wystąpienie ciągu może zawierać znaki null. Jeśli takie znaki null są obecne, ciąg zostanie obcięty, gdy będzie traktowany jako zakończone
char*
wartości null. notatka końcowa
23.8 o stałym rozmiarze
23.8.1 Ogólne
o stałym rozmiarze są używane do deklarowania tablic w stylu C jako elementów członkowskich struktur i są przydatne przede wszystkim w przypadku współdziałania z niezarządzanymi interfejsami API.
23.8.2 Deklaracje buforu o stałym rozmiarze
Bufor o stałym rozmiarze jest elementem członkowskim reprezentującym magazyn dla buforu o stałej długości zmiennych danego typu. Deklaracja buforu o stałym rozmiarze wprowadza co najmniej jeden bufor o stałym rozmiarze danego typu elementu.
Uwaga: podobnie jak tablica, bufor o stałym rozmiarze może być uważany za zawierający elementy. W związku z tym termin typ elementu zdefiniowany dla tablicy jest również używany z buforem o stałym rozmiarze. notatka końcowa
o stałym rozmiarze są dozwolone tylko w deklaracjach struktur i mogą występować tylko w niebezpiecznych kontekstach (§23.2).
fixed_size_buffer_declaration
: attributes? fixed_size_buffer_modifier* 'fixed' buffer_element_type
fixed_size_buffer_declarators ';'
;
fixed_size_buffer_modifier
: 'new'
| 'public'
| 'internal'
| 'private'
| 'unsafe'
;
buffer_element_type
: type
;
fixed_size_buffer_declarators
: fixed_size_buffer_declarator (',' fixed_size_buffer_declarator)*
;
fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;
Deklaracja buforu o stałym rozmiarze może zawierać zestaw atrybutów (§22), new
modyfikator (§15.3.5), modyfikatory ułatwień dostępu odpowiadające dowolnym zadeklarowanym dostępom dozwolonym dla składowych struktury (§16.4.3) i unsafe
modyfikator (§23.2). Atrybuty i modyfikatory mają zastosowanie do wszystkich elementów członkowskich zadeklarowanych przez deklarację buforu o stałym rozmiarze. Jest to błąd dla tego samego modyfikatora, który pojawia się wiele razy w deklaracji buforu o stałym rozmiarze.
Deklaracja buforu o stałym rozmiarze nie może zawierać static
modyfikatora.
Typ elementu buforu deklaracji buforu o stałym rozmiarze określa typ elementu wprowadzonych przez deklarację. Typ elementu buforu jest jednym ze wstępnie zdefiniowanych typów sbyte
, , short
ushort
uint
long
int
byte
char
float
ulong
double
lub .bool
Po typie elementu buforu następuje lista deklaratorów o stałym rozmiarze, z których każdy wprowadza nowy element członkowski. Deklarator buforu o stałym rozmiarze składa się z identyfikatora, który nazywa element członkowski, po którym następuje wyrażenie stałe ujęte w [
]
i tokeny. Wyrażenie stałe określa liczbę elementów w elemencie członkowskim wprowadzonym przez deklarator bufora o stałym rozmiarze. Typ wyrażenia stałego jest niejawnie konwertowany na typ int
, a wartość jest niezerową liczbą całkowitą dodatnią.
Elementy buforu o stałym rozmiarze należy określić sekwencyjnie w pamięci.
Deklaracja buforu o stałym rozmiarze, która deklaruje wiele o stałym rozmiarze, jest równoważna wielu deklaracjom deklaracji buforu o jednym stałym rozmiarze z tymi samymi atrybutami i typami elementów.
Przykład:
unsafe struct A { public fixed int x[5], y[10], z[100]; }
jest równoważny
unsafe struct A { public fixed int x[5]; public fixed int y[10]; public fixed int z[100]; }
przykład końcowy
23.8.3 o stałym rozmiarze w wyrażeniach
Odnośnik składowy (§12.5) elementu buforowego o stałym rozmiarze jest kontynuowany dokładnie tak jak wyszukiwanie składowych pola.
W wyrażeniu można odwoływać się do bufora o stałym rozmiarze przy użyciu simple_name (§12.8.4), member_access (§12.8.7) lub element_access (§12.8.12).
Gdy element członkowski buforu o stałym rozmiarze jest przywołyny jako prosta nazwa, efekt jest taki sam jak dostęp do elementu członkowskiego formularza this.I
, gdzie I
jest elementem członkowskim buforu o stałym rozmiarze.
W dostępie do elementu członkowskiego formularza E.I
, w którym E.
może być niejawny this.
, jeśli E
jest typem struktury i wyszukiwaniem I
składowym w tym typie struktury identyfikuje element członkowski o stałym rozmiarze, E.I
jest obliczany i klasyfikowany w następujący sposób:
- Jeśli wyrażenie
E.I
nie występuje w niebezpiecznym kontekście, wystąpi błąd czasu kompilacji. - Jeśli
E
wartość jest klasyfikowana jako wartość, wystąpi błąd czasu kompilacji. - W przeciwnym razie, jeśli
E
jest zmienną ruchomą (§23.4), wówczas:- Jeśli wyrażenie jest fixed_pointer_initializer (§23.7), wynik wyrażenia jest wskaźnikiem do pierwszego elementu elementu składowego buforu
I
o stałym rozmiarze w elemencieE
.E.I
- W przeciwnym razie, jeśli wyrażenie jest primary_no_array_creation_expression (§12.8.12.1) w element_access (§12.8.12) formularza
E.I[J]
, wynikE.I
jest wskaźnikiem,P
do pierwszego elementu buforaI
o stałym rozmiarze w ,E
a otaczające element_access jest następnie oceniane jako pointer_element_access (§23.6.4).P[J]
E.I
- W przeciwnym razie wystąpi błąd czasu kompilacji.
- Jeśli wyrażenie jest fixed_pointer_initializer (§23.7), wynik wyrażenia jest wskaźnikiem do pierwszego elementu elementu składowego buforu
E
W przeciwnym razie odwołuje się do zmiennej stałej, a wynik wyrażenia jest wskaźnikiem do pierwszego elementu buforuI
o stałym rozmiarze w elemencieE
. Wynik jest typuS*
, gdzie S jest typemI
elementu , i jest klasyfikowany jako wartość.
Dostęp do kolejnych elementów buforu o stałym rozmiarze można uzyskać przy użyciu operacji wskaźnika z pierwszego elementu. W przeciwieństwie do dostępu do tablic, dostęp do elementów bufora o stałym rozmiarze jest niebezpieczną operacją i nie jest sprawdzany zakres.
Przykład: Następujące deklaracje i używają struktury z elementem buforu o stałym rozmiarze.
unsafe struct Font { public int size; public fixed char name[32]; } class Test { unsafe static void PutString(string s, char* buffer, int bufSize) { int len = s.Length; if (len > bufSize) { len = bufSize; } for (int i = 0; i < len; i++) { buffer[i] = s[i]; } for (int i = len; i < bufSize; i++) { buffer[i] = (char)0; } } unsafe static void Main() { Font f; f.size = 10; PutString("Times New Roman", f.name, 32); } }
przykład końcowy
23.8.4 Sprawdzanie określonego przypisania
o stałym rozmiarze nie podlegają określonym sprawdzaniu przydziałów (§9.4), a elementy członkowskie buforu o stałym rozmiarze są ignorowane do celów określonego sprawdzania przypisania zmiennych typów struktury.
Gdy najbardziej zewnętrzna zmienna struktury składowej buforu o stałym rozmiarze jest zmienną statyczną, zmienną wystąpienia wystąpienia wystąpienia lub elementem tablicy, elementy bufora o stałym rozmiarze są automatycznie inicjowane do ich wartości domyślnych (§9.3). We wszystkich innych przypadkach początkowa zawartość buforu o stałym rozmiarze jest niezdefiniowana.
23.9 Alokacja stosu
Aby uzyskać ogólne informacje na temat operatora, zobacz §12.8.22.stackalloc
W tym miejscu omówiono zdolność tego operatora do spowodowania wskaźnika.
W niebezpiecznym kontekście, jeśli stackalloc_expression (§12.8.22) występuje jako początkowe wyrażenie local_variable_declaration (§13.6.2), gdzie local_variable_type jest typem wskaźnika (§23.3) lub wnioskowanym (var
), wynik stackalloc_expression jest wskaźnikiem T *
typu, który ma rozpoczynać przydzielony blok, gdzie T
to unmanaged_type stackalloc_expression.
W każdym innym zakresie semantyka local_variable_declarations (§13.6.2) i stackalloc_expressions (§12.8.22) w niebezpiecznych kontekstach przestrzegać tych określonych dla bezpiecznych kontekstów.
Przykład:
unsafe { // Memory uninitialized int* p1 = stackalloc int[3]; // Memory initialized int* p2 = stackalloc int[3] { -10, -15, -30 }; // Type int is inferred int* p3 = stackalloc[] { 11, 12, 13 }; // Can't infer context, so pointer result assumed var p4 = stackalloc[] { 11, 12, 13 }; // Error; no conversion exists long* p5 = stackalloc[] { 11, 12, 13 }; // Converts 11 and 13, and returns long* long* p6 = stackalloc[] { 11, 12L, 13 }; // Converts all and returns long* long* p7 = stackalloc long[] { 11, 12, 13 }; }
przykład końcowy
W przeciwieństwie do dostępu do tablic lub stackalloc
bloków typu "ed" Span<T>
, dostęp do elementów stackalloc
typu "ed bloku wskaźnika" jest niebezpieczną operacją i nie jest sprawdzany zakres.
Przykład: w poniższym kodzie
class Test { static string IntToString(int value) { if (value == int.MinValue) { return "-2147483648"; } int n = value >= 0 ? value : -value; unsafe { char* buffer = stackalloc char[16]; char* p = buffer + 16; do { *--p = (char)(n % 10 + '0'); n /= 10; } while (n != 0); if (value < 0) { *--p = '-'; } return new string(p, 0, (int)(buffer + 16 - p)); } } static void Main() { Console.WriteLine(IntToString(12345)); Console.WriteLine(IntToString(-999)); } }
wyrażenie
stackalloc
jest używane w metodzieIntToString
w celu przydzielenia buforu 16 znaków na stosie. Bufor jest automatycznie odrzucany, gdy metoda zwraca.Należy jednak pamiętać, że
IntToString
można go przepisać w trybie awaryjnym, czyli bez użycia wskaźników w następujący sposób:class Test { static string IntToString(int value) { if (value == int.MinValue) { return "-2147483648"; } int n = value >= 0 ? value : -value; Span<char> buffer = stackalloc char[16]; int idx = 16; do { buffer[--idx] = (char)(n % 10 + '0'); n /= 10; } while (n != 0); if (value < 0) { buffer[--idx] = '-'; } return buffer.Slice(idx).ToString(); } }
przykład końcowy
Koniec warunkowo normatywnego tekstu.
ECMA C# draft specification