Partage via


23 Code non sécurisé

23.1 Général

Une implémentation qui ne prend pas en charge le code non sécurisé est nécessaire pour diagnostiquer l’utilisation des règles syntaxiques définies dans cette clause.

Le reste de cette clause, y compris toutes ses sous-clauses, est conditionnellement normative.

Remarque : Le langage C# principal, tel que défini dans les clauses précédentes, diffère notamment de C et C++ dans son omission de pointeurs comme type de données. Au lieu de cela, C# fournit des références et la possibilité de créer des objets gérés par un garbage collector. Cette conception, associée à d’autres fonctionnalités, rend C# un langage beaucoup plus sûr que C ou C++. Dans le langage C# principal, il n’est tout simplement pas possible d’avoir une variable non initialisée, un pointeur « débordant » ou une expression qui indexe un tableau au-delà de ses limites. Les catégories entières de bogues qui pestent régulièrement C et C++ programmes sont ainsi éliminés.

Bien que pratiquement chaque construction de type pointeur en C ou C++ ait un équivalent de type référence en C#, il existe néanmoins des situations où l’accès aux types de pointeurs devient une nécessité. Par exemple, l’interfaçage avec le système d’exploitation sous-jacent, l’accès à un appareil mappé en mémoire ou l’implémentation d’un algorithme critique dans le temps peut ne pas être possible ou pratique sans accéder aux pointeurs. Pour répondre à ce besoin, C# permet d’écrire du code non sécurisé.

Dans le code non sécurisé, il est possible de déclarer et d’utiliser des pointeurs, d’effectuer des conversions entre des pointeurs et des types intégraux, de prendre l’adresse des variables, etc. Dans un sens, l’écriture de code non sécurisé ressemble beaucoup à l’écriture de code C dans un programme C#.

Le code non sécurisé est en fait une fonctionnalité « sécurisée » du point de vue des développeurs et des utilisateurs. Le code non sécurisé doit être clairement marqué avec le modificateur unsafe, afin que les développeurs ne puissent pas utiliser accidentellement des fonctionnalités dangereuses et que le moteur d’exécution fonctionne pour s’assurer que le code non sécurisé ne peut pas être exécuté dans un environnement non approuvé.

Note de fin

23.2 Contextes non sécurisés

Les fonctionnalités non sécurisées de C# sont disponibles uniquement dans des contextes non sécurisés. Un contexte non sécurisé est introduit en incluant un unsafe modificateur dans la déclaration d’un type, d’un membre ou d’une fonction locale, ou en employant un unsafe_statement :

  • Une déclaration d’une classe, d’un struct, d’une interface ou d’un délégué peut inclure un unsafe modificateur, auquel cas, l’étendue textuelle entière de cette déclaration de type (y compris le corps de la classe, du struct ou de l’interface) est considérée comme un contexte non sécurisé.

    Remarque : Si la type_declaration est partielle, seule cette partie est un contexte non sécurisé. Note de fin

  • Une déclaration d’un champ, d’une méthode, d’une propriété, d’un événement, d’un indexeur, d’un opérateur, d’un constructeur d’instance, d’un finaliseur, d’un constructeur statique ou d’une fonction locale peut inclure un unsafe modificateur, auquel cas, l’étendue textuelle entière de cette déclaration de membre est considérée comme un contexte non sécurisé.
  • Une unsafe_statement permet d’utiliser un contexte non sécurisé dans un bloc. L’étendue textuelle entière du bloc associé est considérée comme un contexte non sécurisé. Une fonction locale déclarée dans un contexte non sécurisé est elle-même dangereuse.

Les extensions de grammaire associées sont indiquées ci-dessous et dans les sous-sections suivantes.

unsafe_modifier
    : 'unsafe'
    ;

unsafe_statement
    : 'unsafe' block
    ;

Exemple : dans le code suivant

public unsafe struct Node
{
    public int Value;
    public Node* Left;
    public Node* Right;
}

le unsafe modificateur spécifié dans la déclaration de struct entraîne l’étendue textuelle entière de la déclaration de struct à devenir un contexte non sécurisé. Par conséquent, il est possible de déclarer les champs et Right les Left champs d’un type de pointeur. L’exemple ci-dessus peut également être écrit

public struct Node
{
    public int Value;
    public unsafe Node* Left;
    public unsafe Node* Right;
}

Ici, les unsafe modificateurs dans les déclarations de champ entraînent l’examen de ces déclarations comme des contextes non sécurisés.

exemple de fin

En dehors de l’établissement d’un contexte non sécurisé, permettant ainsi l’utilisation de types de pointeur, le unsafe modificateur n’a aucun effet sur un type ou un membre.

Exemple : dans le code suivant

public class A
{
    public unsafe virtual void F() 
    {
        char* p;
        ...
    }
}

public class B : A
{
    public override void F() 
    {
        base.F();
        ...
    }
}

le modificateur non sécurisé sur la F méthode entraîne A simplement l’étendue textuelle d’un F contexte non sécurisé dans lequel les fonctionnalités non sécurisées de la langue peuvent être utilisées. Dans le remplacement de l’élément B, il n’est pas nécessaire de spécifier à nouveau le unsafe modificateur, sauf si, bien sûr, la F méthode en B soi a besoin d’accéder F à des fonctionnalités non sécurisées.

La situation est légèrement différente lorsqu’un type de pointeur fait partie de la signature de la méthode

public unsafe class A
{
    public virtual void F(char* p) {...}
}

public class B: A
{
    public unsafe override void F(char* p) {...}
}

Ici, étant donné que Fla signature inclut un type de pointeur, elle ne peut être écrite qu’dans un contexte non sécurisé. Toutefois, le contexte non sécurisé peut être introduit en rendant la classe entière non sécurisée, comme c’est le cas dans , ou en Aincluant un unsafe modificateur dans la déclaration de méthode, comme c’est le cas dans B.

exemple de fin

