Udostępnij za pośrednictwem


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 i Right 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 w A programie po prostu powoduje, że tekstowy zakres F staje się niebezpiecznym kontekstem, w którym mogą być używane niebezpieczne funkcje języka. W przesłonięcie F w Bpliku nie ma potrzeby ponownego unsafe określania modyfikatora — chyba że F sama metoda B 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 Fzawiera 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 , Alub przez dołączenie unsafe 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 typu int*, wyrażenie *P oznacza int zmienną znajdującą się pod adresem zawartym w Pelem. przykład końcowy

Podobnie jak odwołanie do obiektu, wskaźnik może mieć wartość null. Zastosowanie operatora pośredniego nulldo 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 ints, liczba elementów tej sekwencji i inna int wartość, następująca metoda zwraca adres tej wartości w tej sekwencji, jeśli wystąpi dopasowanie; w przeciwnym razie zwraca nullwartość :

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:

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, intuintlongushortlub ulong do dowolnego pointer_type.
  • Z dowolnego pointer_type do sbyte, byte, , shortushort, int, uintlonglub ulong.

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 bytewartość , 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 lub ulong 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średnictwem System.Array lub implementowane interfejsy może spowodować wyjątek w czasie wykonywania, ponieważ typy wskaźników nie są konwertowane na object.
  • Niejawne i jawne konwersje odwołań (§10.2.8, §10.3.5) z typu S[] tablicy jednowymiarowej do System.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 typu T[] 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->IP 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ż operacja P->I jest dokładnie równoważna (*P).Imetodzie , 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, , uintlonglub 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ż operacja P[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 zainicjowania p. Przypisanie *p do w efekcie inicjuje ielement , 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, longlub 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 Pelement .

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ądzanego T, pod warunkiem, że typ T* jest niejawnie konwertowany do typu wskaźnika podanego w instrukcji fixed . 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 Tniezarządzanego , pod warunkiem, że typ jest niejawnie konwertowany na typ T* 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 trwania fixed instrukcji. Jeśli wyrażenie tablicy jest null lub tablica ma zero elementów, inicjator oblicza adres równy zero.
  • Wyrażenie typu string, pod warunkiem, że typ char* jest niejawnie konwertowany na typ wskaźnika podany w instrukcji fixed . W takim przypadku inicjator oblicza adres pierwszego znaku w ciągu, a cały ciąg musi pozostać na stałym adresie przez czas trwania fixed instrukcji . Zachowanie instrukcji fixed jest definiowane przez implementację, jeśli wyrażenie ciągu to null.
  • 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(), gdzie T jest unmanaged_type i T* jest niejawnie konwertowany na typ wskaźnika podany w fixed instrukcji.string W tym przypadku inicjator oblicza adres zwracanej zmiennej, a zmienna ta ma gwarancję pozostania na stałym adresie przez czas trwania fixed 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. Metoda GetPinnableReference powinna zwrócić odwołanie do adresu równego zero, na przykład zwrócone z System.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 instrukcjifixed. 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 trwania fixed 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ąpienia aa[0] tablicy określenie w fixed instrukcji jest takie samo, jak po prostu określenie elementu a.

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 atablicy , 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 scią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 instrukcji fixed zwracany z tej metody, ref int gdy jest wywoływany c , jest używany do inicjowania int* wskaźnika p. 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, , shortushortuintlongintbytecharfloatulongdoublelub .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 elemencie E. 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], wynik E.I jest wskaźnikiem, Pdo pierwszego elementu bufora I o stałym rozmiarze w , Ea 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.
  • E W przeciwnym razie odwołuje się do zmiennej stałej, a wynik wyrażenia jest wskaźnikiem do pierwszego elementu buforu I o stałym rozmiarze w elemencie E. Wynik jest typu S*, gdzie S jest typem Ielementu , 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 stackallocbloków typu "ed" Span<T> , dostęp do elementów stackalloctypu "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 metodzie IntToString 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.