Lorsque le unsafe modificateur est utilisé sur une déclaration de type partielle (§15.2.7), seul ce composant particulier est considéré comme un contexte non sécurisé.

23.3 Types de pointeurs

Dans un contexte non sécurisé, un type (§8.1) peut être un pointer_type ainsi qu’un value_type, un reference_type ou un type_parameter. Dans un contexte non sécurisé, un pointer_type peut également être le type d’élément d’un tableau (§17). Une pointer_type peut également être utilisée dans une expression de typeof (§12.8.18) en dehors d’un contexte non sécurisé (comme l’utilisation n’est pas dangereuse).

Un pointer_type est écrit en tant que unmanaged_type (§8.8) ou le mot clé void, suivi d’un * jeton :

pointer_type
    : value_type ('*')+
    | 'void' ('*')+
    ;

Le type spécifié avant le * type de pointeur dans un type de pointeur est appelé le type de référence du type de pointeur. Il représente le type de la variable à laquelle une valeur du type pointeur pointe.

Une pointer_type peut uniquement être utilisée dans un array_type dans un contexte non sécurisé (§23.2). Un non_array_type est tout type qui n’est pas lui-même un array_type.

Contrairement aux références (valeurs des types de référence), les pointeurs ne sont pas suivis par le garbage collector , le garbage collector n’a aucune connaissance des pointeurs et des données vers lesquelles ils pointent. Pour cette raison, un pointeur n’est pas autorisé à pointer vers une référence ou à un struct qui contient des références, et le type de référence d’un pointeur doit être un unmanaged_type. Les types de pointeur eux-mêmes sont des types non managés. Un type de pointeur peut donc être utilisé comme type de pointeur pour un autre type de pointeur.

La règle intuitive pour le mélange de pointeurs et de références est que les références de références (objets) sont autorisées à contenir des pointeurs, mais que les références de pointeurs ne sont pas autorisées à contenir des références.

Exemple : Voici quelques exemples de types de pointeurs dans le tableau ci-dessous :

Exemple Description
byte* Pointeur vers byte
char* Pointeur vers char
int** Pointeur vers le pointeur vers int
int*[] Tableau unidimensionnel de pointeurs vers int
void* Pointeur vers un type inconnu

exemple de fin

Pour une implémentation donnée, tous les types de pointeurs doivent avoir la même taille et la même représentation.

Remarque : Contrairement à C et C++, lorsque plusieurs pointeurs sont déclarés dans la même déclaration, en C#, il * est écrit avec le type sous-jacent uniquement, et non comme ponctuateur de préfixe sur chaque nom de pointeur. Par exemple :

int* pi, pj; // NOT as int *pi, *pj;  

Note de fin

La valeur d’un pointeur ayant un type T* représente l’adresse d’une variable de type T. L’opérateur d’indirection * de pointeur (§23.6.2) peut être utilisé pour accéder à cette variable.

Exemple : Étant donné une variable P de type int*, l’expression *P indique la int variable trouvée à l’adresse contenue dans P. exemple de fin

Comme une référence d’objet, un pointeur peut être null. L’application de l’opérateur indirection à un nullpointeur -valued entraîne un comportement défini par l’implémentation (§23.6.2). Un pointeur avec une valeur null est représenté par tous les bits-zéro.

Le void* type représente un pointeur vers un type inconnu. Étant donné que le type de référence est inconnu, l’opérateur indirection ne peut pas être appliqué à un pointeur de type void*, ni aucun arithmétique ne peut être effectué sur un tel pointeur. Toutefois, un pointeur de type void* peut être converti en n’importe quel autre type de pointeur (et vice versa) et comparé aux valeurs d’autres types de pointeur (§23.6.8).

Les types de pointeur sont une catégorie distincte de types. Contrairement aux types référence et aux types valeur, les types de object pointeur n’héritent pas et aucune conversion n’existe entre les types de pointeur et object. En particulier, la boxe et le déboxing (§8.3.13) ne sont pas pris en charge pour les pointeurs. Toutefois, les conversions sont autorisées entre différents types de pointeur et entre les types de pointeur et les types intégraux. Ceci est décrit dans le §23.5.

Un pointer_type ne peut pas être utilisé comme argument de type (§8.4) et l’inférence de type (§12.6.3) échoue sur les appels de méthode générique qui auraient déduit un argument de type pour être un type de pointeur.

Un pointer_type ne peut pas être utilisé comme type de sous-expression d’une opération liée dynamiquement (§12.3.3).

Une pointer_type ne peut pas être utilisée comme type du premier paramètre dans une méthode d’extension (§15.6.10).

Une pointer_type peut être utilisée comme type de champ volatile (§15.5.4).

L’effacement dynamique d’un type E* est le type de pointeur avec le type de référence de l’effacement dynamique de E.

Une expression avec un type de pointeur ne peut pas être utilisée pour fournir la valeur dans un member_declarator dans un anonymous_object_creation_expression (§12.8.17.7).

La valeur par défaut (§9.3) pour tout type de pointeur est null.

Remarque : Bien que les pointeurs puissent être passés en tant que paramètres de référence, cela peut entraîner un comportement non défini, car le pointeur peut bien être défini pour pointer vers une variable locale qui n’existe plus lorsque la méthode appelée retourne, ou l’objet fixe auquel il était utilisé pour pointer, n’est plus fixe. Par exemple :

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
        }
    }
}

Note de fin

Une méthode peut retourner une valeur d’un certain type et ce type peut être un pointeur.

Exemple : Lorsqu’un pointeur est attribué à une séquence contiguë de s, le nombre d’éléments intde cette séquence et une autre int valeur, la méthode suivante retourne l’adresse de cette valeur dans cette séquence, si une correspondance se produit ; sinon, elle retourne null:

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;
}

exemple de fin

Dans un contexte non sécurisé, plusieurs constructions sont disponibles pour fonctionner sur des pointeurs :

  • L’opérateur unaire * peut être utilisé pour effectuer un indirection de pointeur (§23.6.2).
  • L’opérateur -> peut être utilisé pour accéder à un membre d’un struct via un pointeur (§23.6.3).
  • L’opérateur [] peut être utilisé pour indexer un pointeur (§23.6.4).
  • L’opérateur unaire & peut être utilisé pour obtenir l’adresse d’une variable (§23.6.5).
  • Les ++ opérateurs et -- les opérateurs peuvent être utilisés pour incrémenter et décrémenter des pointeurs (§23.6.6).
  • Le binaire + et - les opérateurs peuvent être utilisés pour effectuer des arithmétiques de pointeur (§23.6.7).
  • Les ==opérateurs , <><=!=et >= les opérateurs peuvent être utilisés pour comparer les pointeurs (§23.6.8).
  • L’opérateur stackalloc peut être utilisé pour allouer de la mémoire à partir de la pile des appels (§23.9).
  • L’instruction fixed peut être utilisée pour corriger temporairement une variable afin que son adresse puisse être obtenue (§23.7).

23.4 Variables fixes et déplaçables

L’opérateur address-of (§23.6.5) et l’instruction fixed (§23.7) divisent les variables en deux catégories : variables fixes et variables déplaçables.

Les variables fixes résident dans des emplacements de stockage qui ne sont pas affectés par l’opération du garbage collector. (Les variables fixes incluent des variables locales, des paramètres de valeur et des variables créées par des pointeurs de désréférencement.) En revanche, les variables déplaçables résident dans des emplacements de stockage qui sont soumis à la réinstallation ou à l’élimination par le garbage collector. (Les exemples de variables déplaçables incluent des champs dans des objets et des éléments de tableaux.)

L’opérateur & (§23.6.5) permet d’obtenir l’adresse d’une variable fixe sans restrictions. Toutefois, étant donné qu’une variable déplaçable est soumise à la réinstallation ou à l’élimination par le garbage collector, l’adresse d’une variable déplaçable ne peut être obtenue qu’à l’aide d’un fixed statement (§23.7) et cette adresse reste valide uniquement pendant la durée de cette fixed instruction.

En termes précis, une variable fixe est l’une des suivantes :

Toutes les autres variables sont classées comme variables déplaçables.

Un champ statique est classé comme variable déplaçable. En outre, un paramètre de référence est classé comme variable déplaçable, même si l’argument donné pour le paramètre est une variable fixe. Enfin, une variable produite par le déreferencing d’un pointeur est toujours classifiée comme variable fixe.

Conversions de pointeur 23.5

23.5.1 Général

Dans un contexte non sécurisé, l’ensemble des conversions implicites disponibles (§10.2) est étendu pour inclure les conversions de pointeur implicite suivantes :

  • De n’importe quel pointer_type au type void*.
  • null Du littéral (§6.4.5.7) à n’importe quelle pointer_type.

En outre, dans un contexte non sécurisé, l’ensemble de conversions explicites disponibles (§10.3) est étendu pour inclure les conversions de pointeur explicite suivantes :

  • De n’importe quel pointer_type à tout autre pointer_type.
  • De sbyte, byteshortuintushortint, , , long, ou ulong à n’importe quelle pointer_type.
  • De n’importe quel pointer_type à sbyte, , shortintushortuintbyte, , longou .ulong

Enfin, dans un contexte non sécurisé, l’ensemble de conversions implicites standard (§10.4.2) inclut les conversions de pointeur suivantes :

  • De n’importe quel pointer_type au type void*.
  • null Du littéral à n’importe quel pointer_type.

Les conversions entre deux types de pointeur ne modifient jamais la valeur réelle du pointeur. En d’autres termes, une conversion d’un type de pointeur vers un autre n’a aucun effet sur l’adresse sous-jacente donnée par le pointeur.

Lorsqu’un type de pointeur est converti en un autre, si le pointeur résultant n’est pas correctement aligné pour le type pointu, le comportement n’est pas défini si le résultat est déréférence. En règle générale, le concept « correctement aligné » est transitif : si un pointeur vers un type A est correctement aligné pour un pointeur vers un type B, qui, à son tour, est correctement aligné pour un pointeur vers un type C, un pointeur vers le type A est correctement aligné pour un pointeur vers le type C.

Exemple : considérez le cas suivant dans lequel une variable ayant un type est accessible via un pointeur vers un autre type :

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
}

exemple de fin

Lorsqu’un type de pointeur est converti en pointeur en pointeur byte, le résultat pointe vers le niveau le plus bas de byte la variable. Incréments successifs du résultat, jusqu’à la taille de la variable, produisent des pointeurs vers les octets restants de cette variable.

Exemple : La méthode suivante affiche chacun des huit octets d’une double valeur hexadécimale :

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();
        }
    }
}

Bien sûr, la sortie produite dépend de l’endianité. Une possibilité est " BA FF 51 A2 90 6C 24 45".

exemple de fin

Les mappages entre les pointeurs et les entiers sont définis par l’implémentation.

Remarque : Toutefois, sur les architectures de processeur 32 et 64 bits avec un espace d’adressage linéaire, les conversions de pointeurs vers ou à partir de types intégraux se comportent généralement exactement comme des conversions ou uint ulong des valeurs, respectivement, vers ou depuis ces types intégraux. Note de fin

23.5.2 Tableaux de pointeurs

Les tableaux de pointeurs peuvent être construits à l’aide de array_creation_expression (§12.8.17.5) dans un contexte non sécurisé. Seules certaines conversions qui s’appliquent à d’autres types de tableaux sont autorisées sur les tableaux de pointeurs :

  • La conversion de référence implicite (§10.2.8) de n’importe quelle array_type vers System.Array et les interfaces qu’il implémente s’applique également aux tableaux de pointeurs. Toutefois, toute tentative d’accès aux éléments de tableau via System.Array ou aux interfaces qu’il implémente peut entraîner une exception au moment de l’exécution, car les types de pointeurs ne sont pas convertibles en object.
  • Les conversions de référence implicites et explicites (§10.2.8, §10.3.5) d’un type S[] de tableau unidimensionnel vers System.Collections.Generic.IList<T> et ses interfaces de base génériques ne s’appliquent jamais aux tableaux de pointeurs.
  • La conversion de référence explicite (§10.3.5) à partir System.Array des interfaces qu’il implémente dans n’importe quel array_type s’applique aux tableaux de pointeurs.
  • Les conversions de référence explicites (§10.3.5) depuis System.Collections.Generic.IList<S> et ses interfaces de base vers un type T[] de tableau unidimensionnel ne s’appliquent jamais aux tableaux de pointeurs, car les types de pointeur ne peuvent pas être utilisés comme arguments de type et il n’y a pas de conversions de types pointeurs en types non pointeurs.

Ces restrictions signifient que l’extension de l’instruction foreach sur les tableaux décrits dans le §9.4.4.17 ne peut pas être appliquée aux tableaux de pointeurs. Au lieu de cela, une foreach instruction du formulaire

foreach (V v in x)embedded_statement

où le type de tableau est un type de tableau du formulaire , n est le nombre de dimensions moins 1 et T ou V est un type de pointeur, est développé à l’aide de x boucles for-boucles imbriquées comme suit : T[,,...,]

{
    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*
            }
        }
    }
}

Les variables a, , i0, i1... in ne sont pas visibles ou accessibles aux x embedded_statement ou tout autre code source du programme. La variable v est en lecture seule dans l’instruction incorporée. S’il n’existe pas de conversion explicite (§23.5) de T (type d’élément) en V, une erreur est générée et aucune autre étape n’est effectuée. Si x la valeur nullest définie, une System.NullReferenceException valeur est levée au moment de l’exécution.

Remarque : bien que les types de pointeurs ne soient pas autorisés en tant qu’arguments de type, les tableaux de pointeurs peuvent être utilisés comme arguments de type. Note de fin

23.6 Pointeurs dans les expressions

23.6.1 Général

Dans un contexte non sécurisé, une expression peut générer un résultat d’un type de pointeur, mais en dehors d’un contexte non sécurisé, il s’agit d’une erreur au moment de la compilation pour qu’une expression soit d’un type de pointeur. En termes précis, en dehors d’un contexte non sécurisé, une erreur au moment de la compilation se produit si un simple_name (§12.8.4), member_access (§12.8.7), invocation_expression (§12.8.10) ou element_access (§12.8.12) est d’un type de pointeur.

Dans un contexte non sécurisé, les productions primary_no_array_creation_expression (§12.8) et unary_expression (§12.9) permettent des constructions supplémentaires, qui sont décrites dans les sous-sections suivantes.

Remarque : la priorité et l’associativité des opérateurs non sécurisés sont implicites par la grammaire. Note de fin

23.6.2 Indirection du pointeur

Un pointer_indirection_expression se compose d’un astérisque (*) suivi d’un unary_expression.

pointer_indirection_expression
    : '*' unary_expression
    ;

L’opérateur unaire * désigne l’indirection de pointeur et est utilisé pour obtenir la variable à laquelle pointe un pointeur. Le résultat de l’évaluation *P, où P est une expression d’un type T*pointeur , est une variable de type T. Il s’agit d’une erreur au moment de la compilation pour appliquer l’opérateur unaire * à une expression de type void* ou à une expression qui n’est pas d’un type de pointeur.

L’effet de l’application de l’opérateur unaire * à un nullpointeur -valued est défini par l’implémentation. En particulier, il n’y a aucune garantie que cette opération lève un System.NullReferenceException.

Si une valeur non valide a été affectée au pointeur, le comportement de l’opérateur unaire * n’est pas défini.

Remarque : Parmi les valeurs non valides pour la déréférence d’un pointeur par l’opérateur unaire * , il s’agit d’une adresse mal alignée pour le type pointé (voir l’exemple dans §23.5) et l’adresse d’une variable après la fin de sa durée de vie.

À des fins d’analyse d’affectation définie, une variable produite par l’évaluation d’une expression du formulaire *P est considérée comme initialement affectée (§9.4.2).

23.6.3 Accès aux membres du pointeur

Un pointer_member_access se compose d’un primary_expression, suivi d’un jeton «-> », suivi d’un identificateur et d’un type_argument_list facultatif.

pointer_member_access
    : primary_expression '->' identifier type_argument_list?
    ;

Dans un accès de membre de pointeur du formulaire P->I, P doit être une expression d’un type de pointeur et I désigner un membre accessible du type auquel P pointe.

Un accès de membre de pointeur du formulaire P->I est évalué exactement comme (*P).I. Pour obtenir une description de l’opérateur d’indirection de pointeur (*), consultez le §23.6.2. Pour obtenir une description de l’opérateur d’accès membre (.), consultez le §12.8.7.

Exemple : dans le code suivant

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());
        }
    }
}

l’opérateur -> est utilisé pour accéder aux champs et appeler une méthode d’un struct par le biais d’un pointeur. Étant donné que l’opération P->I est exactement équivalente à (*P).I, la Main méthode peut également avoir été écrite :

class Test
{
    static void Main()
    {
        Point point;
        unsafe
        {
            Point* p = &point;
            (*p).x = 10;
            (*p).y = 20;
            Console.WriteLine((*p).ToString());
        }
    }
}

exemple de fin

23.6.4 Accès à l’élément pointeur

Une pointer_element_access se compose d’une primary_no_array_creation_expression suivie d’une expression entre «[ » et «] ».

pointer_element_access
    : primary_no_array_creation_expression '[' expression ']'
    ;

Dans un accès d’élément de pointeur du formulaire P[E], P doit être une expression d’un type de pointeur autre que void*, et E doit être une expression qui peut être convertie implicitement en int, uint, longou ulong.

Un accès à l’élément pointeur du formulaire P[E] est évalué exactement comme *(P + E). Pour obtenir une description de l’opérateur d’indirection de pointeur (*), consultez le §23.6.2. Pour obtenir une description de l’opérateur d’ajout de pointeur (+), consultez le §23.6.7.

Exemple : dans le code suivant

class Test
{
    static void Main()
    {
        unsafe
        {
            char* p = stackalloc char[256];
            for (int i = 0; i < 256; i++)
            {
                p[i] = (char)i;
            }
        }
    }
}

Un accès à un élément de pointeur est utilisé pour initialiser la mémoire tampon de caractères dans une for boucle. Étant donné que l’opération P[E] équivaut précisément à *(P + E), l’exemple peut également avoir été écrit :

class Test
{
    static void Main()
    {
        unsafe
        {
            char* p = stackalloc char[256];
            for (int i = 0; i < 256; i++)
            {
                *(p + i) = (char)i;
            }
        }
    }
}

exemple de fin

L’opérateur d’accès aux éléments de pointeur ne vérifie pas les erreurs hors limites et le comportement lors de l’accès à un élément hors limites n’est pas défini.

Remarque : Il s’agit de la même chose que C et C++. Note de fin

23.6.5 L’opérateur d’adresse

Une addressof_expression se compose d’un ampersand (&) suivi d’un unary_expression.

addressof_expression
    : '&' unary_expression
    ;

Étant donné une expression E qui est d’un type T et est classifiée comme variable fixe (§23.4), la construction &E calcule l’adresse de la variable donnée par E. Le type du résultat est T* et est classé comme une valeur. Une erreur au moment de la compilation se produit si E elle n’est pas classifiée comme variable, si E elle est classée comme variable locale en lecture seule ou si E elle désigne une variable déplaçable. Dans le dernier cas, une instruction fixe (§23.7) peut être utilisée pour « corriger » temporairement la variable avant d’obtenir son adresse.

Remarque : Comme indiqué dans le §12.8.7, en dehors d’un constructeur d’instance ou d’un constructeur statique pour un struct ou une classe qui définit un readonly champ, ce champ est considéré comme une valeur, et non une variable. Par conséquent, son adresse ne peut pas être prise. De même, l’adresse d’une constante ne peut pas être prise. Note de fin

L’opérateur & ne nécessite pas que son argument soit définitivement attribué, mais après une & opération, la variable à laquelle l’opérateur est appliqué est considérée comme définitivement affectée dans le chemin d’exécution dans lequel l’opération se produit. Il incombe au programmeur de s’assurer que l’initialisation correcte de la variable a effectivement lieu dans cette situation.

Exemple : dans le code suivant

class Test
{
    static void Main()
    {
        int i;
        unsafe
        {
            int* p = &i;
            *p = 123;
        }
        Console.WriteLine(i);
    }
}

i est considéré comme définitivement attribué après l’opération &i utilisée pour initialiser p. L’affectation à *p initialiser ien vigueur, mais l’inclusion de cette initialisation est la responsabilité du programmeur et aucune erreur au moment de la compilation ne se produit si l’affectation a été supprimée.

exemple de fin

Remarque : Les règles d’affectation définitive pour l’opérateur & existent de telle sorte que l’initialisation redondante de variables locales puisse être évitée. Par exemple, de nombreuses API externes prennent un pointeur vers une structure qui est renseignée par l’API. Les appels à ces API passent généralement l’adresse d’une variable de struct locale, et sans la règle, l’initialisation redondante de la variable de struct serait nécessaire. Note de fin

Remarque : Lorsqu’une variable locale, un paramètre de valeur ou un tableau de paramètres est capturé par une fonction anonyme (§12.8.24), cette variable locale, paramètre ou tableau de paramètres n’est plus considérée comme une variable fixe (§23.7), mais est plutôt considérée comme une variable déplaçable. Par conséquent, il s’agit d’une erreur pour tout code non sécurisé qui prend l’adresse d’une variable locale, d’un paramètre de valeur ou d’un tableau de paramètres capturé par une fonction anonyme. Note de fin

23.6.6 Incrément et décrémentation du pointeur

Dans un contexte non sécurisé, les opérateurs (§12.8.16 et §12.9.6) peuvent être appliqués aux variables de pointeur de tous les types, sauf void*.-- ++ Ainsi, pour chaque type T*de pointeur, les opérateurs suivants sont implicitement définis :

T* operator ++(T* x);
T* operator --(T* x);

Les opérateurs produisent les mêmes résultats que x+1 et x-1, respectivement (§23.6.7). En d’autres termes, pour une variable de pointeur de type T*, l’opérateur ++ ajoute sizeof(T) à l’adresse contenue dans la variable et l’opérateur -- soustrait sizeof(T) de l’adresse contenue dans la variable.

Si une opération d’incrémentation ou de décrémentation de pointeur dépasse le domaine du type de pointeur, le résultat est défini par l’implémentation, mais aucune exception n’est produite.

23.6.7 Pointeur arithmétique

Dans un contexte non sécurisé, l’opérateur (§12.10.5) et - l’opérateur (§12.10.6) peuvent être appliqués aux valeurs de tous les types de pointeurs, sauf void*.+ Ainsi, pour chaque type T*de pointeur, les opérateurs suivants sont implicitement définis :

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);

Étant donné une expression d’un type T* pointeur et une expression N de type int, uintou longulong, les expressions P + N et N + P calculent la valeur de pointeur du type T* qui résulte de l’ajout N * sizeof(T) à l’adresse donnée par P.P De même, l’expression P – N calcule la valeur de pointeur du type T* qui résulte de la soustraction N * sizeof(T) de l’adresse donnée par P.

Étant donné deux expressions, P et Q, d’un type T*de pointeur , l’expression P – Q calcule la différence entre les adresses données P , puis Q divise cette différence par sizeof(T). Le type du résultat est toujours long. En effet, P - Q est calculé en tant que ((long)(P) - (long)(Q)) / sizeof(T).

Exemple :

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}");
        }
    }
}

qui produit la sortie :

p - q = -14
q - p = 14

exemple de fin

Si une opération arithmétique de pointeur dépasse le domaine du type de pointeur, le résultat est tronqué de manière définie par l’implémentation, mais aucune exception n’est produite.

Comparaison des pointeurs 23.6.8

Dans un contexte non sécurisé, les ==opérateurs , ><<=!=et >= opérateurs (§12.12) peuvent être appliqués aux valeurs de tous les types de pointeur. Les opérateurs de comparaison de pointeur sont les suivants :

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);

Étant donné qu’une conversion implicite existe de n’importe quel type de pointeur vers le void* type, les opérandes de n’importe quel type de pointeur peuvent être comparés à l’aide de ces opérateurs. Les opérateurs de comparaison comparent les adresses données par les deux opérandes comme s’ils étaient des entiers non signés.

23.6.9 Opérateur sizeof

Pour certains types prédéfinis (§12.8.19), l’opérateur sizeof génère une valeur constante int . Pour tous les autres types, le résultat de l’opérateur est défini par l’implémentation sizeof et est classé comme une valeur, et non comme une constante.

L’ordre dans lequel les membres sont emballés dans un struct n’est pas spécifié.

À des fins d’alignement, il peut y avoir un remplissage sans nom au début d’un struct, dans un struct et à la fin du struct. Le contenu des bits utilisés comme remplissage est indéterminé.

Lorsqu’il est appliqué à un opérande qui a un type de struct, le résultat est le nombre total d’octets d’une variable de ce type, y compris tout remplissage.

23.7 L’instruction fixe

Dans un contexte non sécurisé, la production embedded_statement (§13.1) permet une construction supplémentaire, l’instruction fixe, qui est utilisée pour « corriger » une variable déplaçable telle que son adresse reste constante pendant la durée de l’instruction.

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
    ;

Chaque fixed_pointer_declarator déclare une variable locale du pointer_type donné et initialise cette variable locale avec l’adresse calculée par le fixed_pointer_initializer correspondant. Une variable locale déclarée dans une instruction fixe est accessible dans toutes les fixed_pointer_initializerse produisant à droite de la déclaration de cette variable, et dans la embedded_statement de l’instruction fixe. Une variable locale déclarée par une instruction fixe est considérée comme en lecture seule. Une erreur au moment de la compilation se produit si l’instruction incorporée tente de modifier cette variable locale (via l’affectation ou les ++ -- opérateurs) ou de la transmettre en tant que paramètre de référence ou de sortie.

Il s’agit d’une erreur d’utilisation d’une variable locale capturée (§12.19.6.2), d’un paramètre de valeur ou d’un tableau de paramètres dans un fixed_pointer_initializer. Un fixed_pointer_initializer peut être l’un des éléments suivants :

  • Le jeton «& » suivi d’un variable_reference (§9.5) vers une variable déplaçable (§23.4) d’un type Tnon managé , à condition que le type T* soit implicitement convertible en type pointeur donné dans l’instruction fixed . Dans ce cas, l’initialiseur calcule l’adresse de la variable donnée et la variable est garantie de rester à une adresse fixe pendant la durée de l’instruction fixe.
  • Expression d’une array_type avec des éléments d’un type Tnon managé , à condition que le type T* soit implicitement convertible en type pointeur donné dans l’instruction fixe. Dans ce cas, l’initialiseur calcule l’adresse du premier élément du tableau, et l’intégralité du tableau est garantie de rester à une adresse fixe pendant la durée de l’instruction fixed . Si l’expression de tableau est null ou si le tableau a zéro élément, l’initialiseur calcule une adresse égale à zéro.
  • Expression de type string, à condition que le type char* soit implicitement convertible en type pointeur donné dans l’instruction fixed . Dans ce cas, l’initialiseur calcule l’adresse du premier caractère dans la chaîne, et la chaîne entière est garantie de rester à une adresse fixe pendant la durée de l’instruction fixed . Le comportement de l’instruction est défini par l’implémentation fixed si l’expression de chaîne est null.
  • Expression de type autre que array_type ou , à condition qu’il existe une méthode accessible ou une méthode d’extension accessible correspondant à la signatureref [readonly] T GetPinnableReference(), où T est un unmanaged_type et T* est implicitement convertible en type pointeur donné dans l’instructionfixed.string Dans ce cas, l’initialiseur calcule l’adresse de la variable retournée et cette variable est garantie de rester à une adresse fixe pendant la durée de l’instruction fixed . Une GetPinnableReference() méthode peut être utilisée par l’instruction lorsque la fixed résolution de surcharge (§12.6.4) produit exactement un membre de fonction et que ce membre de fonction satisfait aux conditions précédentes. La GetPinnableReference méthode doit retourner une référence à une adresse égale à zéro, telle que celle retournée lorsqu’il n’y a pas de System.Runtime.CompilerServices.Unsafe.NullRef<T>() données à épingler.
  • Un simple_name ou member_access qui fait référence à un membre de mémoire tampon de taille fixe d’une variable déplaçable, à condition que le type du membre de la mémoire tampon de taille fixe soit implicitement convertible en type de pointeur donné dans l’instruction fixed . Dans ce cas, l’initialiseur calcule un pointeur vers le premier élément de la mémoire tampon de taille fixe (§23.8.3) et la mémoire tampon de taille fixe est garantie de rester à une adresse fixe pendant la durée de l’instruction fixed .

Pour chaque adresse calculée par un fixed_pointer_initializer l’instruction fixed garantit que la variable référencée par l’adresse n’est pas soumise à la réinstallation ou à l’élimination par le garbage collector pendant la durée de l’instruction fixed .

Exemple : Si l’adresse calculée par un fixed_pointer_initializer fait référence à un champ d’un objet ou à un élément d’une instance de tableau, l’instruction fixe garantit que l’instance d’objet conteneur n’est pas déplacée ou supprimée pendant la durée de vie de l’instruction. exemple de fin

Il incombe au programmeur de s’assurer que les pointeurs créés par des instructions fixes ne survivent pas au-delà de l’exécution de ces instructions.

Exemple : lorsque des pointeurs créés par fixed des instructions sont passés à des API externes, il incombe au programmeur de s’assurer que les API ne conservent aucune mémoire de ces pointeurs. exemple de fin

Les objets fixes peuvent entraîner la fragmentation du tas (car ils ne peuvent pas être déplacés). Pour cette raison, les objets doivent être résolus uniquement si nécessaire, puis uniquement pour la durée la plus courte possible.

Exemple : l’exemple

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);
        }
    }
}

illustre plusieurs utilisations de l’instruction fixed . La première instruction fixe et obtient l’adresse d’un champ statique, la deuxième instruction fixe et obtient l’adresse d’un champ d’instance, ainsi que les troisièmes correctifs d’instruction et obtient l’adresse d’un élément de tableau. Dans chaque cas, il aurait été une erreur d’utiliser l’opérateur normal & , car les variables sont toutes classées comme variables déplaçables.

Les troisième et quatrième fixed instructions de l’exemple ci-dessus produisent des résultats identiques. En général, pour une instance ade tableau, la a[0] spécification dans une fixed instruction est la même que la simple spécification a.

exemple de fin

Dans un contexte non sécurisé, les éléments de tableau de tableaux unidimensionnels sont stockés dans l’ordre croissant de l’index, en commençant par l’index 0 et en se terminant par l’index Length – 1. Pour les tableaux multidimensionnels, les éléments de tableau sont stockés de telle sorte que les index de la dimension la plus à droite soient augmentés en premier, puis la dimension gauche suivante, et ainsi de suite à gauche.

Dans une fixed instruction qui obtient un pointeur p vers une instance ade tableau, les valeurs de pointeur allant de p à p + a.Length - 1 représenter les adresses des éléments du tableau. De même, les variables allant de p[0] à p[a.Length - 1] représenter les éléments de tableau réels. Étant donné la façon dont les tableaux sont stockés, un tableau de n’importe quelle dimension peut être traité comme s’il s’agissait d’un tableau linéaire.

Exemple :

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();
            }
        }
    }
}

qui produit la sortie :

[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

exemple de fin

Exemple : dans le code suivant

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);
        }
    }
}

une fixed instruction est utilisée pour corriger un tableau afin que son adresse puisse être passée à une méthode qui prend un pointeur.

exemple de fin

Une char* valeur produite en corrigeant une instance de chaîne pointe toujours vers une chaîne terminée par null. Dans une instruction fixe qui obtient un pointeur p vers une instance sde chaîne, les valeurs de pointeur allant de p à p + s.Length ‑ 1 représenter les adresses des caractères de la chaîne, et la valeur p + s.Length du pointeur pointe toujours vers un caractère Null (le caractère avec la valeur '\0').

Exemple :

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);
        }
    }
}

exemple de fin

Exemple : Le code suivant montre un fixed_pointer_initializer avec une expression de type autre que array_type ou 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)
        {
            // ...
        }
    }
}

Le type C a une méthode accessible GetPinnableReference avec la signature correcte. Dans l’instruction fixed , le ref int retour de cette méthode lorsqu’elle est appelée c est utilisée pour initialiser le int* pointeur p. exemple de fin

La modification d’objets de type managé par le biais de pointeurs fixes peut entraîner un comportement non défini.

Remarque : par exemple, étant donné que les chaînes sont immuables, il incombe au programmeur de s’assurer que les caractères référencés par un pointeur vers une chaîne fixe ne sont pas modifiés. Note de fin

Remarque : L’arrêt automatique null des chaînes est particulièrement pratique lors de l’appel d’API externes qui attendent des chaînes de type C. Notez toutefois qu’une instance de chaîne est autorisée à contenir des caractères Null. Si de tels caractères null sont présents, la chaîne s’affiche tronquée lorsqu’elle est traitée comme une valeur null char*terminée. Note de fin

23.8 Mémoires tampons de taille fixe

23.8.1 Général

Les mémoires tampons de taille fixe sont utilisées pour déclarer des tableaux en ligne « C-style » en tant que membres de structs et sont principalement utiles pour interagir avec des API non managées.

23.8.2 Déclarations de mémoire tampon de taille fixe

Une mémoire tampon de taille fixe est un membre qui représente le stockage d’une mémoire tampon de longueur fixe de variables d’un type donné. Une déclaration de mémoire tampon de taille fixe introduit une ou plusieurs mémoires tampons de taille fixe d’un type d’élément donné.

Remarque : Comme un tableau, une mémoire tampon de taille fixe peut être considérée comme contenant des éléments. Par conséquent, le type d’élément de terme tel que défini pour un tableau est également utilisé avec une mémoire tampon de taille fixe. Note de fin

Les mémoires tampons de taille fixe sont autorisées uniquement dans les déclarations de struct et peuvent uniquement se produire dans des contextes non sécurisés (§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 ']'
    ;

Une déclaration de mémoire tampon de taille fixe peut inclure un ensemble d’attributs (§22), un new modificateur (§15.3.5), des modificateurs d’accessibilité correspondant à l’une des accessibilités déclarées autorisées pour les membres de struct (§16.4.3) et un unsafe modificateur (§23.2). Les attributs et les modificateurs s’appliquent à tous les membres déclarés par la déclaration de mémoire tampon de taille fixe. Il s’agit d’une erreur pour que le même modificateur apparaisse plusieurs fois dans une déclaration de mémoire tampon de taille fixe.

Une déclaration de mémoire tampon de taille fixe n’est pas autorisée à inclure le static modificateur.

Le type d’élément de mémoire tampon d’une déclaration de mémoire tampon de taille fixe spécifie le type d’élément des mémoires tampons introduites par la déclaration. Le type d’élément de mémoire tampon doit être l’un des types sbyteprédéfinis , byteshort, ushort, int, uint, , long, ulongchar, , floatdoubleou .bool

Le type d’élément de mémoire tampon est suivi d’une liste de déclarateurs de mémoire tampon de taille fixe, chacun d’entre eux introduit un nouveau membre. Un déclarateur de mémoire tampon de taille fixe se compose d’un identificateur qui nomme le membre, suivi d’une expression constante placée entre [ des jetons et ] des jetons. L’expression constante indique le nombre d’éléments dans le membre introduit par ce déclarateur de mémoire tampon de taille fixe. Le type de l’expression constante doit être implicitement convertible en type int, et la valeur doit être un entier positif non nul.

Les éléments d’une mémoire tampon de taille fixe doivent être disposés séquentiellement en mémoire.

Une déclaration de mémoire tampon de taille fixe qui déclare plusieurs mémoires tampons de taille fixe équivaut à plusieurs déclarations d’une seule déclaration de mémoire tampon de taille fixe avec les mêmes attributs et types d’éléments.

Exemple :

unsafe struct A
{
    public fixed int x[5], y[10], z[100];
}

équivaut à :

unsafe struct A
{
    public fixed int x[5];
    public fixed int y[10];
    public fixed int z[100];
}

exemple de fin

23.8.3 Mémoires tampons de taille fixe dans les expressions

La recherche de membre (§12.5) d’un membre de mémoire tampon de taille fixe se poursuit exactement comme la recherche de membre d’un champ.

Une mémoire tampon de taille fixe peut être référencée dans une expression à l’aide d’un simple_name (§12.8.4), d’un member_access (§12.8.7) ou d’un element_access (§12.8.12).

Lorsqu’un membre de mémoire tampon de taille fixe est référencé sous la forme d’un nom simple, l’effet est le même qu’un accès membre du formulaire this.I, où I est le membre de la mémoire tampon de taille fixe.

Dans un accès membre du formulaire E.IE. il peut s’agir du type implicite this., s’il E s’agit d’un type de struct et d’une recherche membre de I ce type de struct identifie un membre de taille fixe, il E.I est évalué et classé comme suit :

  • Si l’expression E.I ne se produit pas dans un contexte non sécurisé, une erreur au moment de la compilation se produit.
  • Si E elle est classifiée comme valeur, une erreur au moment de la compilation se produit.
  • Sinon, s’il s’agit E d’une variable déplaçable (§23.4), puis :
    • Si l’expression E.I est un fixed_pointer_initializer (§23.7), le résultat de l’expression est un pointeur vers le premier élément du membre I de mémoire tampon de taille fixe dans E.
    • Sinon, si l’expression E.I est un primary_no_array_creation_expression (§12.8.12.1) dans un element_access (§12.8.12) du formulaire E.I[J], alors le résultat est E.I un pointeur, Pvers le premier élément du membre I de mémoire tampon de taille fixe dans E, et le element_access englobant est ensuite évalué comme l’pointer_element_access (§23.6.4) P[J].
    • Sinon, une erreur au moment de la compilation se produit.
  • Sinon, E référence une variable fixe et le résultat de l’expression est un pointeur vers le premier élément du membre I de mémoire tampon de taille fixe dans E. Le résultat est de type S*, où S est le type d’élément de I, et est classé comme une valeur.

Les éléments suivants de la mémoire tampon de taille fixe sont accessibles à l’aide d’opérations de pointeur à partir du premier élément. Contrairement à l’accès aux tableaux, l’accès aux éléments d’une mémoire tampon de taille fixe est une opération non sécurisée et n’est pas vérifié par plage.

Exemple : le code suivant déclare et utilise un struct avec un membre de mémoire tampon de taille fixe.

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);
    }
}

exemple de fin

23.8.4 Vérification de l’affectation définitive

Les mémoires tampons de taille fixe ne sont pas soumises à la vérification de l’affectation définie (§9.4) et les membres de mémoire tampon de taille fixe sont ignorés à des fins de vérification de l’affectation définitive des variables de type struct.

Lorsque la variable de struct la plus externe d’un membre de mémoire tampon de taille fixe est une variable statique, une variable d’instance d’une instance de classe ou un élément de tableau, les éléments de la mémoire tampon de taille fixe sont automatiquement initialisés à leurs valeurs par défaut (§9.3). Dans tous les autres cas, le contenu initial d’une mémoire tampon de taille fixe n’est pas défini.

23.9 Allocation de pile

Consultez le §12.8.22 pour obtenir des informations générales sur l’opérateur stackalloc. Ici, la capacité de cet opérateur à entraîner un pointeur est abordée.

Dans un contexte non sécurisé si une stackalloc_expression (§12.8.22) se produit en tant qu’expression d’initialisation d’un local_variable_declaration (§13.6.2), où le local_variable_type est un type de pointeur (§23.3) ou déduit (var), le résultat de l’stackalloc_expression est un pointeur de type T * à partir du bloc alloué, où T est la unmanaged_type du stackalloc_expression.

Dans tous les autres aspects, la sémantique des local_variable_declaration(§13.6.2) et des stackalloc_expression(§12.8.22) dans des contextes non sécurisés suivent celles définies pour les contextes sécurisés.

Exemple :

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 };
}

exemple de fin

Contrairement à l’accès aux tableaux ou stackallocaux blocs de Span<T> type « ed », l’accès aux éléments d’un stackalloc« bloc de pointeur ed de type est une opération non sécurisée et n’est pas vérifié par plage.

Exemple : dans le code suivant

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));
    }
}

une stackalloc expression est utilisée dans la IntToString méthode pour allouer une mémoire tampon de 16 caractères sur la pile. La mémoire tampon est automatiquement ignorée lorsque la méthode retourne.

Notez toutefois que cela IntToString peut être réécrit en mode sans échec ; autrement dit, sans utiliser de pointeurs, comme suit :

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();
    }
}

exemple de fin

Fin du texte normatif conditionnel.