12 Expressions
12.1 Général
Une expression est une séquence d’opérateurs et d’opérandes. Cette clause définit la syntaxe, l’ordre d’évaluation des opérandes et des opérateurs, ainsi que la signification des expressions.
12.2 Classifications des expressions
12.2.1 Général
Le résultat d’une expression est classé en l’une des catégories suivantes :
- Une valeur. Chaque valeur possède un type associé.
- Une variable. Sauf indication contraire, une variable a un type explicite et possède un type associé, à savoir le type déclaré de la variable. Une variable dont le type est implicite n’a pas de type associé.
- Un littéral nul. Une expression de cette classification peut être convertie implicitement en type référence ou en type valeur nullable.
- Une fonction anonyme. Une expression avec cette classification peut être convertie implicitement en type délégué ou type d'arborescence d'expressions compatibles.
- Un tuple. Chaque tuple possède un nombre fixe d’éléments, chacun comportant une expression et un nom d’élément de tuple optionnel.
- Un accès à une propriété. Chaque accès de propriété a un type associé, à savoir le type de la propriété. En outre, un accès de propriété peut avoir une expression d’instance associée. Lorsqu’un accesseur d’un accès de propriété d’instance est appelé, le résultat de l’évaluation de l’expression d’instance devient l’instance représentée par
this
(§12.8.14). - Un accès à un indexeur. Chaque accès d’indexeur possède un type associé, à savoir le type des éléments de l’indexeur. En outre, un accès d’indexeur a une expression d’instance associée et une liste d’arguments associée. Lorsqu’un accesseur d’un accès d’indexeur est appelé, le résultat de l’évaluation de l’expression d’instance devient l’instance représentée par
this
(§12.8.14), et le résultat de l’évaluation de la liste d’arguments devient la liste de paramètres de l’appel. - Nothing. Cela se produit lorsque l’expression est un appel d’une méthode dont le type de retour est
void
. Une expression classée comme rien n’est valide que dans le contexte d’une statement_expression (§13.7) ou comme corps d’une lambda_expression (§12.19).
Pour les expressions qui apparaissent en tant que sous-expressions d’expressions plus grandes, avec les restrictions indiquées, le résultat peut également être classé dans l’une des catégories suivantes :
- Espace de noms. Une expression de cette classification ne peut apparaître que sur le côté gauche d’un member_access (§12.8.7). Dans tout autre contexte, une expression classée comme un espace de noms entraîne une erreur de compilation.
- Type. Une expression de cette classification ne peut apparaître que sur le côté gauche d’un member_access (§12.8.7). Dans tout autre contexte, une expression classée comme un type entraîne une erreur de compilation.
- Un groupe de méthodes, c'est-à-dire un ensemble de méthodes surchargées résultant d’une recherche de membres (§12.5). Un groupe de méthodes peut avoir une expression d’instance associée et une liste d’arguments de type associée. Lorsque une méthode d’instance est appelée, le résultat de l’évaluation de l’expression d’instance devient l’instance représentée par
this
(§12.8.14). Un groupe de méthodes est autorisé dans une invocation_expression (§12.8.10) ou une delegate_creation_expression (§12.8.17.6), et peut être converti implicitement en un type délégué compatible (§10.8). Dans tout autre contexte, une expression classée comme un groupe de méthodes entraîne une erreur de compilation. - Un accès à un événement. Chaque accès d’événement a un type associé, à savoir le type de l’événement. En outre, un accès d’événement peut avoir une expression d’instance associée. Un accès à un événement peut apparaître comme l'opérande gauche des opérateurs
+=
et-=
(§12.21.5). Dans tout autre contexte, une expression classée comme accès d’événement entraîne une erreur de compilation. Lorsqu’un accesseur d’un accès d’événement d’instance est appelé, le résultat de l’évaluation de l’expression d’instance devient l’instance représentée parthis
(§12.8.14). - Une expression throw, qui peut être utilisée dans plusieurs contextes pour lever une exception dans une expression. Une expression de type « throw » peut être convertie par une conversion implicite vers n'importe quel type.
Un accès à une propriété ou à un indexeur est toujours reclassifié en tant que valeur en invoquant soit l'accesseur get, soit l'accesseur set. L’accesseur particulier est déterminé par le contexte de l’accès à la propriété ou à l’indexeur : si l’accès est la cible d’une affectation, l’accesseur set est appelé pour assigner une nouvelle valeur (§12.21.2). Sinon, l’accesseur get est appelé pour obtenir la valeur actuelle (§12.2.2).
Un accesseur d’instance est un accès de propriété sur une instance, un accès d’événement sur une instance ou un accès d’indexeur.
12.2.2 Valeurs des expressions
La plupart des constructions qui impliquent une expression exigent au final que celle-ci dénote une valeur. Dans ce cas, si l’expression réelle dénote un espace de noms, un type, un groupe de méthodes ou rien, une erreur de compilation se produit. Cependant, si l’expression dénote un accès de propriété, un accès d’indexeur ou une variable, la valeur de la propriété, de l’indexeur ou de la variable est substituée implicitement :
- La valeur d’une variable est tout simplement la valeur actuellement stockée dans l’emplacement mémoire identifié par la variable. Une variable doit être considérée comme définitivement assignée (§9.4) avant que sa valeur puisse être obtenue, sinon une erreur de compilation se produit.
- La valeur d'une expression d'accès à une propriété est obtenue en appelant l'accesseur 'get' de la propriété. Si la propriété ne possède pas d’accesseur get, une erreur de compilation se produit. Sinon, un appel de membre fonctionnel (§12.6.6) est effectué, et le résultat de l’appel devient la valeur de l’expression d’accès à la propriété.
- La valeur d’une expression d’accès à un indexeur est obtenue en appelant l’accesseur get de l’indexeur. Si l’indexeur ne possède pas d’accesseur get, une erreur de compilation se produit. Sinon, un appel de membre fonctionnel (§12.6.6) est effectué avec la liste d’arguments associée à l’expression d’accès à l’indexeur, et le résultat de l’appel devient la valeur de l’expression d’accès à l’indexeur.
- La valeur d’une expression de tuple est obtenue en appliquant une conversion implicite de tuple (§10.2.13) au type de l’expression de tuple. Il est erroné d'obtenir la valeur d'une expression de tuple qui n'a pas de type.
12.3 Liaison statique et dynamique
12.3.1 Général
La liaison est le processus qui consiste à déterminer ce à quoi une opération fait référence, sur la base du type ou de la valeur des expressions (arguments, opérandes, récepteurs). Par exemple, la liaison d’un appel de méthode est déterminée en fonction du type du récepteur et des arguments. La liaison d'un opérateur est déterminée en fonction du type de ses opérandes.
En C#, la liaison d’une opération est généralement déterminée à la compilation, sur la base du type de ses sous-expressions à la compilation. De même, si une expression contient une erreur, celle-ci est détectée et signalée à la compilation. Cette approche est connue sous le nom de liaison statique.
Cependant, si une expression est une expression dynamique (c’est-à-dire qu’elle a le type dynamic
), cela indique que toute liaison à laquelle elle participe doit se fonder sur son type à l’exécution plutôt que sur le type dont elle dispose à la compilation. La liaison d'une telle opération est donc différée jusqu'au moment où l'opération doit être exécutée pendant l'exécution du programme. On parle alors de liaison dynamique.
Lorsqu’une opération est liée dynamiquement, peu ou pas de vérifications sont effectuées à la compilation. Au contraire, si la liaison à l’exécution échoue, des erreurs sont signalées sous forme d’exceptions à l’exécution.
Les opérations suivantes en C# sont sujettes à la liaison :
- Accès de membre :
e.M
- Invocation de méthode :
e.M(e₁,...,eᵥ)
- Invocation d'un délégué :
e(e₁,...,eᵥ)
- Accès d’élément :
e[e₁,...,eᵥ]
- Création d'objet : nouveau
C(e₁,...,eᵥ)
- Opérateurs unaires surchargés :
+
,-
,!
(seulement la négation logique),~
,++
,--
,true
,false
- Opérateurs binaires surchargés :
+
,-
,*
,/
,%
,&
,&&
,|
,||
,??
,^
,<<
,>>
,==
,!=
,>
,<
,>=
,<=
- Opérateurs d’affectation :
=
,= ref
,+=
,-=
,*=
,/=
,%=
,&=
,|=
,^=
,<<=
,>>=
- Conversions implicites et explicites
Lorsque aucune expression dynamique n’est impliquée, C# utilise par défaut la liaison statique, ce qui signifie que les types des sous-expressions à la compilation sont utilisés dans le processus de sélection. Cependant, lorsque l’une des sous-expressions dans les opérations énumérées ci-dessus est une expression dynamique, l’opération est alors liée dynamiquement.
Il s’agit d’une erreur de compilation si un appel de méthode est lié dynamiquement et que l’un des paramètres, comme le récepteur, est un paramètre d’entrée.
12.3.2 Temps de liaison
La liaison statique a lieu à la compilation, tandis que la liaison dynamique a lieu à l’exécution. Dans les sous-clauses suivantes, le terme binding-time se réfère soit à la compilation, soit à l’exécution, selon le moment où la liaison a lieu.
Exemple : ce qui suit illustre les notions de liaison statique et dynamique ainsi que le moment de la liaison :
object o = 5; dynamic d = 5; Console.WriteLine(5); // static binding to Console.WriteLine(int) Console.WriteLine(o); // static binding to Console.WriteLine(object) Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)
Les deux premiers appels sont statiquement liés : la surcharge de
Console.WriteLine
est choisie en fonction du type de leur argument au moment de la compilation. Le temps de liaison est donc compile-time.Le troisième appel est lié dynamiquement : la surcharge de
Console.WriteLine
est choisie en fonction du type d'exécution de son argument. Cela se produit parce que l’argument est une expression dynamique : son type à la compilation est dynamic. Ainsi, le temps de liaison pour le troisième appel est le run-time.exemple final
12.3.3 Liaison dynamique
Cette sous-clause est informative.
La liaison dynamique permet aux programmes C# d’interagir avec des objets dynamiques, c’est-à-dire des objets qui ne respectent pas les règles normales du système de types de C#. Les objets dynamiques peuvent être des objets d’autres langages de programmation avec différents systèmes de types, ou ils peuvent être des objets configurés par programme pour implémenter leur propre sémantique de liaison pour différentes opérations.
Le mécanisme par lequel un objet dynamique implémente sa propre sémantique est défini par l’implémentation. Une interface donnée, encore une fois définie par l’implémentation, est mise en œuvre par des objets dynamiques pour signaler au runtime C# qu’ils possèdent une sémantique particulière. Ainsi, chaque fois que des opérations sur un objet dynamique sont liées dynamiquement, leur propre sémantique de liaison prend le relais, plutôt que celle de C# telle que spécifiée dans cette spécification.
Bien que le but de la liaison dynamique soit de permettre l’interopérabilité avec des objets dynamiques, C# autorise la liaison dynamique sur tous les objets, qu’ils soient dynamiques ou non. Cela permet une intégration plus fluide des objets dynamiques, car les résultats des opérations sur ceux-ci ne sont pas nécessairement des objets dynamiques, mais restent d’un type inconnu du programmeur à la compilation. De plus, la liaison dynamique peut aider à éliminer le code basé sur la réflexion, souvent source d’erreurs, même lorsque les objets impliqués ne sont pas dynamiques.
12.3.4 Types de sous-expressions
Lorsqu’une opération est liée statiquement, le type d’une sous-expression (par exemple, un récepteur, un argument, un index ou un opérande) est toujours considéré comme étant le type à la compilation de cette expression.
Lorsqu’une opération est liée dynamiquement, le type d’une sous-expression est déterminé de différentes manières selon le type à la compilation de la sous-expression :
- Une sous-expression de type à la compilation dynamic est considérée comme ayant le type de la valeur effective à laquelle l’expression évalue à l’exécution
- Une sous-expression dont le type à la compilation est un paramètre de type est considérée comme ayant le type auquel le paramètre de type est lié à l’exécution
- Sinon, la sous-expression est considérée comme ayant son type compile-time.
12.4 Opérateurs
12.4.1 Général
Les expressions sont construites à partir de d’opérandes et d’opérateurs. Les opérateurs d’une expression indiquent les opérations à appliquer aux opérandes.
Exemple : des exemples d’opérateurs incluent
+
,-
,*
,/
, etnew
. Les littéraux, les champs, les variables locales et les expressions sont des exemples d’opérandes. exemple final
Il existe trois types d’opérateurs :
- Opérateurs unaires. Les opérateurs unaires prennent un opérande et utilisent soit la notation préfixe (comme
–x
), soit la notation postfixe (commex++
). - Les opérateurs binaires. Les opérateurs binaires prennent deux opérandes et utilisent tous la notation infixe (comme
x + y
). - Opérateur ternaire. Un seul opérateur ternaire existe :
?:
. Il prend trois opérandes et utilise la notation infixe (c ? x : y
).
L’ordre d’évaluation des opérateurs dans une expression est déterminé par la priorité et l’association des opérateurs (§12.4.2).
Les opérateurs d'une expression sont évalués de gauche à droite.
Exemple : Dans
F(i) + G(i++) * H(i)
, la méthodeF
est appelée en utilisant l’ancienne valeur dei
, puis la méthodeG
est appelée avec l’ancienne valeur dei
et, enfin, la méthodeH
est appelée avec la nouvelle valeur de i. Ceci est distinct de la priorité des opérateurs et n'a aucun rapport avec celle-ci. exemple final
Certains opérateurs peuvent être surchargés. La surcharge d’opérateur (§12.4.3) autorise la spécification d’implémentations d’opérateurs définies par l’utilisateur pour les opérations où l’un des deux opérandes ou les deux sont d’un type classe ou struct défini par l’utilisateur.
12.4.2 Préséance et associativité des opérateurs
Quand une expression contient plusieurs opérateurs, la priorité des opérateurs contrôle l’ordre dans lequel ils sont évalués.
Remarque : par exemple, l’expression
x + y * z
est évaluée commex + (y * z)
parce que l’opérateur*
a une priorité supérieure à l’opérateur binaire+
. fin de la remarque
La priorité d’un opérateur est établie par la définition de sa production grammaticale associée.
Remarque : par exemple, une expression additive_expression est constituée d'une séquence de multiplicative_expression séparées par les opérateurs ou
+
,-
ce qui donne aux opérateurs+
et-
une priorité inférieure à celle des opérateurs*
,/
, et%
. fin de la remarque
Remarque : le tableau suivant résume tous les opérateurs par ordre de priorité décroissante :
Sous-clause Catégorie Opérateurs §12.8 Principale x.y
x?.y
f(x)
a[x]
a?[x]
x++
x--
x!
new
typeof
default
checked
unchecked
delegate
stackalloc
§12.9 Unaire +
-
!x
~
++x
--x
(T)x
await x
§12.10 Multiplicatif *
/
%
§12.10 Additive +
-
§12.11 Shift <<
>>
§12.12 Relations et test de type <
>
<=
>=
is
as
§12.12 Equality ==
!=
§12.13 ET logique &
§12.13 XOR logique ^
§12.13 OU logique \|
§12.14 AND conditionnel &&
§12.14 OR conditionnel \|\|
§12.15 et §12.16 Coalescence des nuls et expression jetée ??
throw x
§12.18 Conditionnelle ?:
§12.21 et §12.19 Affectation et expression lambda =
= ref
*=
/=
%=
+=
-=
<<=
>>=
&=
^=
\|=
=>
fin de la remarque
Lorsqu'un opérande se situe entre deux opérateurs de même priorité, l'associativité des opérateurs contrôle l'ordre dans lequel les opérations sont effectuées :
- À l’exception des opérateurs d’affectation et de l’opérateur de fusion null, tous les opérateurs binaires sont à associativité à gauche, ce qui signifie que les opérations sont effectuées de gauche à droite.
Exemple :
x + y + z
est évalué comme(x + y) + z
. exemple final - Les opérateurs d’affectation, l’opérateur de coalescence null et l’opérateur conditionnel (
?:
) sont associatifs à droite, ce qui signifie que les opérations sont effectuées de droite à gauche.Exemple :
x = y = z
est évalué commex = (y = z)
. exemple final
La priorité et l’associativité peuvent être contrôlées à l’aide de parenthèses.
Exemple :
x + y * z
multiplie d’abordy
parz
puis ajoute le résultat àx
, tandis que(x + y) * z
additionne d’abordx
ety
puis multiplie le résultat parz
. exemple final
12.4.3 Surcharge des opérateurs
Tous les opérateurs unaires et binaires ont des implémentations prédéfinies. De plus, des implémentations définies par l’utilisateur peuvent être introduites en incluant des déclarations d’opérateurs (§15.10) dans les classes et les structs. Les implémentations d’opérateurs définies par l’utilisateur priment toujours sur les implémentations prédéfinies : seules les implémentations prédéfinies seront prises en compte lorsqu’aucune implémentation d’opérateur définie par l’utilisateur applicable n’existe, comme décrit dans §12.4.4 et §12.4.5.
Les opérateurs unaires surchargés sont :
+ - !
(négation logique seulement) ~ ++ -- true false
Remarque: bien que
true
etfalse
ne soient pas utilisés explicitement dans les expressions (et ne figurent donc pas dans le tableau de priorité de §12.4.2), ils sont considérés comme des opérateurs car ils sont appelés dans plusieurs contextes d’expression : les expressions booléennes (§12.24) et les expressions impliquant les opérateurs conditionnels (§12.18) et conditionnels logiques (§12.14). fin de la remarque
Note : L'opérateur null-forgiving (postfix
!
, §12.8.9) n'est pas un opérateur surchargeable. fin de la remarque
Les opérateurs binaires surchargés sont :
+ - * / % & | ^ << >> == != > < <= >=
Seuls les opérateurs énumérés ci-dessus peuvent être surchargés. En particulier, il n'est pas possible de surcharger l'accès aux membres, l'invocation de méthodes ou les opérateurs =
, &&
, ||
, ??
, ?:
, =>
, checked
, unchecked
, new
, typeof
, default
, as
et is
.
Lorsqu’un opérateur binaire est surchargé, l’opérateur d’affectation composé correspondant, le cas échéant, est également surchargé implicitement.
Exemple : une surcharge de l’opérateur
*
est également une surcharge de l’opérateur*=
. Cela est décrit plus en détail dans §12.21. exemple final
L'opérateur d'affectation lui-même, (=)
, ne peut pas être surchargé. Une affectation effectue toujours un stockage simple d'une valeur dans une variable (§12.21.2).
Les opérations de fonte, telles que (T)x
, sont surchargées en fournissant des conversions définies par l'utilisateur (§10.5).
Remarque : les conversions définies par l’utilisateur n’affectent pas le comportement des opérateurs
is
ouas
. fin de la remarque
L'accès aux éléments, tel que a[x]
, n'est pas considéré comme un opérateur surchargeable. En revanche, l’indexation définie par l’utilisateur est prise en charge via les indexeurs (§15.9).
Dans les expressions, les opérateurs sont référencés à l’aide de la notation opérateur, et dans les déclarations, ils sont référencés à l’aide de la notation fonctionnelle. Le tableau suivant montre la relation entre la notation opérateur et la notation fonctionnelle pour les opérateurs unaires et binaires. Dans la première entrée, « op » désigne tout opérateur unaire préfixe surchargeable. Dans la deuxième entrée, « op » désigne les opérateurs unaires postfixe ++
et --
. Dans la troisième entrée, « op » désigne tout opérateur binaire surchargeable.
Remarque : pour voir un exemple de surcharge des opérateurs
++
et--
, voir §15.10.2. fin de la remarque
Notation des opérateurs | Notation fonctionnelle |
---|---|
«op» x |
operator «op»(x) |
x «op» |
operator «op»(x) |
x «op» y |
operator «op»(x, y) |
Les déclarations d’opérateurs définis par l’utilisateur exigent toujours qu’au moins l’un des paramètres soit du type classe ou struct qui contient la déclaration de l’opérateur.
Remarque : Il n’est donc pas possible pour un opérateur défini par l’utilisateur d’avoir la même signature qu’un opérateur prédéfini. fin de la remarque
Les déclarations d’opérateurs définis par l’utilisateur ne peuvent pas modifier la syntaxe, la priorité ou l’associativité d’un opérateur.
Exemple: l’opérateur
/
est toujours un opérateur binaire, a toujours le niveau de priorité spécifié dans §12.4.2, et est toujours associatif gauche. exemple final
Remarque : bien qu’il soit possible pour un opérateur défini par l’utilisateur d’effectuer tout calcul qu’il convient, les implémentations qui produisent des résultats autres que ceux attendus intuitivement sont fortement déconseillées. Par exemple, une implémentation de l’opérateur
==
doit comparer les deux opérandes pour l’égalité et retourner un résultatbool
approprié. fin de la remarque
Les descriptions des opérateurs individuels dans §12.9 à §12.21 précisent les implémentations prédéfinies des opérateurs ainsi que toute règle supplémentaire s’appliquant à chaque opérateur. Les descriptions utilisent les termes de résolution de surcharge d'opérateur unaire, de résolution de surcharge d'opérateur binaire, de promotion numérique et d'opérateur levé, dont les définitions se trouvent dans les sous-clauses suivantes.
12.4.4 Résolution de la surcharge de l'opérateur unaire
Une opération de la forme «op» x
ou x «op»
, où « op » est un opérateur unaire surchargeable et x
est une expression de type X
, est traitée comme suit :
- L’ensemble des opérateurs définis par l’utilisateur candidats fourni par
X
pour l’opérationoperator «op»(x)
est déterminé en utilisant les règles de §12.4.6. - Si l’ensemble des opérateurs définis par l’utilisateur candidats n’est pas vide, il devient alors l’ensemble des opérateurs candidats pour l’opération. Dans le cas contraire, les implémentations prédéfinies de
operator «op»
binaire, y compris leurs formes augmentées, deviennent l'ensemble des opérateurs candidats pour l'opération. Les implémentations prédéfinies d’un opérateur donné sont spécifiées dans la description de l’opérateur. Les opérateurs prédéfinis fournis par un type enum ou délégué ne sont inclus dans cet ensemble que lorsque le type à temps de liaison - ou le type sous-jacent s'il s'agit d'un type nullable - de l'un des opérandes est le type enum ou délégué. - Les règles de sélection de surcharge de §12.6.4 sont appliquées à l’ensemble des opérateurs candidats pour sélectionner le meilleur opérateur en fonction de la liste d’arguments
(x)
, et cet opérateur devient le résultat du processus de sélection de surcharge. Si la sélection de surcharge ne parvient pas à sélectionner un unique meilleur opérateur, une erreur de moment de liaison se produit.
12.4.5 Résolution de surcharge d'opérateur binaire
Une opération de la forme x «op» y
, où « op » est un opérateur binaire surchargeable, x
est une expression de type X
et y
est une expression de type Y
, est traitée comme suit :
- L’ensemble des opérateurs définis par l’utilisateur candidats fourni par
X
etY
pour l’opérationoperator «op»(x, y)
est déterminé. Cet ensemble consiste en l’union des opérateurs candidats fournis parX
et ceux fournis parY
, chacun déterminé en utilisant les règles de §12.4.6. Pour l’ensemble combiné, les candidats sont fusionnés comme suit :- Si
X
etY
sont convertibles par identité, ou siX
etY
proviennent d’un type de base commun, alors les opérateurs candidats communs n’apparaissent qu’une seule fois dans l’ensemble combiné. - S’il existe une conversion par identité entre
X
etY
, et si un opérateur«op»Y
fourni parY
a le même type de retour qu’un«op»X
fourni parX
et que les types des opérandes de«op»Y
sont convertibles par identité en ceux correspondants de«op»X
, alors seul«op»X
figure dans l’ensemble.
- Si
- Si l’ensemble des opérateurs définis par l’utilisateur candidats n’est pas vide, il devient alors l’ensemble des opérateurs candidats pour l’opération. Dans le cas contraire, les implémentations prédéfinies de
operator «op»
binaire, y compris leurs formes augmentées, deviennent l'ensemble des opérateurs candidats pour l'opération. Les implémentations prédéfinies d’un opérateur donné sont spécifiées dans la description de l’opérateur. Pour les opérateurs prédéfinis de type enum et délégué, les seuls opérateurs pris en compte sont ceux fournis par un type enum ou délégué qui est le type de temps de liaison de l'un des opérandes. - Les règles de sélection de surcharge de §12.6.4 sont appliquées à l’ensemble des opérateurs candidats pour sélectionner le meilleur opérateur en fonction de la liste d’arguments
(x, y)
, et cet opérateur devient le résultat du processus de sélection de surcharge. Si la sélection de surcharge ne parvient pas à sélectionner un unique meilleur opérateur, une erreur de moment de liaison se produit.
12.4.6 Candidats d’opérateurs définis par l’utilisateur
Étant donné un type T
et une opération operator «op»(A)
, où « op » est un opérateur surchargeable et A
est une liste d’arguments, l’ensemble des opérateurs définis par l’utilisateur candidats fourni par T
pour l’opérateur «op»(A)
est déterminé comme suit :
- Déterminez le type
T₀
. SiT
est un type valeur nullable,T₀
est son type sous-jacent ; sinon,T₀
est égal àT
. - Pour toutes les déclarations
operator «op»
dansT₀
et toutes les formes levées de tels opérateurs, si au moins un opérateur est applicable (§12.6.4.2) par rapport à la liste d’argumentsA
, alors l’ensemble des opérateurs candidats consiste en tous ces opérateurs applicables dansT₀
. - Sinon, si
T₀
estobject
, l’ensemble des opérateurs candidats est vide. - Sinon, l’ensemble des opérateurs candidats fourni par
T₀
est l’ensemble des opérateurs candidats fourni par la classe de base directe deT₀
, ou par la classe de base effective deT₀
siT₀
est un paramètre de type.
12.4.7 Promotions numériques
12.4.7.1 Général
Cette sous-clause est informative.
§12.4.7 et ses sous-clauses résument l’effet combiné de :
- les règles de conversions numériques implicites (§10.2.3) ;
- les règles pour une conversion meilleure (§12.6.4.7) ; et
- les opérateurs arithmétiques disponibles (§12.10), relationnels (§12.12) et logiques intégrales (§12.13.2).
La promotion numérique consiste à effectuer automatiquement certaines conversions implicites des opérandes des opérateurs numériques unaire et binaire prédéfinis. La promotion numérique n’est pas un mécanisme à part entière, mais plutôt le résultat de l’application de la résolution de surcharge aux opérateurs prédéfinis. La promotion numérique n’affecte spécifiquement pas l’évaluation des opérateurs définis par l’utilisateur, bien que ceux-ci puissent être implémentés pour produire des effets similaires.
Comme exemple de promotion numérique, considérons les implémentations prédéfinies de l'opérateur binaire *
:
int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);
Lorsque les règles de résolution de surcharge (§12.6.4) sont appliquées à cet ensemble d’opérateurs, cela a pour effet de sélectionner le premier opérateur pour lequel des conversions implicites existent à partir des types d’opérandes.
Exemple: Pour l’opération
b * s
, oùb
est unbyte
ets
est unshort
, la résolution de surcharge sélectionneoperator *(int, int)
comme étant le meilleur opérateur. Ainsi, cela a pour effet de convertirb
ets
enint
, et le type du résultat estint
. De même, pour l’opérationi * d
, oùi
est unint
etd
est undouble
, la résolutionoverload
sélectionneoperator *(double, double)
comme étant le meilleur opérateur. exemple final
Fin du texte informatif.
12.4.7.2 Promotions numériques unaires
Cette sous-clause est informative.
La promotion numérique unaire se produit pour les opérandes des opérateurs unaires prédéfinis +
, -
et ~
. La promotion numérique unaire consiste simplement à convertir des opérandes de type sbyte
, byte
, short
, ushort
ou char
en type int
. En outre, pour l'opérateur unaire -, la promotion numérique unaire convertit les opérandes de type uint
en opérandes de type long
.
Fin du texte informatif.
12.4.7.3 Promotions numériques binaires
Cette sous-clause est informative.
La promotion numérique binaire se produit pour les opérandes des opérateurs binaires prédéfinis +
, -
, *
, /
, %
, &
, |
, ^
, ==
, !=
, >
, <
, >=
et <=
. La promotion numérique binaire convertit implicitement les deux opérandes en un type commun qui, dans le cas des opérateurs non relationnels, devient également le type du résultat de l’opération. La promotion numérique binaire consiste à appliquer les règles suivantes, dans l’ordre indiqué :
- Si l’un des opérandes est de type
decimal
, l’autre opérande est convertie en typedecimal
, ou une erreur de liaison se produit si l’autre opérande est de typefloat
oudouble
. - Sinon, si l’un des opérandes est de type
double
, l’autre opérande est convertie en typedouble
. - Sinon, si l’un des opérandes est de type
float
, l’autre opérande est convertie en typefloat
. - Sinon, si l’un des opérandes est de type
ulong
, l’autre opérande est convertie en typeulong
, ou une erreur de liaison se produit si l’autre opérande est de typetype sbyte
,short
,int
oulong
. - Sinon, si l’un des opérandes est de type
long
, l’autre opérande est convertie en typelong
. - Sinon, si l’un des opérandes est de type
uint
et l’autre de typesbyte
,short
ouint
, les deux opérandes sont converties en typelong
. - Sinon, si l’un des opérandes est de type
uint
, l’autre opérande est convertie en typeuint
. - Sinon, les deux opérandes sont convertis au type
int
.
Remarque: La première règle interdit toute opération mélangeant le type
decimal
avec les typesdouble
etfloat
. Cette règle découle du fait qu’il n’existe aucune conversion implicite entre le typedecimal
et les typesdouble
etfloat
. fin de la remarque
Remarque: notez également qu’il n’est pas possible qu’un opérande soit de type
ulong
lorsque l’autre opérande est d’un type entier signé. Parce qu’aucun type entier ne peut représenter l’intervalle complet deulong
ainsi que les types entiers signés. fin de la remarque
Dans chacun des cas décrits ci-dessus, une expression de transtypage peut être utilisée pour convertir explicitement un opérande en un type compatible avec l’autre opérande.
Exemple: Dans le code suivant
decimal AddPercent(decimal x, double percent) => x * (1.0 + percent / 100.0);
une erreur de liaison se produit parce qu’un
decimal
ne peut pas être multiplié par undouble
. L’erreur est résolue en convertissant explicitement le second opérande endecimal
, comme suit:decimal AddPercent(decimal x, double percent) => x * (decimal)(1.0 + percent / 100.0);
exemple final
Fin du texte informatif.
12.4.8 Opérateurs étendus
Les opérateurs décalés permettent aux opérateurs prédéfinis et définis par l'utilisateur qui opèrent sur des types de valeurs non nullables d'être également utilisés avec des formes nullables de ces types. Les opérateurs liftés sont construits à partir d'opérateurs prédéfinis et d'opérateurs définis par l'utilisateur qui satisfont à certaines exigences, comme décrit ci-après :
- Pour les opérateurs unaires
+
,++
,-
,--
,!
(négation logique) et~
, une forme relevée de l’opérateur existe si les types de l’opérande et du résultat sont tous deux des types de valeur non-nullables. La forme augmentée est construite en ajoutant un seul modificateur?
aux types de l'opérande et du résultat. L'opérateur lifté produit une valeurnull
si l'opérande estnull
. Dans le cas contraire, il décompresse l'opérande, applique l'opérateur sous-jacent et recouvre le résultat. - Pour les opérateurs binaires
+
,-
,*
,/
,%
,&
,|
,^
,<<
et>>
, une forme relevée (lifted) de l’opérateur existe si les types des opérandes et du résultat sont tous des types de valeur non-nullables. La forme élevée est construite en ajoutant un seul modificateur?
à chaque type d'opérande et de résultat. L’opérateur levé produit une valeurnull
si un ou les deux opérandes sontnull
, à l'exception des opérateurs&
et|
du typebool?
, comme décrit dans §12.13.5. Si les deux opérandes sont non, l'opérateur lifté ouvre l'opérande, applique l'opérateur sous-jacent et enveloppe le résultat. - Pour les opérateurs d’égalité
==
et!=
, une forme relevée de l’opérateur existe si les types des opérandes sont tous deux des types de valeur non-nullables et si le type du résultat estbool
. La forme surélevée est construite en ajoutant un modificateur?
unique à chaque type d’opérande. L’opérateur relevé (lifted) considère que deux valeurs de typenull
sont égales, et qu’une valeur de typenull
est différente de toute valeur de type nonnull
. Si les deux opérandes sont non-null
, l'opérateur lifté déballe les opérandes et applique l'opérateur sous-jacent pour produire le résultatbool
. - Pour les opérateurs relationnels
<
,>
,<=
, et>=
, une forme relevée de l’opérateur existe si les types des opérandes sont tous deux des types de valeur non-nullables et si le type du résultat estbool
. La forme surélevée est construite en ajoutant un modificateur?
unique à chaque type d’opérande. L'opérateur élevé produit la valeurfalse
si l'un des opérandes ou les deux sontnull
. Dans le cas contraire, il décompresse les opérandes et applique l'opérateur sous-jacent pour produire le résultatbool
.
12.5 Recherche de membre
12.5.1 Général
Une recherche de membre est le processus par lequel la signification d’un nom dans le contexte d’un type est déterminée. Une consultation de membre peut se produire dans le cadre de l'évaluation d'un simple_name (§12.8.4) ou d'un member_access (§12.8.7) dans une expression. Si le simple_name ou le member_access apparaît comme primary_expression d'une invocation_expression (§12.8.10.2), on dit que le membre est invoqué.
Si un membre est une méthode ou un événement, ou s’il s’agit d’une constante, d’un champ ou d’une propriété d’un type délégué (§20) ou du type dynamic
(§8.2.4), alors on dit que le membre est appelable.
La recherche de membre prend en compte non seulement le nom d’un membre, mais aussi le nombre de paramètres de type qu’il possède et si le membre est accessible. Pour la recherche de membre, les méthodes génériques et les types génériques imbriqués possèdent le nombre de paramètres de type indiqué dans leurs déclarations respectives et tous les autres membres n’ont aucun paramètre de type.
Une recherche de membre d'un nom N
avec des arguments de type K
dans un type T
est traitée comme suit :
- Tout d’abord, un ensemble de membres accessibles nommés
N
est déterminé :- Si
T
est un paramètre de type, alors l’ensemble est l’union des ensembles de membres accessibles nommésN
dans chacun des types spécifiés comme contrainte primaire ou contrainte secondaire (§15.2.5) pourT
, ainsi que l’ensemble des membres accessibles nommésN
dansobject
. - Sinon, l’ensemble se compose de tous les membres accessibles (§7.5) nommés
N
dansT
, y compris les membres hérités et les membres accessibles nommésN
dansobject
. SiT
est un type construit, l’ensemble des membres est obtenu en substituant les arguments de type comme décrit dans §15.3.3. Les membres comportant un modificateuroverride
sont exclus de l’ensemble.
- Si
- Ensuite, si
K
est égal à zéro, tous les types imbriqués dont les déclarations incluent des paramètres de type sont supprimés. SiK
n’est pas égal à zéro, tous les membres avec un nombre différent de paramètres de type sont supprimés. LorsqueK
est égal à zéro, les méthodes possédant des paramètres de type ne sont pas supprimées, puisque le processus d’inférence de type (§12.6.3) pourrait être en mesure d’inférer les arguments de type. - Ensuite, si le membre est appelé, tous les membres non appelables sont supprimés de l’ensemble.
- Ensuite, les membres masqués par d’autres membres sont supprimés de l’ensemble. Pour chaque membre
S.M
dans l’ensemble, oùS
est le type dans lequel le membreM
est déclaré, les règles suivantes sont appliquées :- Si
M
est une constante, un champ, une propriété, un événement ou un membre d’énumération, alors tous les membres déclarés dans un type de base deS
sont retirés de l’ensemble. - Si
M
est une déclaration de type, alors tous les non-types déclarés dans un type de base deS
sont retirés de l’ensemble, et toutes les déclarations de type comportant le même nombre de paramètres de type queM
déclarées dans un type de base deS
sont retirées de l’ensemble. - Si
M
est une méthode, alors tous les membres n’étant pas une méthode déclarés dans un type de base deS
sont supprimés de l’ensemble.
- Si
- Ensuite, les membres d’interface masqués par des membres de classe sont supprimés de l’ensemble. Cette étape n’a d’effet que si
T
est un paramètre de type et queT
possède à la fois une classe de base effective autre queobject
et un ensemble d’interfaces effectif non vide (§15.2.5). Pour chaque membreS.M
dans l’ensemble, oùS
est le type dans lequel le membreM
est déclaré, les règles suivantes sont appliquées siS
est une déclaration de classe autre queobject
:- Si
M
est une constante, un champ, une propriété, un événement, un membre d’énumération ou une déclaration de type, alors tous les membres déclarés dans une déclaration d’interface sont retirés de l’ensemble. - Si
M
est une méthode, alors tous les membres n’étant pas une méthode déclarés dans une déclaration d’interface sont retirés de l’ensemble, et toutes les méthodes ayant la même signature queM
déclarées dans une déclaration d’interface sont supprimées de l’ensemble.
- Si
- Enfin, après avoir retiré les membres masqués, le résultat de la recherche est déterminé :
- Si l’ensemble se compose d’un seul membre qui n’est pas une méthode, alors ce membre est le résultat de la recherche.
- Sinon, si l'ensemble contient uniquement des méthodes, alors ce groupe de méthodes constitue le résultat de la recherche.
- Dans le cas contraire, la recherche est ambiguë et une erreur de liaison se produit.
Pour les recherches de membres dans les types autres que les paramètres de type et les interfaces, et les recherches de membres dans les interfaces strictement à héritage simple (chaque interface de la chaîne d’héritage ayant exactement zéro ou une interface de base directe), l’effet des règles de recherche est simplement que les membres dérivés masquent les membres de base ayant le même nom ou la même signature. De telles consultations d'héritage unique ne sont jamais ambiguës. Les ambiguïtés pouvant éventuellement survenir lors des recherches de membres dans des interfaces d’héritage multiple sont décrites dans §18.4.6.
Remarque: cette phase ne prend en compte qu’un seul type d’ambiguïté. Si la recherche de membre aboutit à un groupe de méthodes, les futures utilisations de ce groupe pourront échouer en raison d’une ambiguïté, par exemple comme décrit dans §12.6.4.1 et §12.6.6.2. fin de la remarque
12.5.2 Types de base
Pour la recherche de membre, un type T
est considéré comme ayant les types de base suivants :
- Si
T
estobject
oudynamic
, alorsT
n’a pas de type de base. - Si
T
est un enum_type, les types de base deT
sont les types de classeSystem.Enum
,System.ValueType
etobject
. - Si
T
est un struct_type, les types de base deT
sont les types de classeSystem.ValueType
etobject
.Remarque: un nullable_value_type est un struct_type (§8.3.1). fin de la remarque
- Si
T
est un class_type, les types de base deT
sont les classes de base deT
, y compris le type de classeobject
. - Si
T
est un interface_type, les types de base deT
sont les interfaces de base deT
et le type de classeobject
. - Si
T
est un array_type, les types de base deT
sont les types de classeSystem.Array
etobject
. - Si
T
est un delegate_type, les types de base deT
sont les types de classeSystem.Delegate
etobject
.
12.6 Membres de fonction
12.6.1 Général
Les membres de fonction sont des membres qui contiennent des instructions exécutables. Les membres de fonction sont toujours des membres de types et ne peuvent pas être des membres d’espaces de noms. C# définit les catégories suivantes de membres de fonction :
- Méthodes
- Propriétés
- Événements
- Indexeurs
- Opérateurs définis par l’utilisateur
- Constructeurs d'instances
- Constructeurs statiques
- Finaliseurs
À l’exception des finaliseurs et des constructeurs statiques (qui ne peuvent pas être appelés explicitement), les instructions contenues dans les membres de fonction sont exécutées par le biais d’appels de ces membres. La syntaxe efficace pour appeler un membre de fonction dépend de la catégorie particulière du membre de fonction.
La liste d’arguments (§12.6.2) d’un appel de membre de fonction fournit des valeurs réelles ou des références de variables pour les paramètres du membre de fonction.
Les appelss de méthodes génériques peuvent utiliser l’inférence de type pour déterminer l’ensemble des arguments de type à passer à la méthode. Ce processus est décrit dans §12.6.3.
Les invocations de méthodes, d'indexeurs, d'opérateurs et de constructeurs d'instances utilisent la résolution de surcharge pour déterminer quel membre de fonction d'un ensemble de candidats appeler. Ce processus est décrit dans §12.6.4.
Une fois qu'un membre de fonction particulier a été identifié au moment de la liaison, éventuellement par le biais d'une résolution de surcharge, le processus d'exécution réel de l'invocation du membre de fonction est décrit au §12.6.6.
Remarque: le tableau suivant résume le traitement qui a lieu dans les constructions impliquant les six catégories de membres de fonction pouvant être appelés explicitement. Dans le tableau,
e
,x
,y
, etvalue
indiquent des expressions classées comme des variables ou des valeurs,T
indique une expression classée comme un type,F
est le nom simple d’une méthode, etP
est le nom simple d’une propriété.
Construction Exemple Description Appel de méthode F(x, y)
La résolution de surcharge est appliquée pour sélectionner la meilleure méthode F
dans la classe ou la structure englobante. La méthode est appelée avec la liste d’arguments(x, y)
. Si la méthode n’est passtatic
, l’expression d’instance estthis
.T.F(x, y)
La résolution de surcharge est appliquée pour sélectionner la meilleure méthode F
dans la classe ou la structureT
. Une erreur de liaison se produit si la méthode n’est passtatic
. La méthode est appelée avec la liste d’arguments(x, y)
.e.F(x, y)
La résolution de surcharge est appliquée pour sélectionner la meilleure méthode F
dans la classe, la structure ou l’interface donnée par le type dee
. Une erreur de liaison se produit si la méthode eststatic
. La méthode est appelée avec l’expression d’instancee
et la liste d’arguments(x, y)
.Accès à la propriété P
L'accesseur get de la propriété P
de la classe ou de la structure qui la contient est invoqué. Une erreur de compilation se produit siP
est en écriture seule. SiP
n’est passtatic
, l’expression d’instance estthis
.P = value
L’accesseur défini de la propriété P
dans la classe ou la structure conteneur est appelé avec la liste d’arguments(value)
. Une erreur de compilation se produit siP
est en lecture seule. SiP
n’est passtatic
, l’expression d’instance estthis
.T.P
L'accesseur de la propriété P
de la classe ou de la structureT
est invoqué. Une erreur de compilation se produit siP
n’est passtatic
ou siP
est en écriture seule.T.P = value
L’accesseur set de la propriété P
dans la classe ou la structureT
est appelé avec la liste d’arguments(value)
. Une erreur de compilation se produit siP
n’est passtatic
ou siP
est en lecture seule.e.P
L'accesseur de la propriété P
de la classe, de la structure ou de l'interface donnée par le type deE
est invoqué avec l'expression d'instancee
. Une erreur de liaison se produit siP
eststatic
ou siP
est en écriture seule.e.P = value
L’accesseur set de la propriété P
dans la classe, la structure ou l’interface, déterminée par le type deE
, est appelé avec l’expression d’instancee
et la liste d’arguments(value)
. Une erreur de liaison se produit siP
eststatic
ou siP
est en lecture seule.Accès aux événements E += value
L'accesseur d'ajout de l'événement E
de la classe ou de la structure qui le contient est invoqué. SiE
n’est passtatic
, l’expression d’instance estthis
.E -= value
L'accesseur de suppression de l'événement E
de la classe ou de la structure contenante est invoqué. SiE
n’est passtatic
, l’expression d’instance estthis
.T.E += value
L'accesseur d'ajout de l'événement E
dans la classe ou la structureT
est invoqué. Une erreur de liaison se produit siE
n’est passtatic
.T.E -= value
L'accesseur de suppression de l'événement E
dans la classe ou la structureT
est invoqué. Une erreur de liaison se produit siE
n’est passtatic
.e.E += value
L'accesseur d'ajout de l'événement E
dans la classe, la structure ou l'interface donnée par le typeE
est invoqué avec l'expression d'instancee
. Une erreur de liaison se produit siE
eststatic
.e.E -= value
L'accesseur de suppression de l'événement E
dans la classe, la structure ou l'interface donnée par le typeE
est invoqué avec l'expression d'instancee
. Une erreur de liaison se produit siE
eststatic
.Accès à l’indexeur e[x, y]
La résolution de surcharge est appliquée pour sélectionner le meilleur indexeur dans la classe, la structure ou l'interface donnée par le type e
. L’accesseur get de l’indexeur est appelé avec l’expression d’instancee
et la liste d’arguments(x, y)
. Une erreur de liaison se produit si l'indexeur est en écriture seule.e[x, y] = value
La résolution de surcharge est appliquée pour sélectionner le meilleur indexeur dans la classe, la structure ou l'interface donnée par le type e
. L’accesseur set de l’indexeur est appelé avec l’expression d’instancee
et la liste d’arguments(x, y, value)
. Une erreur de liaison se produit si l’indexeur est en lecture seule.Invocation d'un opérateur -x
La résolution de surcharge est appliquée pour sélectionner le meilleur opérateur unaire dans la classe ou la structure donnée par le type de x
. L’opérateur sélectionné est appelé avec la liste d’arguments(x)
.x + y
La résolution de surcharge est appliquée pour sélectionner le meilleur opérateur binaire dans les classes ou structures données par les types de x
ety
. L’opérateur sélectionné est appelé avec la liste d’arguments(x, y)
.Invocation du constructeur d'instance new T(x, y)
La résolution de surcharge est appliquée pour sélectionner le meilleur constructeur d’instance dans la classe ou la structure T
. Le constructeur d’instance est appelé avec la liste d’arguments(x, y)
.fin de la remarque
12.6.2 Listes d’arguments
12.6.2.1 Général
Chaque appel de membre de fonction ou de délégué inclut une liste d’arguments, qui fournit des valeurs réelles ou des références de variables pour les paramètres du membre de fonction. La syntaxe pour spécifier la liste d’arguments d’un appel de membre de fonction dépend de la catégorie du membre de fonction :
- Par exemple, pour les constructeurs, les méthodes, les indexeurs et les délégués, les arguments sont spécifiés comme une argument_list, comme décrit ci-dessous. Pour les indexeurs, lors de l'invocation de l'accesseur set, la liste d'arguments inclut en plus l'expression spécifiée comme opérande droit de l'opérateur d'affectation.
Remarque: cet argument supplémentaire n’est pas utilisé pour la résolution de surcharge, mais uniquement lors de l’invocation de l’accesseur set. fin de la remarque
- Pour les propriétés, la liste d’arguments est vide lors de l’appel de l’accesseur get, et se compose de l’expression spécifiée comme opérande droit de l’opérateur d’affectation lors de l’appel de l’accesseur set.
- Pour les événements, la liste d’arguments se compose de l’expression spécifiée en tant qu’opérande droit de l’opérateur
+=
ou-=
. - Pour les opérateurs définis par l’utilisateur, la liste d’arguments se compose de l’opérande unique de l’opérateur unaire ou des deux opérandes de l’opérateur binaire.
Les arguments des propriétés (§15.7) et des événements (§15.8) sont toujours passés en tant que paramètres de valeur (§15.6.2.2). Les arguments des opérateurs définis par l’utilisateur (§15.10) sont toujours passés en tant que paramètres de valeur (§15.6.2.2) ou paramètres d’entrée (§9.2.8). Les arguments des indexeurs (§15.9) sont toujours passés en tant que paramètres de valeur (§15.6.2.2), paramètres d’entrée (§9.2.8) ou tableaux de paramètres (§15.6.2.4). Les paramètres de sortie et de référence ne sont pas pris en charge pour ces catégories de membres de fonction.
Les arguments d’un constructeur d’instance, d’une méthode, d’un indexeur ou d’un appel de délégué sont spécifiés comme une argument_list :
argument_list
: argument (',' argument)*
;
argument
: argument_name? argument_value
;
argument_name
: identifier ':'
;
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' variable_reference
;
Une argument_list se compose d’un ou plusieurs arguments, séparés par des virgules. Chaque argument se compose d’un argument_name optionnel suivi d’une argument_value. Un argument avec un argument_name est appelé named argument, tandis qu’un argument sans argument_name est un positional argument.
argument_value peut prendre l’une des formes suivantes :
- Une expression, indiquant que l’argument est passé en tant que paramètre de valeur ou est transformé en un paramètre d’entrée puis passé comme tel, comme déterminé par (§12.6.4.2 et décrit dans §12.6.2.3.
- Le mot-clé
in
suivi d’une variable_reference (§9.5), indiquant que l’argument est passé en tant que paramètre d’entrée (§15.6.2.3.2). Une variable doit être définitivement assignée (§9.4) avant de pouvoir être passée en tant que paramètre d’entrée. - Le mot-clé
ref
suivi d’une variable_reference (§9.5), indiquant que l’argument est passé en tant que paramètre de référence (§15.6.2.3.3). Une variable doit être définitivement assignée (§9.4) avant de pouvoir être passée en tant que paramètre de référence. - Le mot-clé
out
suivi d’une variable_reference (§9.5), indiquant que l’argument est passé en tant que paramètre de sortie (§15.6.2.3.4). Une variable est considérée comme définitivement assignée (§9.4) après un appel de membre de fonction dans lequel la variable est passée en tant que paramètre de sortie.
La forme détermine respectivement le mode de passage de paramètre de l’argument : valeur, entrée, référence ou sortie. Toutefois, comme mentionné ci-dessus, un argument avec le mode de passage de valeur peut être transformé en un argument avec le mode de passage d’entrée.
Passer un champ volatile (§15.5.4) en tant que paramètre d’entrée, de sortie ou de référence provoque un avertissement, car le champ ne peut pas être considéré comme volatile par la méthode appelée.
12.6.2.2 Paramètres correspondants
Pour chaque argument dans une liste d’arguments, il doit exister un paramètre correspondant dans le membre de fonction ou le délégué appelé.
La liste des paramètres utilisée dans ce qui suit est déterminée comme suit :
- Pour les méthodes virtuelles et les indexeurs définis dans les classes, la liste des paramètres est prise à partir de la première déclaration ou redéfinition du membre de fonction trouvée en commençant par le type statique du récepteur et en recherchant dans ses classes de base.
- Pour les méthodes partielles, la liste des paramètres de la déclaration de la méthode partielle définie est utilisée.
- Pour tous les autres membres de fonction et délégués, il n’existe qu’une seule liste de paramètres : celle qui est utilisée.
La position d’un argument ou d’un paramètre est définie comme le nombre d’arguments ou de paramètres le précédant dans la liste d’arguments ou la liste des paramètres.
Les paramètres correspondants pour les arguments de membre de fonction sont établis comme suit :
- Arguments dans la liste d'arguments des constructeurs, méthodes, indexeurs et délégués d'instance :
- Un argument positionnel pour lequel un paramètre se trouve à la même position dans la liste des paramètres correspond à ce paramètre, sauf si le paramètre est un tableau de paramètres et que le membre de fonction est appelé sous sa forme étendue.
- Un argument positionnel d’un membre de fonction avec un tableau de paramètres appelé sous sa forme étendue, qui se situe à la position du tableau de paramètres ou après celle-ci dans la liste des paramètres, correspond à un élément du tableau de paramètres.
- Un argument nommé correspond au paramètre du même nom dans la liste des paramètres.
- Pour les indexeurs, lors de l'invocation de l'accesseur set, l'expression spécifiée en tant qu'opérande droit de l'opérateur d'affectation correspond au paramètre
value
implicite de la déclaration de l'accesseur set.
- Pour les propriétés, il n'y a pas d'arguments lors de l'appel de l'accesseur get. Lorsqu'on invoque l'accesseur set, l'expression spécifiée comme opérande droit de l'opérateur d'affectation correspond au paramètre implicite value de la déclaration de l'accesseur set.
- Pour les opérateurs unaires définis par l’utilisateur (y compris les conversions), l’opérande unique correspond au paramètre unique de la déclaration de l’opérateur.
- Pour les opérateurs binaires définis par l’utilisateur, l’opérande gauche correspond au premier paramètre, et l’opérande droite correspond au second paramètre de la déclaration de l’opérateur.
- Un argument non nommé ne correspond à aucun paramètre lorsqu’il suit un argument nommé hors de position ou un argument nommé qui correspond à un tableau de paramètres.
Remarque : cela empêche que
void M(bool a = true, bool b = true, bool c = true);
soit appelé parM(c: false, valueB);
. Le premier argument est utilisé hors de position (l’argument est utilisé en première position, mais le paramètre nomméc
est en troisième position), de sorte que les arguments suivants doivent être nommés. En d'autres termes, les arguments nommés non consécutifs ne sont autorisés que lorsque le nom et la position permettent de trouver le même paramètre correspondant. fin de la remarque
12.6.2.3 Évaluation à l’exécution des listes d’arguments
Lors du traitement à l’exécution d’un appel de membre de fonction (§12.6.6), les expressions ou références de variables d’une liste d’arguments sont évaluées dans l’ordre, de gauche à droite, comme suit :
Pour un argument par valeur, si le mode de passage du paramètre est valeur
l'expression d'argument est évaluée et une conversion implicite (§10.2) au type de paramètre correspondant est effectuée. La valeur résultante devient la valeur initiale du paramètre de valeur lors de l’appel du membre de fonction.
Dans le cas contraire, le mode de passage du paramètre est l'entrée. Si l’argument est une référence à une variable et qu’il existe une conversion d’identité (§10.2.2) entre le type de l’argument et le type du paramètre, l’emplacement de stockage résultant devient l’emplacement de stockage représenté par le paramètre dans l’appel du membre de fonction. Sinon, un emplacement de stockage est créé avec le même type que celui du paramètre correspondant. L’expression d’argument est évaluée et une conversion implicite (§10.2) au type de paramètre correspondant est effectuée. La valeur résultante est stockée dans cet emplacement de stockage. Cet emplacement de stockage est représenté par le paramètre d’entrée dans l’appel du membre de fonction.
Exemple : étant donné les déclarations suivantes et les appels de méthode :
static void M1(in int p1) { ... } int i = 10; M1(i); // i is passed as an input argument M1(i + 5); // transformed to a temporary input argument
Dans l’appel de méthode
M1(i)
,i
lui-même est passé en tant qu’argument d’entrée, car il est classé comme une variable et possède le même typeint
que le paramètre d’entrée. Dans l’appel de méthodeM1(i + 5)
, une variableint
sans nom est créée, initialisée avec la valeur de l’argument, puis passée en tant qu’argument d’entrée. Veuillez consulter §12.6.4.2 et §12.6.4.4.exemple final
Pour un argument d’entrée, de sortie ou de référence, la référence à la variable est évaluée et l’emplacement de stockage résultant devient l’emplacement de stockage représenté par le paramètre dans l’appel du membre de fonction. Pour un argument d’entrée ou de référence, la variable doit être assignée au moment de l’appel de la méthode. Si la référence à la variable est fournie en tant qu’argument de sortie, ou est un élément de tableau d’un reference_type, un contrôle à l’exécution est effectué afin de s’assurer que le type d’élément du tableau est identique au type du paramètre. Si cette vérification échoue, un
System.ArrayTypeMismatchException
est lancé.
Remarque : ce contrôle à l’exécution est requis en raison de la covariance des tableaux (§17.6). fin de la remarque
Exemple: Dans le code suivant
class Test { static void F(ref object x) {...} static void Main() { object[] a = new object[10]; object[] b = new string[10]; F(ref a[0]); // Ok F(ref b[1]); // ArrayTypeMismatchException } }
le deuxième appel de
F
entraîne le déclenchement d’uneSystem.ArrayTypeMismatchException
parce que le type effectif deb
eststring
et nonobject
.exemple final
Les méthodes, indexeurs et constructeurs d’instance peuvent déclarer leur paramètre le plus à droite comme un tableau de paramètres (§15.6.2.4). Ces membres de fonction sont appelés soit sous leur forme normale, soit sous leur forme étendue, selon celle qui est applicable (§12.6.4.2) :
- Lorsqu’un membre de fonction avec un tableau de paramètres est appelé sous sa forme normale, l’argument fourni pour le tableau de paramètres doit être une seule expression implicitement convertible (§10.2) au type de tableau de paramètres. Dans ce cas, le tableau de paramètres fonctionne exactement comme un paramètre de valeur.
- Lorsqu’un membre de fonction avec un tableau de paramètres est appelé sous sa forme étendue, l’appel doit spécifier zéro ou plusieurs arguments positionnels pour le tableau de paramètres, chaque argument étant une expression implicitement convertible (§10.2) au type d’élément du tableau de paramètres. Dans ce cas, l’appel crée une instance du type de tableau de paramètres d’une longueur correspondant au nombre d’arguments, initialise les éléments de cette instance avec les valeurs des arguments fournis, et utilise cette instance de tableau nouvellement créée comme argument effectif.
Les expressions d’une liste d’arguments sont toujours évaluées dans l’ordre textuel.
Exemple : Ainsi, l'exemple
class Test { static void F(int x, int y = -1, int z = -2) => Console.WriteLine($"x = {x}, y = {y}, z = {z}"); static void Main() { int i = 0; F(i++, i++, i++); F(z: i++, x: i++); } }
génère la sortie
x = 0, y = 1, z = 2 x = 4, y = -1, z = 3
exemple final
Lorsqu’un membre de fonction avec un tableau de paramètres est appelé sous sa forme étendue avec au moins un argument étendu, l’appel est traité comme si une expression de création de tableau avec un initialiseur de tableau (§12.8.17.5) avait été insérée autour des arguments étendus. Un tableau vide est passé lorsqu’il n’y a aucun argument pour le tableau de paramètres ; il n’est pas précisé si la référence transmise correspond à un nouveau tableau vide alloué ou à un tableau vide existant.
Exemple : Étant donné la déclaration
void F(int x, int y, params object[] args);
les invocations suivantes de la version étendue de la méthode
F(10, 20, 30, 40); F(10, 20, 1, "hello", 3.0);
correspond exactement à
F(10, 20, new object[] { 30, 40 }); F(10, 20, new object[] { 1, "hello", 3.0 });
exemple final
Lorsque des arguments sont omis dans un membre de fonction avec des paramètres optionnels correspondants, les arguments par défaut de la déclaration du membre de fonction sont transmis implicitement. (Cela peut impliquer la création d’un emplacement de stockage, comme décrit ci-dessus.)
Remarque : comme ceux-ci sont toujours constants, leur évaluation n’aura pas d’impact sur l’évaluation des autres arguments. fin de la remarque
12.6.3 Inférence de type
12.6.3.1 Général
Lorsqu’une méthode générique est appelée sans spécifier d’arguments de type, un processus d’inférence de type tente d’inférer des arguments de type pour l’appel. La présence de l’inférence de type permet d’utiliser une syntaxe plus pratique pour appeler une méthode générique, et permet au programmeur d’éviter de spécifier des informations de type redondantes.
Exemple :
class Chooser { static Random rand = new Random(); public static T Choose<T>(T first, T second) => rand.Next(2) == 0 ? first : second; } class A { static void M() { int i = Chooser.Choose(5, 213); // Calls Choose<int> string s = Chooser.Choose("apple", "banana"); // Calls Choose<string> } }
Grâce à l’inférence de type, les arguments de type
int
etstring
sont déterminés à partir des arguments de la méthode.exemple final
L'inférence de type se produit dans le cadre du traitement du temps de liaison d'une invocation de méthode (§12.8.10.2) et a lieu avant l'étape de résolution de surcharge de l'invocation. Lorsqu’un groupe de méthodes particulier est spécifié dans un appel de méthode, et qu’aucun argument de type n’est spécifié dans l’appel de la méthode, l’inférence de type est appliquée à chaque méthode générique du groupe. Si l’inférence de type réussit, alors les arguments de type inférés sont utilisés pour déterminer les types d’arguments pour la résolution de surcharges ultérieures. Si la résolution de surcharges choisit une méthode générique comme celle à appeler, alors les arguments de type inférés sont utilisés comme arguments de type pour l’appel. Si l’inférence de type pour une méthode particulière échoue, cette méthode ne participe pas à la résolution de surcharges. L'échec de l'inférence de type, en soi, ne provoque pas d'erreur de liaison. Cependant, cela conduit souvent à une erreur de liaison lorsque la résolution de surcharge ne parvient pas à trouver de méthodes applicables.
Si chaque argument fourni ne correspond pas à exactement un paramètre de la méthode (§12.6.2.2), ou s’il existe un paramètre non optionnel sans argument correspondant, alors l’inférence échoue immédiatement. Sinon, supposons que la méthode générique ait la signature suivante :
Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)
Avec un appel de méthode de la forme M(E₁ ...Eₓ)
, la tâche de l’inférence de type est de trouver des arguments de type uniques S₁...Sᵥ
pour chacun des paramètres de type X₁...Xᵥ
afin que l’appel M<S₁...Sᵥ>(E₁...Eₓ)
devienne valide.
Le processus d’inférence de type est décrit ci-dessous sous forme d’algorithme. Un compilateur conforme peut être implémenté en utilisant une approche alternative, à condition d’aboutir au même résultat dans tous les cas.
Au cours du processus d’inférence, chaque paramètre de type Xᵢ
est fixé à un type particulier Sᵢ
ou non fixé avec un ensemble de limites associées . Chacune des limites est de type T
. Initialement, chaque variable de type Xᵢ
est non fixée avec un ensemble de bornes vide.
L’inférence de type se déroule par étapes. Chaque étape tentera d’inférer des arguments de type pour davantage de variables de type en se basant sur les résultats de l’étape précédente. La première étape effectue quelques inférences initiales de bornes, tandis que la deuxième étape fixe les variables de type à des types spécifiques et infère d’autres bornes. La deuxième étape peut devoir être répétée plusieurs fois.
Remarque: l’inférence de type est également utilisée dans d’autres contextes, notamment pour la conversion de groupes de méthodes (§12.6.3.14) et pour trouver le meilleur type commun d’un ensemble d’expressions (§12.6.3.15). fin de la remarque
12.6.3.2 La première phase
Pour chacun des arguments de la méthode Eᵢ
:
- Si
Eᵢ
est une fonction anonyme, une inférence explicite du type du paramètre (§12.6.3.8) est faite deEᵢ
àTᵢ
- Sinon, si
Eᵢ
a un typeU
et que le paramètre correspondant est un paramètre de valeur (§15.6.2.2), alors une inférence par borne inférieure (§12.6.3.10) est effectuée deU
àTᵢ
. - Sinon, si
Eᵢ
a un typeU
et que le paramètre correspondant est un paramètre de référence (§15.6.2.3.3), ou un paramètre de sortie (§15.6.2.3.4), alors une inférence exacte (§12.6.3.9) est effectuée deU
àTᵢ
. - Sinon, si
Eᵢ
a un typeU
et si le paramètre correspondant est un paramètre d’entrée (§15.6.2.3.2) etEᵢ
est un argument d’entrée, un d’inférence exacte (§12.6.3.9) est effectuée deU
àTᵢ
. - Sinon, si
Eᵢ
a un typeU
et que le paramètre correspondant est un paramètre d’entrée (§15.6.2.3.2), alors une inférence par borne inférieure (§12.6.3.10) est effectuée deU
àTᵢ
. - Sinon, aucune inférence n’est effectuée pour cet argument.
12.6.3.3 La deuxième phase
La deuxième phase se déroule comme suit :
- Toutes les variables de type
Xᵢ
non fixées qui ne dépendent pas (§12.6.3.6) d'unXₑ
quelconque sont fixées (§12.6.3.12). - Si aucune variable de type de ce genre n'existe, toutes les variables de type non fixées
Xᵢ
sont fixes pour lesquelles toutes les conditions suivantes sont respectées :- Il existe au moins une variable de type
Xₑ
qui dépend deXᵢ
. -
Xᵢ
possède un ensemble non vide de bornes
- Il existe au moins une variable de type
- S’il n’existe aucune variable de ce type et qu’il reste encore des variables de type non fixées, l’inférence de type échoue.
- Sinon, s'il n'existe pas d'autres variables de type non fixées, l'inférence de type réussit.
- Sinon, pour tous les arguments
Eᵢ
avec le type de paramètreTᵢ
correspondant où les types de sortie (§12.6.3.5) contiennent des variables de type non fixéesXₑ
mais pas les types d'entrée (§12.6.3.4), une inférence de type de sortie (§12.6.3.7) est effectuée à partir deEᵢ
toTᵢ
. Ensuite, la deuxième phase est répétée.
12.6.3.4 Types d’entrée
Si E
est un groupe de méthodes ou une fonction anonyme à typage implicite et que T
est un type de délégué ou un type d’arbre d’expression, alors tous les types de paramètres de T
sont types d’entrée deE
avec le typeT
.
12.6.3.5 Types de sortie
Si E
est un groupe de méthodes ou une fonction anonyme et T
est un type délégué ou un type d’arborescence d’expressions, alors, le type de retour de T
est un type de sortie deE
avec le typeT
.
12.6.3.6 Dépendance
Une variable de type non fixéXᵢ
dépend directement d'une variable de type non fixéXₑ
si, pour un certain argument, Eᵥ
de type Tᵥ
Xₑ
apparaît dans un type d'entrée de Eᵥ
de type Tᵥ
et Xᵢ
apparaît dans un type de sortie de Eᵥ
de type Tᵥ
.
Xₑ
dépend deXᵢ
si Xₑ
dépend directement deXᵢ
ou si Xᵢ
dépend directement deXᵥ
et Xᵥ
dépend deXₑ
. « dépend de » est donc la clôture transitive mais non réflexive de « dépend directement de ».
12.6.3.7 Inférences de type de sortie
Une inférence de type de sortie est faite à partir d'une expression E
vers un type T de la manière suivante :
- Si
est une fonction anonyme avec un type de retour déduit ( §12.6.3.13 ) etest un type délégué ou un type d’arborescence d’expressions avec le type de retour , alors une inférence à borne inférieure ( §12.6.3.10 ) est effectuéede à . - Dans le cas contraire, si
E
est un groupe de méthodes et queT
est un type de délégué ou un type d’arbre d’expression avec des types de paramètresT₁...Tᵥ
et un type de retourTₓ
, et que la résolution de surcharges deE
avec les typesT₁...Tᵥ
donne une méthode unique avec un type de retourU
, alors une inférence par borne inférieure est effectuée deU
àTₓ
. - Dans le cas contraire, si
E
est une expression de typeU
, une inférence pour limite inférieure est effectuée deU
àT
. - Sinon, aucune inférence n’est effectuée.
12.6.3.8 Inférences explicites de type de paramètre
Une inférence de type de paramètre explicite est faite à partir d'une expression E
vers un type T
de la manière suivante :
- Si
E
est une fonction anonyme explicitement typée avec des types de paramètresU₁...Uᵥ
etT
est un type délégué ou un type d'arbre d'expression avec des types de paramètresV₁...Vᵥ
, alors pour chaqueUᵢ
une inférence exacte (§12.6.3.9) est faite de versUᵢ
le correspondantVᵢ
.
12.6.3.9 Inférences exactes
Une inférence exacte d'un type U
à un type V
est effectuée comme suit :
- Si
V
fait partie des non fixéesXᵢ
, alorsU
est ajouté à l’ensemble des bornes exactes pourXᵢ
. - Sinon, les ensembles
V₁...Vₑ
etU₁...Uₑ
sont déterminés en vérifiant si l’un des cas suivants s’applique :-
V
est un type de tableauV₁[...]
etU
est un type de tableauU₁[...]
de même dimension -
V
est le typeV₁?
etU
est le typeU₁
-
V
est un type construitC<V₁...Vₑ>
etU
est un type construitC<U₁...Uₑ>
Si l'un de ces cas s'applique, une inférence exacte est faite de chaqueUᵢ
auVᵢ
correspondant.
-
- Sinon, aucune inférence n’est effectuée.
12.6.3.10 Inférences de limite inférieure
Une inférence par borne inférieure d'un type U
à un type V
est effectuée comme suit :
- Si
V
est l’un desXᵢ
non corrigés,U
est ajouté à l’ensemble de limites inférieures pourXᵢ
. - Sinon, si
V
est le typeV₁?
etU
est le typeU₁?
, alors une inférence par borne inférieure est effectuée deU₁
àV₁
. - Sinon, les ensembles
U₁...Uₑ
etV₁...Vₑ
sont déterminés en vérifiant si l’un des cas suivants s’applique :-
V
est un type de tableauV₁[...]
etU
est un type de tableauU₁[...]
de même dimension -
V
est l’un deIEnumerable<V₁>
,ICollection<V₁>
,IReadOnlyList<V₁>>
,IReadOnlyCollection<V₁>
ouIList<V₁>
etU
est un type de tableau unidimensionnelU₁[]
-
V
est un typeclass
,struct
,interface
oudelegate
construitC<V₁...Vₑ>
et il existe un typeC<U₁...Uₑ>
unique tel queU
(ou, siU
est un typeparameter
, sa classe de base effective ou tout membre de son ensemble d'interfaces effectives) est identique à,inherits
de (directement ou indirectement), ou implémente (directement ou indirectement)C<U₁...Uₑ>
. - (La restriction « unicité » signifie que dans le cas de l’interface
C<T>{} class U: C<X>, C<Y>{}
, aucune inférence n’est effectuée lors de l’inférence deU
versC<T>
parce queU₁
pourrait êtreX
ouY
.)
Si l’un de ces cas s’applique, alors une inférence est effectuée de chaqueUᵢ
vers leVᵢ
correspondant comme suit : - Si l’on ne sait pas que
Uᵢ
est un type de référence, alors une inférence exacte est effectuée. - Sinon, si
U
est de type tableau, une inférence de borne inférieure est effectuée. - Sinon, si
V
estC<V₁...Vₑ>
, alors l’inférence dépend du paramètre de typei-th
deC
:- S’il est covariant, alors une inférence par borne inférieure est effectuée.
- S’il est contravariant, alors une inférence par borne supérieure est effectuée.
- S’il est invariant, alors une inférence exacte est effectuée.
-
- Sinon, aucune inférence n’est effectuée.
12.6.3.11 Inférences de limite supérieure
Une inférence de limite supérieure d'un type U
à un type V
est effectuée comme suit :
- Si
V
fait partie des non fixéesXᵢ
, alorsU
est ajouté à l’ensemble des bornes supérieures pourXᵢ
. - Sinon, les ensembles
V₁...Vₑ
etU₁...Uₑ
sont déterminés en vérifiant si l’un des cas suivants s’applique :-
U
est un type de tableauU₁[...]
etV
est un type de tableauV₁[...]
de même dimension -
U
est l’un deIEnumerable<Uₑ>
,ICollection<Uₑ>
,IReadOnlyList<Uₑ>
,IReadOnlyCollection<Uₑ>
ouIList<Uₑ>
etV
est un type de tableau unidimensionnelVₑ[]
-
U
est le typeU1?
etV
est le typeV1?
-
U
est une classe construite, une structure, une interface ou un délégué de typeC<U₁...Uₑ>
etV
est un typeclass, struct, interface
oudelegate
qui estidentical
à,inherits
de (directement ou indirectement), ou implémente (directement ou indirectement) un type uniqueC<V₁...Vₑ>
. - (La restriction « unicité » signifie qu’étant donnée une interface
C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}
, aucune inférence n’est effectuée lors de l’inférence deC<U₁>
versV<Q>
. Aucune inférence n’est effectuée deU₁
, ni versX<Q>
ni versY<Q>
.)
Si l’un de ces cas s’applique, alors une inférence est effectuée de chaqueUᵢ
vers leVᵢ
correspondant comme suit : - Si l’on ne sait pas que
Uᵢ
est un type de référence, alors une inférence exacte est effectuée. - Sinon, si
V
est un type de tableau, alors une inférence par borne supérieure est effectuée. - Sinon, si
U
estC<U₁...Uₑ>
, alors l’inférence dépend du paramètre de typei-th
deC
:- S’il est covariant, alors une inférence par borne supérieure est effectuée.
- S’il est contravariant, alors une inférence par borne inférieure est effectuée.
- S’il est invariant, alors une inférence exacte est effectuée.
-
- Sinon, aucune inférence n’est effectuée.
12.6.3.12 Fixation
Une variable de type Xᵢ
non fixée avec un ensemble de bornes est fixée comme suit :
- L'ensemble des types candidats
Uₑ
commence par être l'ensemble de tous les types de l'ensemble des bornes deXᵢ
. - Chaque limite de
Xᵢ
est examinée à son tour : pour chaque limite exacte U deXᵢ
, tous les typesUₑ
qui ne sont pas identiques àU
sont supprimés de l’ensemble des candidats. Pour chaque borne inférieureU
deXᵢ
, tous les typesUₑ
vers lesquels il n'existe pas de conversion implicite à partir deU
sont supprimés de l'ensemble des candidats. Pour chaque borne supérieure U deXᵢ
, tous les typesUₑ
à partir desquels il n'y a pas de conversion implicite versU
sont retirés de l'ensemble des candidats. - Si parmi les types candidats restants
Uₑ
, il existe un type uniqueV
vers lequel il y a une conversion implicite depuis tous les autres types candidats, alorsXᵢ
est fixé àV
. - Sinon, l’inférence de type échoue.
12.6.3.13 Type de retour déduit
Le type de retour inféré d’une fonction anonyme F
est utilisé lors de l’inférence de type et de la résolution de surcharges. Le type de retour inféré ne peut être déterminé que pour une fonction anonyme où tous les types de paramètres sont connus, soit parce qu’ils sont spécifiés explicitement, fournis via une conversion de fonction anonyme ou inférés lors de l’inférence de type sur un appel de méthode générique englobante.
Le type de retour effectif inféré est déterminé comme suit :
- Si le corps de
F
est une expression possédant un type, alors le type de retour effectif inféré deF
est le type de cette expression. - Si le corps de
F
est un bloc et que l’ensemble des expressions dans les instructionsreturn
du bloc a un type le plus communT
(§12.6.3.15), alors le type de retour effectif dérivé deF
estT
. - Sinon, un type de retour effectif ne peut être inféré pour
F
.
Le type de retour inféré est déterminé comme suit :
- Si
F
est asynchrone et que le corps deF
est soit une expression sans classification (§12.2), soit un bloc où aucune instructionreturn
n’a d’expressions, le type de retour inféré est«TaskType»
(§15.15.1). - Si
F
est asynchrone et a un type de retour effectif inféréT
, le type de retour inféré est«TaskType»<T>»
(§15.15.1). - Si
F
n’est pas asynchrone et a un type de retour effectif inféréT
, le type de retour inféré estT
. - Sinon, un type de retour ne peut être inféré pour
F
.
Exemple : À titre d’exemple d’inférence de type impliquant des fonctions anonymes, considérez la méthode d’extension
Select
déclarée dans la classeSystem.Linq.Enumerable
:namespace System.Linq { public static class Enumerable { public static IEnumerable<TResult> Select<TSource,TResult>( this IEnumerable<TSource> source, Func<TSource,TResult> selector) { foreach (TSource element in source) { yield return selector(element); } } } }
En supposant que l’espace de noms
System.Linq
a été importé avec une directiveusing namespace
, et étant donné une classeCustomer
avec une propriétéName
de typestring
, la méthodeSelect
peut être utilisée pour sélectionner les noms d’une liste de clients :List<Customer> customers = GetCustomerList(); IEnumerable<string> names = customers.Select(c => c.Name);
L'invocation de la méthode d'extension (§12.8.10.3) de
Select
est traitée par la réécriture de cette invocation en une invocation de méthode statique :IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);
Puisque les arguments de type n’ont pas été spécifiés explicitement, l’inférence de type est utilisée pour déduire les arguments de type. Tout d'abord, l'argument des clients est lié au paramètre source, en déduisant que
TSource
estCustomer
. Ensuite, à l’aide du processus d’inférence de type de fonction anonyme, comme décrit ci-dessus,c
est donné le typeCustomer
, et l’expressionc.Name
est liée au type de retour du paramètre de sélecteur, en déduisant queTResult
eststring
. Ainsi, l'invocation est équivalente àSequence.Select<Customer,string>(customers, (Customer c) => c.Name)
et le résultat est de type
IEnumerable<string>
.L’exemple suivant montre comment l’inférence de type de fonction anonyme permet aux informations de type de circuler entre les arguments lors d'un appel de méthode générique. Etant donné la méthode et l'invocation suivantes :
class A { static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2) { return f2(f1(value)); } static void M() { double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours); } }
L’inférence de type pour l’appel se poursuit comme suit : Tout d’abord, l’argument « 1:15:30 » est lié au paramètre de valeur, en déduisant que
X
eststring
. Ensuite, le paramètre de la première fonction anonyme,s
, est doté du type déduitstring
, et l’expressionTimeSpan.Parse(s)
est liée au type de retour def1
, déduisant ainsi queY
estSystem.TimeSpan
. Enfin, le paramètre de la deuxième fonction anonyme,t
, se voit attribuer le type déduitSystem.TimeSpan
, et l'expressiont.TotalHours
est liée au type de retour def2
, en déduisant queZ
estdouble
. Ainsi, le résultat de l’appel est de typedouble
.exemple final
12.6.3.14 Inférence de type pour la conversion de groupes de méthodes
À l’instar des appels de méthodes génériques, l’inférence de type doit également être appliquée lorsqu’un groupe de méthodes M
contenant une méthode générique est converti en un type de délégué donné D
(§10.8). Étant donné une méthode
Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)
et dans le cas où le groupe de méthodes M
est affecté au type de délégué D
, la tâche de l’inférence de type est de trouver des arguments de type S₁...Sᵥ
de manière à ce que l’expression :
M<S₁...Sᵥ>
devienne compatible (§20.2) avec D
.
Contrairement à l’algorithme d’inférence de type pour les appels de méthode générique, dans ce cas, il n'existe que des types d'argument , aucune expression d'argument . Plus particulièrement, il n’y a pas de fonctions anonymes et donc pas besoin de plusieurs phases d’inférence.
Au lieu de cela, tous les Xᵢ
sont considérés comme non fixés, et une inférence de borne inférieure est faite à partir de chaque type d'argument Uₑ
D
de jusqu'au type de paramètre Tₑ
correspondant de M
. Si aucune borne n'a été trouvée pour l'un des Xᵢ
, l'inférence de type échoue. Dans le cas contraire, tous les Xᵢ
sont fixés aux Sᵢ
correspondants, qui sont le résultat de l'inférence de type.
12.6.3.15 Recherche du meilleur type commun d’un ensemble d’expressions
Dans certains cas, un type commun doit être inféré pour un ensemble d’expressions. En particulier, les types d'éléments des tableaux implicitement typés et les types de retour des fonctions anonymes avec corps de bloc sont trouvés de cette manière.
Le meilleur type commun pour un ensemble d’expressions E₁...Eᵥ
est déterminé comme suit :
- Une nouvelle variable de type non fixé
X
est introduite. - Pour chaque expression
Ei
, une inférence de type de sortie (§12.6.3.7) est effectuée de l'expression àX
. -
X
est fixé (§12.6.3.12), si possible, et le type résultant est le meilleur type commun. - Sinon, l’inférence échoue.
Remarque : de manière intuitive, cette inférence est équivalente à appeler une méthode
void M<X>(X x₁ ... X xᵥ)
avecEᵢ
comme arguments et à en déduireX
. fin de la remarque
12.6.4 Résolution de surcharge
12.6.4.1 Général
La résolution de surcharge est un mécanisme de temps de liaison permettant de sélectionner le meilleur membre de fonction à invoquer compte tenu d'une liste d'arguments et d'un ensemble de membres de fonction candidats. La résolution de surcharges sélectionne le membre de fonction à appeler dans les contextes distincts suivants en C#:
- Appel d’une méthode nommée dans une invocation_expression (§12.8.10).
- Appel d'un constructeur d'instance nommé dans une expression_de_création_d'objet (§12.8.17.2).
- Invocation d'un accesseur d'indexeur par l'intermédiaire d'un element_access (§12.8.12).
- L’appel d’un opérateur prédéfini ou défini par l’utilisateur référencé dans une expression (§12.4.4 et §12.4.5).
Chacun de ces contextes définit l’ensemble des membres de fonction candidats et la liste des arguments de manière propre. Par exemple, l’ensemble des candidats pour un appel de méthode n’inclut pas les méthodes marquées override (§12.5), et les méthodes d’une classe de base ne sont pas candidates si une méthode dans une classe dérivée est applicable (§12.8.10.2).
Une fois les membres de fonction candidats et la liste d’arguments identifiés, la sélection du meilleur membre de fonction est la même dans tous les cas :
- Tout d’abord, l’ensemble des membres de fonction candidats est réduit à ceux qui sont applicables par rapport à la liste d’arguments donnée (§12.6.4.2). Si cet ensemble réduit est vide, une erreur compile-time se produit.
- Ensuite, le meilleur membre de fonction parmi l’ensemble des membres de la fonction candidate applicable est identifié. Si l'ensemble ne contient qu'un seul membre de fonction, ce membre de fonction est le meilleur membre de fonction. Sinon, le meilleur membre de fonction est celui qui est supérieur à tous les autres membres de fonction par rapport à la liste d’arguments donnée, à condition que chaque membre de fonction soit comparé à tous les autres en utilisant les règles de §12.6.4.3. S'il n'y a pas exactement un membre de fonction qui est meilleur que tous les autres membres de fonction, alors l'invocation du membre de fonction est ambiguë et une erreur de liaison se produit.
Les sous-sections suivantes définissent la signification exacte des termes membre de fonction applicable et membre de fonction supérieur.
12.6.4.2 Membre de fonction applicable
On dit d’un membre de fonction qu’il est un membre de fonction applicable par rapport à une liste d’arguments A
lorsque tous les points suivants sont vrais:
- Chaque argument dans
A
correspond à un paramètre dans la déclaration du membre de fonction comme décrit dans §12.6.2.2, au maximum un argument correspond à chaque paramètre, et tout paramètre auquel aucun argument ne correspond est un paramètre optionnel. - Pour chaque argument dans
A
, le mode de passage de l’argument est identique au mode de passage du paramètre correspondant, et- pour un paramètre valeur ou un tableau de paramètres, une conversion implicite (§10.2) existe de l’expression de l’argument vers le type du paramètre correspondant, ou
- pour un paramètre référence ou de sortie, il existe une conversion identité entre le type de l’expression de l’argument (le cas échéant) et le type du paramètre correspondant, ou
- pour un paramètre d’entrée quand l’argument correspondant possède le modificateur
in
, il existe une conversion identité entre le type de l’expression de l’argument (le cas échéant) et le type du paramètre correspondant, ou - pour un paramètre d’entrée quand l’argument correspondant omet le modificateur
in
, une conversion implicite (§10.2) existe de l’expression de l’argument vers le type du paramètre correspondant.
Pour un membre de fonction qui inclut un tableau de paramètres, si le membre de fonction est applicable selon les règles ci-dessus, on dit qu’il est applicable sous sa forme normale. Si un membre de fonction qui inclut un tableau de paramètres n’est pas applicable sous sa forme normale, il se peut que le membre de fonction soit applicable sous sa forme étendue :
- La forme étendue est construite en remplaçant le tableau de paramètres dans la déclaration du membre de fonction par zéro ou plusieurs paramètres valeur du type élément du tableau de paramètres de sorte que le nombre d’arguments dans la liste d’arguments
A
corresponde au nombre total de paramètres. SiA
comporte moins d’arguments que le nombre de paramètres fixes dans la déclaration du membre de fonction, la forme étendue du membre de fonction ne peut pas être construite et n’est donc pas applicable. - Sinon, la forme étendue est applicable si pour chaque argument dans
A
, l’une des conditions suivantes est satisfaite :- le mode de passage de paramètre de l’argument est identique au mode de passage de paramètre du paramètre correspondant et :
- pour un paramètre de valeur fixe ou un paramètre de valeur créé par l’extension, une conversion implicite (§10.2) existe de l’expression d’argument au type du paramètre correspondant ; ou
- pour un paramètre par référence, le type de l’expression de l’argument est identique au type du paramètre correspondant.
- le mode de passage de paramètre de l’argument est valeur, et le mode de passage de paramètre du paramètre associé est d’entrée, et une conversion implicite (§10.2) existe de l’expression d’argument au type du paramètre associé.
- le mode de passage de paramètre de l’argument est identique au mode de passage de paramètre du paramètre correspondant et :
Lorsque la conversion implicite du type de l’argument vers le type du paramètre d’un paramètre d’entrée est une conversion implicite dynamique (§10.2.10), les résultats sont indéfinis.
Exemple : étant donné les déclarations suivantes et les appels de méthode :
public static void M1(int p1) { ... } public static void M1(in int p1) { ... } public static void M2(in int p1) { ... } public static void Test() { int i = 10; uint ui = 34U; M1(in i); // M1(in int) is applicable M1(in ui); // no exact type match, so M1(in int) is not applicable M1(i); // M1(int) and M1(in int) are applicable M1(i + 5); // M1(int) and M1(in int) are applicable M1(100u); // no implicit conversion exists, so M1(int) is not applicable M2(in i); // M2(in int) is applicable M2(i); // M2(in int) is applicable M2(i + 5); // M2(in int) is applicable }
exemple final
- Une méthode statique n’est applicable que si le groupe de méthodes résulte d’un simple_name ou d’un member_access via un type.
- Une méthode d’instance n’est applicable que si le groupe de méthodes résulte d’un simple_name, d’un member_access via une variable ou une valeur, ou d’un base_access.
- Si le groupe de méthodes résulte d’un simple_name, une méthode d’instance n’est applicable que si l’accès
this
est autorisé §12.8.14.
- Si le groupe de méthodes résulte d’un simple_name, une méthode d’instance n’est applicable que si l’accès
- Lorsque le groupe de méthodes résulte d’un member_access qui peut provenir soit d’une instance, soit d’un type, comme décrit dans §12.8.7.2, les méthodes d’instance et statiques sont applicables.
- Une méthode générique dont les arguments de type (spécifiés explicitement ou déduits) ne satisfont pas toutes leurs contraintes n’est pas applicable.
- Dans le contexte d’une conversion de groupe de méthodes, il doit exister une conversion identité (§10.2.2) ou une conversion de référence implicite (§10.2.8) du type de retour de la méthode vers le type de retour du délégué. Sinon, la méthode candidate n’est pas applicable.
12.6.4.3 Meilleur membre de fonction
Pour déterminer le meilleur membre de la fonction, une liste d’arguments simplifiée A
est construite, contenant uniquement les expressions d’argument elles-mêmes dans l’ordre dans lequel elles apparaissent dans la liste d’arguments d’origine, en excluant les arguments out
ou ref
.
Les listes de paramètres pour chaque membre de la fonction candidate sont construites de la manière suivante :
- La forme développée est utilisée si le membre de la fonction était applicable uniquement dans la forme développée.
- Les paramètres optionnels sans argument correspondant sont supprimés de la liste de paramètres
- Les paramètres de référence et de sortie sont supprimés de la liste de paramètres
- Les paramètres sont réordonnés de façon à occuper la même position que l’argument correspondant dans la liste d’arguments.
Étant donnée une liste d’arguments A
avec un ensemble d’expressions d’argument {E₁, E₂, ..., Eᵥ}
et deux membres de fonction applicables Mᵥ
et Mₓ
avec des types de paramètres {P₁, P₂, ..., Pᵥ}
et {Q₁, Q₂, ..., Qᵥ}
, Mᵥ
est défini comme étant un meilleur membre de fonction que Mₓ
si
- pour chaque argument, la conversion implicite de
Eᵥ
enQᵥ
n’est pas meilleure que la conversion implicite deEᵥ
enPᵥ
, et - pour au moins un argument, la conversion de
Eᵥ
enPᵥ
est meilleure que la conversion deEᵥ
enQᵥ
.
Si les séquences de types de paramètres {P₁, P₂, ..., Pᵥ}
et {Q₁, Q₂, ..., Qᵥ}
sont équivalentes (c'est-à-dire que chaque Pᵢ
possède une conversion d'identité vers le Qᵢ
correspondant), les règles de départage suivantes sont appliquées, dans l'ordre, pour déterminer le meilleur membre de la fonction.
- Si
Mᵢ
est une méthode non générique et queMₑ
est une méthode générique, alorsMᵢ
est mieux queMₑ
. - Sinon, si
Mᵢ
est applicable sous sa forme normale et queMₑ
possède un tableau de paramètres et n’est applicable que sous sa forme étendue, alorsMᵢ
est mieux queMₑ
. - Sinon, si les deux méthodes possèdent des tableaux de paramètres et ne sont applicables que sous leurs formes étendues, et si le tableau de paramètres de
Mᵢ
comporte moins d’éléments que le tableau de paramètres deMₑ
, alorsMᵢ
est mieux queMₑ
. - Sinon, si
Mᵥ
a des types de paramètres plus spécifiques queMₓ
, alorsMᵥ
est mieux queMₓ
. Soit{R1, R2, ..., Rn}
et{S1, S2, ..., Sn}
les types de paramètres non instanciés et non étendus deMᵥ
et deMₓ
. Les types de paramètresMᵥ
sont plus spécifiques que ceux lesMₓ
si, pour chaque paramètre,Rx
n’est pas moins spécifique queSx
, et, pour au moins un paramètre,Rx
est plus spécifique queSx
:- Un paramètre de type est moins spécifique qu’un paramètre non typé.
- De manière récursive, un type construit est plus spécifique qu’un autre type construit (avec le même nombre d’arguments de type) si au moins un argument de type est plus spécifique et qu’aucun argument de type n’est moins spécifique que l’argument de type correspondant dans l’autre.
- Un type de tableau est plus spécifique qu’un autre type de tableau (ayant le même nombre de dimensions) si le type élément du premier est plus spécifique que le type élément du second.
- Sinon, si un membre est un opérateur non lifté et que l'autre est un opérateur lifté, celui qui n'est pas lifté est préférable.
- Si aucun membre de fonction n’a été jugé supérieur, et que tous les paramètres de
Mᵥ
ont un argument correspondant alors que des arguments par défaut doivent être substitués pour au moins un paramètre optionnel dansMₓ
, alorsMᵥ
est mieux queMₓ
. - Si pour au moins un paramètre,
Mᵥ
utilise le choix de passage de paramètres supérieur (§12.6.4.4) que le paramètre correspondant dansMₓ
et qu’aucun des paramètres dansMₓ
n’utilise le choix de passage de paramètres supérieur à celui deMᵥ
, alorsMᵥ
est mieux queMₓ
. - Sinon, aucun membre de fonction n'est meilleur.
12.6.4.4 Meilleur mode de passage de paramètres
Il est permis d'avoir des paramètres correspondants dans deux méthodes surchargées qui ne diffèrent que par le mode de passage des paramètres, à condition qu'un des paramètres soit passé par valeur, comme suit :
public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
Étant donné int i = 10;
, conformément au § 12.6.4.2, les appels M1(i)
et M1(i + 5)
entraînent l'application des deux surcharges. Dans de tels cas, la méthode avec le mode de passage de paramètre de valeur est le meilleur choix de mode de passage de paramètre.
Remarque : un tel choix n’est pas nécessaire pour les arguments de passage par entrée, sortie ou référence, car ces arguments ne correspondent qu’aux modes de passage de paramètres exacts. fin de la remarque
12.6.4.5 Meilleure conversion à partir de l’expression
Étant donnée une conversion implicite C₁
qui convertit une expression E
en un type T₁
, et une conversion implicite C₂
qui convertit une expression E
en un type T₂
, C₁
est une meilleur conversion que C₂
si l’une des conditions suivantes est remplie :
-
E
correspond exactement àT₁
etE
ne correspond pas exactement àT₂
(§12.6.4.6) E
correspond exactement à l'un ou à aucun des deuxT₁
etT₂
, etT₁
est une meilleure cible de conversion queT₂
(§12.6.4.7)-
E
est un groupe de méthodes (§12.2),T₁
est compatible (§20.4) avec la meilleure méthode unique du groupe de méthodes pour la conversionC₁
, etT₂
n’est pas compatible avec la meilleure méthode unique du groupe de méthodes pour la conversionC₂
12.6.4.6 Expression correspondant exactement
Étant donné une expression E
et un type T
, E
correspond exactementT
si l’une des conditions suivantes est remplie :
E
a un typeS
, et une conversion d'identité existe deS
àT
E
est une fonction anonyme,T
est soit un délégué de typeD
, soit un arbre d'expression de typeExpression<D>
, et l'une des conditions suivantes est remplie :- Un type de retour inféré
X
existe pourE
dans le contexte de la liste de paramètres deD
(§12.6.3.12), et une conversion identité existe deX
vers le type de retour deD
-
E
est uneasync
lambda sans valeur de retour, etD
a un type de retour qui est un«TaskType»
non générique - Soit
E
est non-asynchrone etD
a un type de retourY
, soitE
est asynchrone etD
a un type de retour«TaskType»<Y>
(§15.15.1), et l'une des conditions suivantes est remplie :- Le corps de
E
est une expression qui correspond exactement àY
- Le corps de
E
est un bloc où chaque instruction return renvoie une expression qui correspond exactement àY
- Le corps de
- Un type de retour inféré
12.6.4.7 Meilleure cible de conversion
Étant donné deux types T₁
et T₂
, T₁
est une meilleure cible de conversion que T₂
si l'une des conditions suivantes est remplie :
- Une conversion implicite de
T₁
versT₂
existe et aucune conversion implicite deT₂
versT₁
n’existe -
T₁
est«TaskType»<S₁>
(§15.15.1),T₂
est«TaskType»<S₂>
etS₁
est une meilleure cible de conversion queS₂
-
T₁
est«TaskType»<S₁>
(§15.15.1),T₂
est«TaskType»<S₂>
etT₁
est plus spécialisé queT₂
-
T₁
estS₁
ouS₁?
oùS₁
est un type intégral signé etT₂
estS₂
ouS₂?
oùS₂
est un type intégral non signé. En particulier :-
S₁
estsbyte
etS₂
estbyte
,ushort
,uint
, ouulong
-
S₁
estshort
etS₂
estushort
,uint
, ouulong
-
S₁
estint
etS₂
estuint
, ouulong
-
S₁
estlong
etS₂
estulong
-
12.6.4.8 Surcharge dans les classes génériques
Remarque : bien que les signatures telles qu’elles sont déclarées doivent être uniques (§8.6), il est possible que la substitution des arguments de type aboutisse à des signatures identiques. Dans une telle situation, la résolution de surcharge sélectionnera la signature la plus spécifique (§12.6.4.3) des signatures d’origine (avant substitution d’arguments de type), si elle existe, et sinon, signale une erreur. fin de la remarque
Exemple : les exemples suivants montrent des surcharges valides et invalides selon cette règle :
public interface I1<T> { ... } public interface I2<T> { ... } public abstract class G1<U> { public abstract int F1(U u); // Overload resolution for G<int>.F1 public abstract int F1(int i); // will pick non-generic public abstract void F2(I1<U> a); // Valid overload public abstract void F2(I2<U> a); } abstract class G2<U,V> { public abstract void F3(U u, V v); // Valid, but overload resolution for public abstract void F3(V v, U u); // G2<int,int>.F3 will fail public abstract void F4(U u, I1<V> v); // Valid, but overload resolution for public abstract void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail public abstract void F5(U u1, I1<V> v2); // Valid overload public abstract void F5(V v1, U u2); public abstract void F6(ref U u); // Valid overload public abstract void F6(out V v); }
exemple final
12.6.5 Vérification compile-time de l'invocation dynamique d'un membre
Bien que la résolution de surcharge d’une opération liée dynamiquement se fasse à l’exécution, il est parfois possible, à la compilation, de connaître la liste des membres de fonction parmi lesquels une surcharge sera choisie :
- Pour un appel délégué (§12.8.10.4), la liste est un unique membre de fonction ayant la même liste de paramètres que le delegate_type de l’appel
- Pour un appel de méthode (§12.8.10.2) sur un type, ou sur une valeur dont le type statique n’est pas dynamique, l’ensemble des méthodes accessibles dans le groupe de méthodes est connu à la compilation.
- Pour une expression de création d’objet (§12.8.17.2), l’ensemble des constructeurs accessibles dans le type est connu à la compilation.
- Pour un accès à un indexeur (§12.8.12.3), l’ensemble des indexeurs accessibles dans le récepteur est connu à la compilation.
Dans ces cas, une vérification limitée au moment de la compilation est effectuée sur chaque membre de l'ensemble connu de membres de fonctions, pour voir s'il est possible de savoir avec certitude qu'il ne sera jamais invoqué au moment de l'exécution. Pour chaque membre de fonction F
, une liste modifiée de paramètres et d’arguments est construite :
- D’abord, si
F
est une méthode générique et que des arguments de type ont été fournis, ceux-ci sont substitués aux paramètres de type dans la liste de paramètres. En revanche, si des arguments de type n’ont pas été fournis, aucune substitution n’a lieu. - Ensuite, tout paramètre dont le type est ouvert (c’est-à-dire qui contient un paramètre de type. Voir §8.4.3) est omis, ainsi que le ou les paramètres correspondants.
Pour que F
passe la vérification, tous les éléments suivants doivent être vrais :
- La liste de paramètres modifiée pour
F
est applicable à la liste d’arguments modifiée au regard de §12.6.4.2. - Tous les types construits dans la liste de paramètres modifiée satisfont leurs contraintes (§8.4.5).
- Si les paramètres de type de
F
ont été substitués lors de l’étape précédente, leurs contraintes sont satisfaites. - Si
F
est une méthode statique, le groupe de méthodes ne doit pas avoir résulté d'un member_access dont le récepteur est connu à la compilation pour être une variable ou une valeur. - Si
F
est une méthode d’instance, le groupe de méthode ne doit pas avoir résulté d'un member_access dont le récepteur est connu, à la compilation, comme étant d'un type.
Si aucun candidat ne satisfait ce test, une erreur de compilation se produit.
12.6.6 Invocation d'un membre de fonction
12.6.6.1 Général
Cette sous-clause décrit le processus qui se déroule au moment de l’exécution pour appeler un membre fonctionnel particulier. On suppose qu'un processus de liaison a déjà déterminé le membre particulier à invoquer, éventuellement en appliquant la résolution de surcharge à un ensemble de membres de fonction candidats.
Pour décrire le processus d’appel, les membres de fonction sont divisés en deux catégories :
- Membres de fonctions statiques. Il s’agit des méthodes statiques, des accesseurs de propriétés statiques et des opérateurs définis par l’utilisateur. Les membres de fonction statiques sont toujours non virtuels.
- Membres de fonctions d'instance. Il s'agit des méthodes d'instance, des constructeurs d'instance, des accesseurs de propriété d'instance et des accesseurs d'indexeur. Les membres fonctionnels d’instance sont soit non virtuels, soit virtuels, et sont toujours appelés sur une instance particulière. L’instance est calculée par une expression d’instance, et elle devient accessible au sein du membre de fonction en tant que
this
(§12.8.14). Pour un constructeur d’instance, l’expression d’instance est considérée comme l’objet nouvellement alloué.
Le traitement à l’exécution d’un appel de membre de fonction se compose des étapes suivantes, où M
est le membre de fonction et, si M
est un membre d’instance, E
est l’expression d’instance :
Si
M
est un membre de fonction statique :- La liste d’arguments est évaluée comme décrit dans §12.6.2.
-
M
est invoquée.
Sinon, si le type de
E
est un type de valeurV
, et queM
est déclaré ou surchargé dansV
:E
est évalué. Si cette évaluation provoque une exception, aucune étape supplémentaire n’est exécutée. Pour un constructeur d’instance, cette évaluation consiste à allouer de la mémoire (généralement à partir d’une pile d’exécution) pour le nouvel objet. Dans ce cas,E
est classifié comme une variable.- Si
E
n’est pas classifié comme une variable, ou siV
n’est pas un type struct en lecture seule (§16.2.2), et queE
est :- un paramètre d’entrée (§15.6.2.3.2), ou
- un champ
readonly
(§15.5.3), ou - une variable de référence ou un retour
readonly
(§9.7),
alors une variable locale temporaire de type
E
est créée et la valeur deE
lui est assignée.E
ensuite reclassifié comme une référence à cette variable locale temporaire. La variable temporaire est accessible en tant quethis
dansM
, mais pas autrement. Ainsi, ce n’est que lorsqueE
peut être écrit qu’il est possible pour l’appelant d’observer les modifications queM
apporte àthis
.- La liste d’arguments est évaluée comme décrit dans §12.6.2.
-
M
est invoquée. La variable référencée parE
devient la variable référencée parthis
.
Sinon :
E
est évalué. Si cette évaluation provoque une exception, aucune étape supplémentaire n’est exécutée.- La liste d’arguments est évaluée comme décrit dans §12.6.2.
- Si le type de
E
est un value_type, une conversion en boîte (§10.2.9) est effectuée pour convertirE
en class_type, etE
est considéré comme appartenant à ce class_type dans les étapes suivantes. Si le value_type est un enum_type, le class_type estSystem.Enum;
sinon, il estSystem.ValueType
. - La valeur de
E
est vérifiée pour être valide. Si la valeur deE
est nulle, un(e)System.NullReferenceException
est levé(e) et aucune autre étape n’est exécutée. - L'implémentation du membre de fonction à invoquer est déterminée :
- Si le type contraignant de
E
est une interface, le membre de fonction à invoquer est l'implémentation deM
fournie par le type d'exécution de l'instance référencée parE
. Ce membre de fonction est déterminé en appliquant les règles de mappage d’interfaces (§18.6.5) afin de déterminer l’implémentation deM
fournie par le type d’exécution de l’instance référencée parE
. - Sinon, si
M
est un membre de fonction virtuel, le membre de fonction à appeler est l’implémentation deM
fournie par le type d’exécution de l’instance référencée parE
. Ce membre de fonction est déterminé en appliquant les règles pour déterminer l’implémentation la plus dérivée (§15.6.4) deM
par rapport au type d’exécution de l’instance référencée parE
. - Sinon,
M
est un membre de fonction non virtuel, et le membre de fonction à appeler estM
lui-même.
- Si le type contraignant de
- L'implémentation du membre de fonction déterminée à l'étape ci-dessus est invoquée. L’objet référencé par
E
devient l’objet référencé par cela.
Le résultat de l’appel d’un constructeur d’instance (§12.8.17.2) est la valeur créée. Le résultat de l'invocation de tout autre membre de fonction est la valeur, le cas échéant, renvoyée (§13.10.5) par son corps.
12.6.6.2 Invocations sur des instances encadrées
Un membre de fonction implémenté dans un value_type peut être appelé via une instance boxée de ce value_type dans les situations suivantes :
- Lorsque le membre de la fonction est un remplacement d'une méthode héritée du type class_type et qu'il est invoqué par l'intermédiaire d'une expression d'instance de ce class_type.
Remarque : le class_type sera toujours
System.Object
,System.ValueType
ouSystem.Enum
. fin de la remarque - Lorsque le membre de fonction est une implémentation d'un membre de fonction d'interface et qu'il est invoqué par l'intermédiaire d'une expression d'instance d'un interface_type.
- Quand le membre de fonction est appelé via un délégué.
Dans ces situations, l’instance encapsulée est considérée comme contenant une variable du type de valeur , et cette variable devient celle à laquelle la fonction membre se réfère lors de l'appel.
Remarque : Cela signifie particulièrement que lorsqu’un membre de fonction est appelé sur une instance boxée, il est possible que le membre de fonction modifie la valeur contenue dans l’instance boxée. fin de la remarque
12.7 Déconstruction
La déconstruction est un processus par lequel une expression est transformée en un tuple d’expressions individuelles. La déconstruction est utilisée lorsque la cible d’une simple affectation est une expression tuple, afin d’obtenir des valeurs à affecter à chacun des éléments de ce tuple.
Une expression E
est déconstructée en une expression sous forme de tuple avec n
éléments de la façon suivante :
- Si
E
est une expression tuple comportantn
éléments, le résultat de la déconstruction est l’expressionE
elle-même. - Sinon, si
E
a un type tuple(T1, ..., Tn)
avecn
éléments, alorsE
est évalué dans une variable temporaire__v
, et le résultat de la déconstruction est l’expression(__v.Item1, ..., __v.Itemn)
. - Sinon, si l’expression
E.Deconstruct(out var __v1, ..., out var __vn)
se résout à la compilation en une instance unique ou une méthode d’extension, cette expression est évaluée, et le résultat de la déconstruction est l’expression(__v1, ..., __vn)
. Une telle méthode est appelée déconstructeur. - Sinon,
E
ne peut pas être décomposée.
Ici, __v
et __v1, ..., __vn
font référence à des variables temporaires par ailleurs invisibles et inaccessibles.
Remarque : une expression de type
dynamic
ne peut pas être déconstruite. fin de la remarque
12.8 Expressions primaires
12.8.1 Général
Les expressions primaires incluent les formes les plus simples d’expressions.
primary_expression
: primary_no_array_creation_expression
| array_creation_expression
;
primary_no_array_creation_expression
: literal
| interpolated_string_expression
| simple_name
| parenthesized_expression
| tuple_expression
| member_access
| null_conditional_member_access
| invocation_expression
| element_access
| null_conditional_element_access
| this_access
| base_access
| post_increment_expression
| post_decrement_expression
| null_forgiving_expression
| object_creation_expression
| delegate_creation_expression
| anonymous_object_creation_expression
| typeof_expression
| sizeof_expression
| checked_expression
| unchecked_expression
| default_value_expression
| nameof_expression
| anonymous_method_expression
| pointer_member_access // unsafe code support
| pointer_element_access // unsafe code support
| stackalloc_expression
;
Remarque : ces règles de grammaire ne sont pas prêtes pour ANTLR car elles font partie d’un ensemble de règles récursives à gauche mutuellement (
primary_expression
,primary_no_array_creation_expression
,member_access
,invocation_expression
,element_access
,post_increment_expression
,post_decrement_expression
,null_forgiving_expression
,pointer_member_access
etpointer_element_access
) que ANTLR ne gère pas. Des techniques standard peuvent être utilisées pour transformer la grammaire afin de supprimer la récursion mutuelle à gauche. Cela n’a pas été fait, car toutes les stratégies d’analyse ne l'exigent pas (par exemple, un analyseur LALR ne le ferait pas) et cela compliquerait la structure et la description. fin de la remarque
pointer_member_access (§23.6.3) et pointer_element_access (§23.6.4) sont uniquement disponibles dans le code non sécurisé (§23).
Les expressions primaires se répartissent entre les array_creation_expressions et les primary_no_array_creation_expressions. Traiter array_creation_expression de cette manière, plutôt que de le lister avec les autres formes d’expressions simples, permet à la grammaire d’interdire du code potentiellement confus tel que
object o = new int[3][1];
qui serait autrement interprété comme
object o = (new int[3])[1];
12.8.2 Littéraux
Un primary_expression qui se compose d’un littéral (§6.4.5) est classé comme une valeur.
12.8.3 Expressions de chaînes interpolées
Une expression interpolated_string_expression se compose de $
, $@
ou @$
, immédiatement suivis de texte à l'intérieur de caractères "
. Dans le texte cité, il y a zéro ou plusieurs interpolations délimitées par des caractères {
et }
, chacune englobant une expression et des spécifications de formatage optionnelles.
Les expressions de chaînes interpolées ont deux formes : régulière (interpolated_regular_string_expression) et verbatim (interpolated_verbatim_string_expression) qui sont similaires d’un point de vie lexical, mais diffèrent sur le plan sémantique des deux formes de littéraux de chaîne (§6.4.5.6).
interpolated_string_expression
: interpolated_regular_string_expression
| interpolated_verbatim_string_expression
;
// interpolated regular string expressions
interpolated_regular_string_expression
: Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
Interpolated_Regular_String_End
;
regular_interpolation
: expression (',' interpolation_minimum_width)?
Regular_Interpolation_Format?
;
interpolation_minimum_width
: constant_expression
;
Interpolated_Regular_String_Start
: '$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Regular_String_Mid
: Interpolated_Regular_String_Element+
;
Regular_Interpolation_Format
: ':' Interpolated_Regular_String_Element+
;
Interpolated_Regular_String_End
: '"'
;
fragment Interpolated_Regular_String_Element
: Interpolated_Regular_String_Character
| Simple_Escape_Sequence
| Hexadecimal_Escape_Sequence
| Unicode_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Regular_String_Character
// Any character except " (U+0022), \\ (U+005C),
// { (U+007B), } (U+007D), and New_Line_Character.
: ~["\\{}\u000D\u000A\u0085\u2028\u2029]
;
// interpolated verbatim string expressions
interpolated_verbatim_string_expression
: Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
Interpolated_Verbatim_String_End
;
verbatim_interpolation
: expression (',' interpolation_minimum_width)?
Verbatim_Interpolation_Format?
;
Interpolated_Verbatim_String_Start
: '$@"'
| '@$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Verbatim_String_Mid
: Interpolated_Verbatim_String_Element+
;
Verbatim_Interpolation_Format
: ':' Interpolated_Verbatim_String_Element+
;
Interpolated_Verbatim_String_End
: '"'
;
fragment Interpolated_Verbatim_String_Element
: Interpolated_Verbatim_String_Character
| Quote_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Verbatim_String_Character
: ~["{}] // Any character except " (U+0022), { (U+007B) and } (U+007D)
;
// lexical fragments used by both regular and verbatim interpolated strings
fragment Open_Brace_Escape_Sequence
: '{{'
;
fragment Close_Brace_Escape_Sequence
: '}}'
;
Six des règles lexicales définies ci-dessus sont context sensitive comme suit :
Règle | Exigences contextuelles |
---|---|
Interpolated_Regular_String_Mid | Reconnu uniquement après un Interpolated_Regular_String_Start, entre les interpolations suivantes et avant l'Interpolated_Regular_String_End correspondant. |
Regular_Interpolation_Format | Reconnu uniquement dans le cadre d'une regular_interpolation et lorsque les deux points ( :) ne sont pas imbriqués dans un type de parenthèse (parenthèses/braces/carré). |
Interpolated_Regular_String_End | Reconnu uniquement après un Interpolated_Regular_String_Start et seulement si tous les jetons intermédiaires sont soit des Interpolated_Regular_String_Mids, soit des jetons pouvant faire partie des regular_interpolations, y compris les jetons pour toutes les interpolated_Regular_String_expressions contenues dans ces interpolations. |
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End | La reconnaissance de ces trois règles suit celle des règles correspondantes ci-dessus, chaque règle de grammaire régulière mentionnée étant remplacée par la règle verbatim correspondante. |
Remarque : les règles ci-dessus sont contextuelles car leurs définitions chevauchent celles d’autres tokens dans le langage. fin de la remarque
Remarque : la grammaire ci-dessus n’est pas prête pour ANTLR en raison des règles lexicales contextuelles. Comme avec d’autres générateurs de lexer, ANTLR supporte les règles lexicales contextuelles, par exemple en utilisant ses lexical modes, mais il s’agit d’un détail d’implémentation et donc non inclus dans cette spécification. fin de la remarque
Une interpolated_string_expression est classée comme une valeur. Si elle est immédiatement convertie en System.IFormattable
ou System.FormattableString
par une conversion implicite de chaîne interpolée (§10.2.5), l’expression de chaîne interpolée a ce type. Dans le cas contraire, elle a le type string
.
Remarque : les différences entre les types possibles d’une interpolated_string_expression peuvent être déterminées à partir de la documentation pour
System.String
(§C.2) etSystem.FormattableString
(§C.3). fin de la remarque
Le but d’une interpolation, tant regular_interpolation que verbatim_interpolation, est de formater la valeur de la expression en string
, soit selon le format spécifié par le Regular_Interpolation_Format ou le Verbatim_Interpolation_Format, soit selon un format par défaut pour le type de l’expression. La chaîne formatée est ensuite modifiée par l'interpolation_minimum_width, le cas échéant, pour produire la string
finale à interpoler dans l'interpolated_string_expression.
Remarque: la manière dont le format par défaut pour un type est déterminé est détaillée dans la documentation pour
System.String
(§C.2) etSystem.FormattableString
(§C.3). Les descriptions des formats standard, qui sont identiques pour Regular_Interpolation_Format et Verbatim_Interpolation_Format, peuvent être trouvées dans la documentation pourSystem.IFormattable
(§C.4) et dans d’autres types de la bibliothèque standard (§C). fin de la remarque
Dans une interpolation_minimum_width, l'expression constante doit avoir une conversion implicite en int
. Que le field width soit la valeur absolue de cette constant_expression et que le alignment soit le signe (positif ou négatif) de la valeur de cette constant_expression :
- Si la valeur du field width est inférieure ou égale à la longueur de la chaîne formatée, la chaîne formatée n’est pas modifiée.
- Sinon, la chaîne formatée est complétée par des caractères d’espace blanc de sorte que sa longueur soit égale à field width :
- Si l'alignement est positif, la chaîne formatée est alignée à droite en ajoutant le remplissage,
- Dans le cas contraire, elle est alignée à gauche en ajoutant le remplissage.
La signification globale d’une interpolated_string_expression, y compris le formatage et le remplissage des interpolations ci-dessus, est définie par une conversion de l’expression en un appel de méthode : si le type de l’expression est System.IFormattable
ou System.FormattableString
, cette méthode est System.Runtime.CompilerServices.FormattableStringFactory.Create
(§C.3) et renvoie une valeur de type System.FormattableString
, sinon, le type doit être string
et la méthode est string.Format
(§C.2) qui renvoie une valeur de type string
.
Dans les deux cas, la liste d’arguments de l’appel est composée d’un format string literal avec des format specifications pour chaque interpolation, et d’un argument pour chaque expression correspondant aux spécifications de format.
Le littéral de la chaîne de format est construit comme suit, où N
est le nombre d'interpolations dans l'expression interpolated_string_expression. Le littéral de la chaîne formatée se compose, dans l'ordre, des éléments suivants :
- Les caractères du Interpolated_Regular_String_Start ou du Interpolated_Verbatim_String_Start.
- Caractères du Interpolated_Regular_String_Mid ou du Interpolated_Verbatim_String_Mid, s'il y en a.
- Ensuite, si
N ≥ 1
pour chaque nombreI
allant de0
àN-1
:- une spécification d'espacement :
- Un caractère d'accolade gauche (
{
) - La représentation décimale de
I
- Ensuite, si l'interpolation_interpolation régulière ou verbatim_interpolation correspondante a une interpolation_minimum_width, une virgule (
,
) suivie de la représentation décimale de la valeur de l'expression constante - Les caractères du Regular_Interpolation_Format ou du Verbatim_Interpolation_Format, le cas échéant, de la regular_interpolation ou de la verbatim_interpolation correspondante.
- Un caractère d'accolade droite (
}
)
- Un caractère d'accolade gauche (
- Les caractères du Interpolated_Regular_String_Mid ou du Interpolated_Verbatim_String_Mid suivant immédiatement l'interpolation correspondante, le cas échéant.
- une spécification d'espacement :
- Enfin, les caractères de l'Interpolated_Regular_String_End ou de l'Interpolated_Verbatim_String_End.
Les arguments suivants sont les expression des interpolations, le cas échéant, dans l'ordre.
Lorsqu’une interpolated_string_expression contient plusieurs interpolations, les expressions dans ces interpolations sont évaluées dans l’ordre textuel de gauche à droite.
Exemple :
Cet exemple utilise les fonctionnalités de spécification de format suivantes:
- la spécification de format
X
qui formate des entiers en hexadécimal majuscule, - le format par défaut pour une valeur
string
est la valeur elle-même, - les valeurs d'alignement positives qui sont justifiées à droite dans la limite de la largeur de champ minimale spécifiée,
- les valeurs d'alignement négatives qui sont justifiées à gauche dans la largeur de champ minimale spécifiée,
- les constantes définies pour l'interpolation_minimum_width, et
- que
{{
et}}
sont respectivement formatés comme{
et}
.
Soit :
string text = "red";
int number = 14;
const int width = -4;
Ensuite :
Expression de la chaîne interpolée | équivalent à string |
Valeur |
---|---|---|
$"{text}" |
string.Format("{0}", text) |
"red" |
$"{{text}}" |
string.Format("{{text}}) |
"{text}" |
$"{ text , 4 }" |
string.Format("{0,4}", text) |
" red" |
$"{ text , width }" |
string.Format("{0,-4}", text) |
"red " |
$"{number:X}" |
string.Format("{0:X}", number) |
"E" |
$"{text + '?'} {number % 3}" |
string.Format("{0} {1}", text + '?', number % 3) |
"red? 2" |
$"{text + $"[{number}]"}" |
string.Format("{0}", text + string.Format("[{0}]", number)) |
"red[14]" |
$"{(number==0?"Zero":"Non-zero")}" |
string.Format("{0}", (number==0?"Zero":"Non-zero")) |
"Non-zero" |
exemple final
12.8.4 Noms simples
Un simple_name se compose d’un identificateur, éventuellement suivi d’une liste d’arguments de type:
simple_name
: identifier type_argument_list?
;
Un simple_name est soit de la forme I
, soit de la forme I<A₁, ..., Aₑ>
, où I
est un identifiant unique et I<A₁, ..., Aₑ>
un type_argument_list facultatif. Lorsqu’aucune type_argument_list n’est spécifiée, considérez e
comme étant égal à zéro. Le simple_name est évalué et classé comme suit :
- Si
e
est égal à zéro et que le simple_name apparaît dans un espace de déclaration de variable locale (§7.3) qui contient directement une variable locale, un paramètre ou une constante nomméeI
, alors le simple_name se réfère à cette variable locale, ce paramètre ou cette constante et est classé comme une variable ou une valeur. - Si
e
est égal à zéro et que le simple_name apparaît dans une déclaration de méthode générique mais en dehors des attributes de sa method_declaration, et si cette déclaration inclut un paramètre de type nomméI
, alors le simple_name se réfère à ce paramètre de type. - Sinon, pour chaque type d’instance
T
(§15.3.2), en commençant par le type d’instance de la déclaration de type englobante immédiate et en continuant avec le type d’instance de chaque déclaration de classe ou struct englobante (le cas échéant) :- Si
e
est égal à zéro et que la déclaration deT
inclut un paramètre de type nomméI
, alors le simple_name se réfère à ce paramètre de type. - Dans le cas contraire, si une recherche de membre (§12.5) de
I
dansT
avec arguments de typee
donne une correspondance :- Si
T
est le type d’instance du type de classe ou de struct immédiatement englobant et que la recherche identifie une ou plusieurs méthodes, le résultat est un groupe de méthodes avec une expression d’instance associée àthis
. Si une liste d’arguments de type a été spécifiée, elle est utilisée pour appeler une méthode générique (§12.8.10.2). - Sinon, si
T
est le type d’instance de la classe ou du struct englobant immédiat, si la recherche identifie un membre d’instance, et si la référence se produit dans le block d’un constructeur d’instance, d’une méthode d’instance ou d’un accesseur d’instance (§12.2.1), le résultat est identique à un accès membre (§12.8.7) de la formethis.I
. Cela ne peut se produire que lorsquee
est égal à zéro. - Sinon, le résultat est identique à un accès membre (§12.8.7) de la forme
T.I
ouT.I<A₁, ..., Aₑ>
.
- Si
- Si
- Sinon, pour chaque espace de noms
N
, en commençant par l’espace de noms dans lequel apparaît le simple_name, en continuant avec chaque espace de noms englobant (le cas échéant) et en terminant par l’espace de noms global, les étapes suivantes sont évaluées jusqu’à ce qu’une entité soit trouvée :- Si
e
est égal à zéro et queI
est le nom d’un espace de noms dansN
, alors :- Si le lieu où apparaît le simple_name est englobé par une déclaration d’espace de noms pour
N
et que la déclaration d’espace de noms contient une extern_alias_directive ou une using_alias_directive qui associe le nomI
à un espace de noms ou un type, alors le simple_name est ambigu et une erreur de compilation survient. - Sinon, le simple_name se réfère à l’espace de noms nommé
I
dansN
.
- Si le lieu où apparaît le simple_name est englobé par une déclaration d’espace de noms pour
- Sinon, si
N
contient un type accessible portant le nomI
et des paramètres de typee
, alors :- Si
e
est égal à zéro et que le lieu où apparaît le simple_name est englobé par une déclaration d’espace de noms pourN
et que cette déclaration d’espace de noms contient une extern_alias_directive ou une using_alias_directive qui associe le nomI
à un espace de noms ou un type, alors le simple_name est ambigu et une erreur de compilation survient. - Sinon, le namespace_or_type_name se réfère au type construit avec les arguments de type donnés.
- Si
- Sinon, si l'emplacement où se trouve le simple_name est entouré d'une déclaration d'espace de noms pour
N
:- Si
e
est égal à zéro et que la déclaration d’espace de noms contient une extern_alias_directive ou une using_alias_directive qui associe le nomI
à un espace de noms ou un type importé, alors le simple_name se réfère à cet espace de noms ou type. - Sinon, si les espaces de noms importés par les using_namespace_directives de la déclaration d'espace de noms contiennent exactement un type ayant le nom
I
et des paramètres de typee
, alors le simple_name fait référence à ce type construit avec les arguments de type donnés. - Dans le cas contraire, si les espaces noms importés par les using_namespace_directives de la déclaration d'espace noms contiennent plus d'un type portant le nom
I
et des paramètres de typee
, le simple_name est ambigu et une erreur de compilation se produit.
- Si
Remarque : cette étape est exactement parallèle à l’étape correspondante dans le traitement d’un namespace_or_type_name (§7.8). fin de la remarque
- Si
- Sinon, si
e
est zéro et queI
est l'identificateur_
, le simple_name est un simple_jet, qui est une forme d'expression de déclaration (§12.17). - Sinon, le simple_name est indéfini et une erreur de compilation survient.
12.8.5 Expressions entre parenthèses
Une parenthesized_expression consiste en une expression entourée de parenthèses.
parenthesized_expression
: '(' expression ')'
;
Une expression entre parenthèses est évaluée en effectuant l'évaluation de l’expression dans les parenthèses. Si l’expression entre parenthèses désigne un espace de noms ou un type, une erreur de compilation survient. Sinon, le résultat de la parenthesized_expression est le résultat de l'évaluation de l'expression contenue.
12.8.6 Expression de tuple
Un tuple_expression représente un tuple et consiste en deux ou plusieurs expressions séparées par des virgules et optionnellement nommées, entre parenthèses. Une deconstruction_expression est une syntaxe abrégée pour un tuple contenant des expressions de déclaration implicitement typées.
tuple_expression
: '(' tuple_element (',' tuple_element)+ ')'
| deconstruction_expression
;
tuple_element
: (identifier ':')? expression
;
deconstruction_expression
: 'var' deconstruction_tuple
;
deconstruction_tuple
: '(' deconstruction_element (',' deconstruction_element)+ ')'
;
deconstruction_element
: deconstruction_tuple
| identifier
;
Une tuple_expression est classée comme un tuple.
Une deconstruction_expressionvar (e1, ..., en)
est une syntaxe abrégée pour le tuple_expression(var e1, ..., var en)
et suit le même comportement. Cela s'applique récursivement à tous les deconstruction_tuples de déconstruction imbriqués dans la deconstruction_expression. Chaque identificateur imbriqué dans une deconstruction_expression introduit ainsi une expression de déclaration (§12.17). Par conséquent, une deconstruction_expression ne peut apparaître que du côté gauche d'une affectation simple.
Une expression de tuple a un type si et seulement si chacune de ses expressions éléments Ei
a un type Ti
. Le type doit être un type de tuple de même arité que l'expression de tuple, où chaque élément est donné par ce qui suit :
- Si l’élément du tuple à la position correspondante porte le nom
Ni
, alors l’élément du type tuple doit êtreTi Ni
. - Sinon, si
Ei
est de la formeNi
ouE.Ni
ouE?.Ni
, l’élément de type tuple doit êtreTi Ni
, sauf si l’une des conditions suivantes s'applique :- Un autre élément de l’expression de tuple porte le nom
Ni
, ou - Un autre élément de tuple sans nom a l'expression d'un élément de tuple sous la forme
Ni
ouE.Ni
ouE?.Ni
, ou -
Ni
est de la formeItemX
, oùX
est une séquence de chiffres décimaux non initiés par0
qui pourrait représenter la position d’un élément du tuple, etX
ne représente pas la position de l’élément.
- Un autre élément de l’expression de tuple porte le nom
- Sinon, l’élément du type tuple doit être
Ti
.
Une expression de tuple est évaluée en évaluant chacune de ses expressions éléments de gauche à droite.
Une valeur tuple peut être obtenue à partir d’une expression de tuple en la convertissant en un type tuple (§10.2.13), en la reclassant en tant que valeur (§12.2.2) ou en faisant de cette valeur la cible d’une affectation de déconstruction (§12.21.2).
Exemple :
(int i, string) t1 = (i: 1, "One"); (long l, string) t2 = (l: 2, null); var t3 = (i: 3, "Three"); // (int i, string) var t4 = (i: 4, null); // Error: no type
Dans cet exemple, les quatre expressions de tuple sont valides. Les deux premières,
t1
ett2
, n'utilisent pas le type de l'expression de tuple, mais appliquent une conversion implicite de tuple. Dans le cas det2
, la conversion implicite de tuple repose sur les conversions implicites de2
àlong
et denull
àstring
. La troisième expression de tuple a un type(int i, string)
et peut donc être reclassée en tant que valeur de ce type. La déclaration det4
, en revanche, est une erreur : l’expression de tuple n’a pas de type car son deuxième élément n’a pas de type.if ((x, y).Equals((1, 2))) { ... };
Cet exemple montre que les n-uplets peuvent parfois conduire à de multiples couches de parenthèses, en particulier lorsque l'expression de n-uplet est le seul argument d'une invocation de méthode.
exemple final
12.8.7 Accès aux membres
12.8.7.1 Général
Un member_access se compose d'une primary_expression, d'un predefined_type ou d'un qualified_alias_member, suivi d'un jeton « .
», suivi d'un identifiant, suivi éventuellement d'une type_argument_list.
member_access
: primary_expression '.' identifier type_argument_list?
| predefined_type '.' identifier type_argument_list?
| qualified_alias_member '.' identifier type_argument_list?
;
predefined_type
: 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int'
| 'long' | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong'
| 'ushort'
;
La production qualified_alias_member est définie au §14.8.
Un member_access est soit de la forme E.I
, soit de la forme E.I<A₁, ..., Aₑ>
, où E
est une primary_expression, un predefined_type ou un qualified_alias_memberI
, est un identifiant unique, et <A₁, ..., Aₑ>
est une type_argument_list facultative. Lorsqu’aucune type_argument_list n’est spécifiée, considérez e
comme étant égal à zéro.
Un member_access avec une primary_expression de type dynamic
est dynamiquement lié (§12.3.3). Dans ce cas, le compilateur classe l’accès membre comme un accès à une propriété de type dynamic
. Les règles ci-dessous pour déterminer le sens du member_access sont alors appliquées à l’exécution, en utilisant le type à l’exécution au lieu du type à la compilation de la primary_expression. Si cette classification à l'exécution conduit à un groupe de méthodes, alors l'accès membre doit être la primary_expression d'une invocation_expression.
Le member_access est évalué et classé comme suit :
- Si
e
est égal à zéro et queE
est un espace de noms et queE
contient un espace de noms imbriqué portant le nomI
, alors le résultat est cet espace de noms. - Sinon, si
E
est un espace de noms et queE
contient un type accessible portant le nomI
etK
paramètres de type, alors le résultat est ce type construit avec les arguments de type donnés. - Si
E
est classé comme un type, siE
n’est pas un paramètre de type, et si une recherche de membre (§12.5) deI
dansE
avecK
paramètres de type produit une correspondance, alorsE.I
est évalué et classé comme suit :Remarque : lorsque le résultat d’une telle recherche de membre est un groupe de méthodes et que
K
est égal à zéro, le groupe de méthodes peut contenir des méthodes avec des paramètres de type. Cela permet que ces méthodes soient prises en compte pour l’inférence des arguments de type. fin de la remarque- Si
I
identifie un type, alors le résultat est ce type construit avec les arguments de type fournis. - Si
I
identifie une ou plusieurs méthodes, alors le résultat est un groupe de méthodes sans expression d’instance associée. - Si
I
identifie une propriété statique, alors le résultat est un accès à une propriété sans expression d’instance associée. - Si
I
identifie un champ statique :- Si le champ est en lecture seule et que la référence se produit en dehors du constructeur statique de la classe ou de la structure dans lequel le champ est déclaré, alors le résultat est une valeur, à savoir la valeur du champ statique
I
dansE
. - Sinon, le résultat est une variable, à savoir le champ statique
I
dansE
.
- Si le champ est en lecture seule et que la référence se produit en dehors du constructeur statique de la classe ou de la structure dans lequel le champ est déclaré, alors le résultat est une valeur, à savoir la valeur du champ statique
- Si
I
identifie un événement statique :- Si la référence se produit au sein de la classe ou du struct dans laquelle l’événement est déclaré, et si l’événement a été déclaré sans event_accessor_declarations (§15.8.1), alors
E.I
est traité exactement comme siI
était un champ statique. - Sinon, le résultat est un accès à un événement sans expression d’instance associée.
- Si la référence se produit au sein de la classe ou du struct dans laquelle l’événement est déclaré, et si l’événement a été déclaré sans event_accessor_declarations (§15.8.1), alors
- Si
I
identifie une constante, alors le résultat est une valeur, à savoir la valeur de cette constante. - Si
I
identifie un membre d’énumération, alors le résultat est une valeur, à savoir la valeur de ce membre d’énumération. - Sinon,
E.I
est une référence de membre invalide, et une erreur de compilation survient.
- Si
- Si
E
est un accès à une propriété, un accès à un indexeur, une variable ou une valeur, dont le type estT
, et qu'une recherche de membre (§12.5) deI
dansT
avec des arguments de typeK
produit une correspondance, alorsE.I
est évalué et classé comme suit :- Tout d’abord, si
E
est un accès à une propriété ou à un indexeur, alors la valeur de l’accès à la propriété ou à l’indexeur est obtenue (§12.2.2) et E est reclassé en tant que valeur. - Si
I
identifie une ou plusieurs méthodes, alors le résultat est un groupe de méthodes avec une expression d’instance associée deE
. - Si
I
identifie une propriété d’instance, alors le résultat est un accès à une propriété avec une expression d’instance associée deE
et un type associé qui est le type de la propriété. SiT
est un type de classe, le type associé est sélectionné à partir de la première déclaration ou redéfinition de la propriété trouvée en commençant parT
et la recherche à travers ses classes de base. - Si
T
est un class_type et queI
identifie un champ d’instance de ce class_type :- Si la valeur de
E
estnull
, unSystem.NullReferenceException
est lancé. - Sinon, si le champ est en lecture seule et que la référence se produit en dehors d’un constructeur d’instance de la classe dans laquelle le champ est déclaré, le résultat est une valeur, à savoir la valeur du champ
I
dans l’objet référencé parE
. - Sinon, le résultat est une variable, à savoir le champ
I
dans l’objet référencé parE
.
- Si la valeur de
- Si
T
est un struct_type et queI
identifie un champ d’instance de ce struct_type :- Si
E
est une valeur ou si le champ est en lecture seule et que la référence se produit en dehors d’un constructeur d’instance du struct dans lequel le champ est déclaré, alors le résultat est la valeur du champI
dans l’instance de struct donnée parE
. - Sinon, le résultat est une variable, à savoir le champ
I
dans l’instance de struct donnée parE
.
- Si
- Si
I
identifie un événement d’instance :- Si la référence se produit au sein de la classe ou du struct dans lequel l’événement est déclaré, et si l’événement a été déclaré sans event_accessor_declarations (§15.8.1), et que la référence ne se produit pas comme côté gauche de l’opérateur
a +=
ou-=
, alorsE.I
est traité exactement comme siI
était un champ d’instance. - Sinon, le résultat est un accès à l'événement avec une expression d'instance associée de
E
.
- Si la référence se produit au sein de la classe ou du struct dans lequel l’événement est déclaré, et si l’événement a été déclaré sans event_accessor_declarations (§15.8.1), et que la référence ne se produit pas comme côté gauche de l’opérateur
- Tout d’abord, si
- Sinon, une tentative est faite de traiter
E.I
comme un appel de méthode d’extension (§12.8.10.3). En cas d'échec,E.I
est une référence de membre non valide et une erreur de liaison se produit.
12.8.7.2 Noms simples et noms de types identiques
Dans un accès membre de la forme E.I
, si E
est un identificateur unique, et si le sens de E
en tant que simple_name (§12.8.4) est une constante, un champ, une propriété, une variable locale ou un paramètre ayant le même type que le sens de E
en tant que type_name (§7.8.1), alors les deux sens possibles de E
sont autorisés. La recherche de membre de E.I
n'est jamais ambiguë, puisque I
est nécessairement un membre du type E
dans les deux cas. En d’autres termes, la règle autorise simplement l’accès aux membres statiques et aux types imbriqués de E
, où une erreur à la compilation se serait autrement produite.
Exemple :
struct Color { public static readonly Color White = new Color(...); public static readonly Color Black = new Color(...); public Color Complement() => new Color(...); } class A { public «Color» Color; // Field Color of type Color void F() { Color = «Color».Black; // Refers to Color.Black static member Color = Color.Complement(); // Invokes Complement() on Color field } static void G() { «Color» c = «Color».White; // Refers to Color.White static member } }
À des fins explicatives uniquement, dans la classe
A
, les occurrences de l’identificateurColor
qui font référence au typeColor
sont délimitées par«...»
, et celles qui font référence au champColor
ne le sont pas.exemple final
12.8.8 Accès membre conditionnel Null
Un null_conditional_member_access est une version conditionnelle de member_access (§12.8.7) et cela constitue une erreur au moment de la liaison si le type de résultat est void
. Pour une expression conditionnelle Null dont le type de résultat peut être void
, voir (§12.8.11).
Un null_conditional_member_access se compose d'une primary_expression suivie des deux jetons « ?
» et « .
», suivie d'un identificateur avec une liste facultative de type_argument_list, suivie de zéro ou plusieurs dependent_accesses dont chacun peut être précédé d'un null_forgiving_operator.
null_conditional_member_access
: primary_expression '?' '.' identifier type_argument_list?
(null_forgiving_operator? dependent_access)*
;
dependent_access
: '.' identifier type_argument_list? // member access
| '[' argument_list ']' // element access
| '(' argument_list? ')' // invocation
;
null_conditional_projection_initializer
: primary_expression '?' '.' identifier type_argument_list?
;
Une expression null_conditional_member_access E
est de la forme P?.A
. Le sens de E
est déterminé comme suit :
Si le type de
P
est un type valeur pouvant être Null :Soit
T
le type deP.Value.A
.Si
T
est un paramètre de type qui n’est pas connu pour être soit un type de référence, soit un type valeur pouvant être Null, une erreur de compilation survient.Si
T
est un type valeur pouvant être Null alors le type deE
estT?
, et le sens deE
est le même que celui de :((object)P == null) ? (T?)null : P.Value.A
Sauf que
P
est évalué une seule fois.Sinon, le type de
E
estT
, et le sens deE
est le même que celui de :((object)P == null) ? (T)null : P.Value.A
Sauf que
P
est évalué une seule fois.
Sinon :
Soit
T
le type de l’expressionP.A
.Si
T
est un paramètre de type qui n’est pas connu pour être soit un type de référence, soit un type valeur pouvant être Null, une erreur de compilation survient.Si
T
est un type valeur pouvant être Null alors le type deE
estT?
, et le sens deE
est le même que celui de :((object)P == null) ? (T?)null : P.A
Sauf que
P
est évalué une seule fois.Sinon, le type de
E
estT
, et le sens deE
est le même que celui de :((object)P == null) ? (T)null : P.A
Sauf que
P
est évalué une seule fois.
Remarque : dans une expression de la forme :
P?.A₀?.A₁
alors si
P
est évalué ànull
, niA₀
niA₁
ne sont évalués. Il en va de même si une expression est une séquence d'opérations null_conditional_member_access ou null_conditional_element_access§12.8.13.fin de la remarque
Un null_conditional_projection_initializer est une restriction de null_conditional_member_access et a la même sémantique. Cela ne se produit que comme initialiseur de projection dans une expression de création d’objet anonyme (§12.8.17.7).
12.8.9 Expressions d'annulation de nullité
12.8.9.1 Général
La valeur, le type, la classification (§12.2) et le safe-context (§16.4.12) d'une expression à compensation nulle sont la valeur, le type, la classification et le safe-context de sa primary_expression.
null_forgiving_expression
: primary_expression null_forgiving_operator
;
null_forgiving_operator
: '!'
;
Note : Les opérateurs de négation postfixe et de négation logique préfixe (§12.9.4), bien que représentés par le même jeton lexical (!
), sont distincts. Seul ce dernier peut être remplacé (§15.10), la définition de l'opérateur null-forgiving est fixe. fin de la remarque
Il s'agit d'une erreur à la compilation d'appliquer l'opérateur null-forgiving plus d'une fois à la même expression, les parenthèses intermédiaires étant sans conséquence.
Exemple : les exemples suivants sont tous invalides :
var p = q!!; // error: applying null_forgiving_operator more than once var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)
exemple final
Le reste de cette sous-clause et les sous-clauses sœurs suivantes sont conditionnellement normatifs.
Un compilateur effectuant une analyse statique de l’état nul (§8.9.5) doit se conformer à la spécification suivante.
L'opérateur null-forgiving est une pseudo-opération au moment de la compilation utilisée pour fournir des informations à l'analyse d'état nul statique d'un compilateur. Elle a deux utilisations : annuler la détermination par le compilateur qu’une expression peut être Null ; et annuler l’émission par le compilateur d’un avertissement lié à la nullabilité.
Appliquer l’opérateur d’indulgence à une expression pour laquelle l’analyse statique de l’état nul du compilateur ne produit aucun avertissement n’est pas une erreur.
12.8.9.2 Remplacement d'une détermination « maybe null ».
Dans certaines circonstances, l’analyse statique de l’état nul d’un compilateur peut déterminer qu’une expression a l’état Null peut être Null et émettre un avertissement diagnostique lorsque d’autres informations indiquent que l’expression ne peut pas être Null. L’application de l’opérateur null-forgiving à une telle expression informe l’analyse statique d’état de nullité du compilateur que l’état null est dans et pas dans. Cela empêche non seulement l’avertissement de diagnostic mais peut également influencer toute analyse en cours.
Exemple : considérez ceci :
#nullable enable public static void M() { Person? p = Find("John"); // returns Person? if (IsValid(p)) { Console.WriteLine($"Found {p!.Name}"); // p can't be null } } public static bool IsValid(Person? person) => person != null && person.Name != null;
Si
IsValid
retournetrue
,p
peut être déréférencé pour accéder à sa propriétéName
, et l’avertissement « déréférencement d’une valeur éventuellement nulle » peut être supprimé à l’aide de!
.exemple final
Exemple : l’opérateur d’indulgence doit être utilisé avec prudence, considérez :
#nullable enable int B(int? x) { int y = (int)x!; // quash warning, throw at runtime if x is null return y; }
Ici, l’opérateur d’indulgence est appliqué à un type valeur et supprime tout avertissement sur
x
. Toutefois, six
estnull
lors de l’exécution, une exception est levée, carnull
ne peut pas être converti enint
.exemple final
12.8.9.3 Remplacement d'autres avertissements d'analyse de nullité
En plus de remplacer les déterminations « maybe null » comme ci-dessus, il peut y avoir d'autres circonstances où l'on souhaite remplacer la détermination de l'analyse statique de l'état nul d'une expression par le compilateur, qui nécessite un ou plusieurs avertissements. Appliquer l’opérateur d’indulgence à une telle expression demande qu’un compilateur n’émette aucun avertissement pour l’expression. En réponse, un compilateur peut choisir de ne pas émettre d’avertissements et peut également modifier son analyse ultérieure.
Exemple : considérez ceci :
#nullable enable public static void Assign(out string? lv, string? rv) { lv = rv; } public string M(string? t) { string s; Assign(out s!, t ?? "«argument was null»"); return s; }
Les types de paramètres de la méthode
Assign
,lv
&rv
, sontstring?
, aveclv
comme paramètre de sortie, et celle-ci effectue une affectation simple.La méthode
M
passe la variables
, de typestring
, comme paramètre de sortie deAssign
. Le compilateur utilise un avertissement cars
n'est pas une variable nullable. Étant donné que le deuxième argument deAssign
ne peut pas être Null, l’opérateur d’indulgence est utilisé pour supprimer l’avertissement.exemple final
Fin du texte conditionnellement normatif.
12.8.10 Expressions d’appel
12.8.10.1 Général
Une invocation_expression est utilisée pour appeler une méthode.
invocation_expression
: primary_expression '(' argument_list? ')'
;
La primary_expression peut être une expression null_forgiving si et seulement si elle a un delegate_type.
Une invocation_expression est dynamiquement liée (§12.3.3) si au moins l'une des conditions suivantes est remplie :
- La primary_expression a le type compile-time
dynamic
. - Au moins un argument de l'argument_list optionnel est de type compile-time
dynamic
.
Dans ce cas, le compilateur classe la invocation_expression comme une valeur de type dynamic
. Les règles ci-dessous visant à déterminer la signification de l'invocation_expression sont alors appliquées au moment de l'exécution, en utilisant le type au moment de l'exécution au lieu du type au moment de la compilation de ceux de la primary_expression et des arguments qui ont le type au moment de la compilation dynamic
. Si la primary_expression n’a pas le type de compilation dynamic
, alors l’appel de méthode subit une vérification limitée à la compilation comme décrit dans §12.6.5.
L' expression_primaire d'une expression_d'appel doit être un ensemble de méthodes ou une valeur d'un type_délégué. Si la primary_expression est un groupe de méthodes, la invocation_expression est un appel de méthode (§12.8.10.2). Si la primary_expression est une valeur d’un delegate_type, la invocation_expression est un appel de délégué (§12.8.10.4). Si la primary_expression n'est ni un groupe de méthodes ni une valeur d'un delegate_type, une erreur de liaison se produit.
La argument_list facultative (§12.6.2) fournit des valeurs ou des références de variables pour les paramètres de la méthode.
Le résultat de l'évaluation d'une invocation_expression est classé comme suit :
- Si la invocation_expression appelle une méthode renvoyant aucune valeur (§15.6.1) ou un délégué renvoyant aucune valeur, le résultat est rien. Une expression classée comme rien est permise uniquement dans le contexte d’une statement_expression (§13.7) ou en tant que corps d’une lambda_expression (§12.19). Dans le cas contraire, une erreur de temps de liaison se produit.
- Sinon, si l' invocation_expression appelle une méthode qui renvoie par référence (§15.6.1) ou un délégué qui renvoie par référence, le résultat est une variable avec un type associé au type de retour de la méthode ou du délégué. Si l’appel concerne une méthode d’instance, et que le récepteur est de type classe
T
, le type associé est choisi à partir de la première déclaration ou redéfinition de la méthode trouvée en partant deT
et en parcourant ses classes de base. - Sinon, le invocation_expression invoque une méthode de retour par valeur (§15.6.1) ou un délégué de retour par valeur, et le résultat est une valeur, avec un type associé correspondant au type de retour de la méthode ou du délégué. Si l’appel concerne une méthode d’instance, et que le récepteur est de type classe
T
, le type associé est choisi à partir de la première déclaration ou redéfinition de la méthode trouvée en partant deT
et en parcourant ses classes de base.
12.8.10.2 Appels de méthode
Pour un appel de méthode, la primary_expression de invocation_expression doit être un groupe de méthodes. Le groupe de méthodes identifie la méthode unique à appeler ou l’ensemble des méthodes surchargées parmi lesquelles choisir une méthode spécifique à appeler. Dans ce dernier cas, la détermination de la méthode spécifique à appeler repose sur le contexte fourni par les types des arguments dans la argument_list.
Le traitement contraignant d'une invocation de méthode de la forme M(A)
, où M
est un groupe de méthodes (comprenant éventuellement un type_argument_list) et A
est un argument_list facultatif, se compose des étapes suivantes :
- L'ensemble des méthodes candidates pour l'invocation de la méthode est construit. Pour chaque méthode
F
associée au groupe de méthodesM
:- Si
F
n’est pas générique,F
est un candidat lorsque :-
M
n’a pas de liste d’arguments de type, et -
F
est applicable par rapport àA
(§12.6.4.2).
-
- Si
F
est générique et queM
n’a pas de liste d’arguments de type,F
est un candidat lorsque :- L’inférence de type (§12.6.3) réussit, en déduisant une liste d’arguments de type pour l’appel, et
- Une fois les arguments de type déduits substitués aux paramètres de type correspondants de la méthode, tous les types construits dans la liste de paramètres de
F
satisfont à leurs contraintes (§8.4.5), et la liste de paramètres deF
est applicable par rapport àA
(§12.6.4.2).
- Si
F
est générique et queM
inclut une liste d’arguments de type,F
est un candidat lorsque :-
F
possède le même nombre de paramètres de type de méthode que celui fourni dans la liste d’arguments de type, et - Une fois les arguments de type substitués aux paramètres de type correspondants de la méthode, tous les types construits dans la liste de paramètres de
F
satisfont à leurs contraintes (§8.4.5), et la liste de paramètres deF
est applicable par rapport àA
(§12.6.4.2).
-
- Si
- L’ensemble des méthodes candidates est réduit pour ne contenir que des méthodes des types les plus dérivés : pour chaque méthode
C.F
de l’ensemble, oùC
est le type dans lequel la méthodeF
est déclarée, toutes les méthodes déclarées dans un type de base deC
sont supprimées de l’ensemble. De plus, siC
est un type classe autre queobject
, toutes les méthodes déclarées dans un type interface sont supprimées de l’ensemble.Remarque : cette dernière règle n’a d’effet que lorsque le groupe de méthodes est le résultat d’une recherche de membre sur un paramètre de type ayant une classe de base effective autre que
object
et un ensemble d’interfaces effectif non vide. fin de la remarque - Si l’ensemble résultant des méthodes candidates est vide, alors un traitement ultérieur selon les étapes suivantes est abandonné, et à la place une tentative de traiter l’appel comme un appel de méthode d’extension (§12.8.10.3) est effectuée. Si cela échoue, alors aucune méthode applicable n’existe, et une erreur de liaison survient.
- La meilleure méthode de l’ensemble des méthodes candidates est identifiée en utilisant les règles de résolution de surcharge de §12.6.4. Si une seule meilleure méthode ne peut être identifiée, l’appel de la méthode est ambiguë, et une erreur de liaison survient. Lors de la résolution de surcharge, les paramètres d’une méthode générique sont considérés après avoir substitué les arguments de type (fournis ou déduits) aux paramètres de type correspondants de la méthode.
Une fois qu'une méthode a été sélectionnée et validée au niveau de la liaison par les étapes ci-dessus, l'invocation réelle au niveau de l'exécution est traitée conformément aux règles d'invocation des membres de fonction décrites au point 12.6.6.
Remarque : l’effet intuitif des règles de résolution décrites ci-dessus est le suivant : Pour localiser la méthode particulière appelée par un appel de méthode, commencez par le type indiqué par l’appel de méthode et remontez la chaîne d’héritage jusqu’à ce qu’au moins une déclaration de méthode applicable, accessible et non redéfinie soit trouvée. Ensuite, effectuez l’inférence de type et la résolution de surcharge sur l’ensemble de méthodes applicables, accessibles et non substituées déclarées dans ce type et appelez la méthode ainsi sélectionnée. Si aucune méthode n’a été trouvée, essayez plutôt de traiter l’appel comme un appel de méthode d’extension. fin de la remarque
12.8.10.3 Appels de méthode d’extension
Dans une invocation de méthode (§12.6.6.2) de l'une des formes suivantes
«expr» . «identifier» ( )
«expr» . «identifier» ( «args» )
«expr» . «identifier» < «typeargs» > ( )
«expr» . «identifier» < «typeargs» > ( «args» )
si le traitement normal de l’invocation ne trouve aucune méthode applicable, une tentative est effectuée pour traiter ce dernier comme une invocation de méthode d’extension. Si « expr » ou l’un quelconque des « args » a le type de compilation dynamic
, les méthodes d’extension ne s’appliqueront pas.
L’objectif est de trouver le meilleur type_nameC
, afin que l’appel statique de méthode correspondante puisse avoir lieu :
C . «identifier» ( «expr» )
C . «identifier» ( «expr» , «args» )
C . «identifier» < «typeargs» > ( «expr» )
C . «identifier» < «typeargs» > ( «expr» , «args» )
Une méthode d’extension Cᵢ.Mₑ
est éligible si :
-
Cᵢ
est une classe non générique, non imbriquée - Le nom de
Mₑ
est identifiant -
Mₑ
est accessible et applicable lorsqu’on l’applique aux arguments en tant que méthode statique, comme indiqué ci-dessus - Une conversion implicite d'identité, de référence ou de boîte existe entre expr et le type du premier paramètre de
Mₑ
.
La recherche de C
se déroule comme ceci :
- En commençant par la déclaration d’espace de noms englobant la plus proche, en poursuivant avec chaque déclaration d’espace de noms englobant, et en terminant par l’unité de compilation contenant, des tentatives successives sont effectuées pour trouver un ensemble candidat de méthodes d’extension :
- Si l’espace de noms ou l’unité de compilation donnée contient directement des déclarations de types non génériques
Cᵢ
avec des méthodes d’extension éligiblesMₑ
, alors l’ensemble de ces méthodes d’extension constitue l’ensemble candidat. - Si des espaces de noms sont importés à l’aide de directives d’espace de noms dans l’espace de noms donné ou dans l’unité de compilation, et qu'ils contiennent directement des déclarations de type non génériques
Cᵢ
avec des méthodes d’extension éligiblesMₑ
, alors ces méthodes d'extension forment l'ensemble des candidats potentiels.
- Si l’espace de noms ou l’unité de compilation donnée contient directement des déclarations de types non génériques
- Si aucun ensemble candidat n’est trouvé dans une déclaration d’espace de noms englobante ou dans une unité de compilation, une erreur de compilation survient.
- Sinon, la résolution de surcharge est appliquée à l'ensemble de candidats comme décrit au §12.6.4. Si aucune méthode optimale unique n’est trouvée, une erreur de compilation survient.
-
C
est le type dans lequel la meilleure méthode est déclarée en tant que méthode d’extension.
En utilisant C
comme cible, l’appel de méthode est ensuite traité comme un appel de méthode statique (§12.6.6).
Remarque: contrairement à un appel de méthode d’instance, aucune exception n’est levée lorsque expr prend la valeur d’une référence nulle. Au lieu de cela, cette valeur
null
est passée à la méthode d’extension, comme cela serait via un appel de méthode statique standard. Il appartient à l’implémentation de la méthode d’extension de décider comment répondre à cet appel. fin de la remarque
Les règles précédentes signifient que les méthodes d’instance priment sur les méthodes d’extension, que les méthodes d’extension disponibles dans les déclarations d’espaces de noms intérieures priment sur celles disponibles dans les déclarations d’espaces de noms extérieures, et que les méthodes d’extension déclarées directement dans un espace de noms priment sur les méthodes d’extension importées dans ce même espace de noms via une directive using namespace.
Exemple :
public static class E { public static void F(this object obj, int i) { } public static void F(this object obj, string s) { } } class A { } class B { public void F(int i) { } } class C { public void F(object obj) { } } class X { static void Test(A a, B b, C c) { a.F(1); // E.F(object, int) a.F("hello"); // E.F(object, string) b.F(1); // B.F(int) b.F("hello"); // E.F(object, string) c.F(1); // C.F(object) c.F("hello"); // C.F(object) } }
Dans l’exemple, la méthode de
B
prend le pas sur la première méthode d’extension, et la méthode deC
prend le pas sur les deux méthodes d’extension.public static class C { public static void F(this int i) => Console.WriteLine($"C.F({i})"); public static void G(this int i) => Console.WriteLine($"C.G({i})"); public static void H(this int i) => Console.WriteLine($"C.H({i})"); } namespace N1 { public static class D { public static void F(this int i) => Console.WriteLine($"D.F({i})"); public static void G(this int i) => Console.WriteLine($"D.G({i})"); } } namespace N2 { using N1; public static class E { public static void F(this int i) => Console.WriteLine($"E.F({i})"); } class Test { static void Main(string[] args) { 1.F(); 2.G(); 3.H(); } } }
La sortie de cet exemple est :
E.F(1) D.G(2) C.H(3)
D.G
prend le pas surC.G
, etE.F
prend le pas surD.F
etC.F
.exemple final
12.8.10.4 Invocations de délégués
Pour une invocation de délégué, la primary_expression de l'invocation_expression doit être une valeur d'un delegate_type. En outre, en considérant le délégation en tant que membre fonctionnel avec la même liste de paramètres que la délégation, la délégation doit être applicable (§12.6.4.2) par rapport à la liste_d'arguments de l' expression_d'invocation.
Le traitement à l'exécution d'une invocation de délégué de la forme D(A)
, où D
est une primary_expression d'un delegate_type et A
est une argument_list facultative, se compose des étapes suivantes :
D
est évalué. Si cette évaluation provoque une exception, aucune étape supplémentaire n’est exécutée.- La liste d’arguments
A
est évaluée. Si cette évaluation provoque une exception, aucune étape supplémentaire n’est exécutée. - La valeur de
D
est vérifiée pour être valide. Si la valeur deD
estnull
, unSystem.NullReferenceException
est lancé et aucune autre étape n'est exécutée. - Sinon,
D
est une référence à une instance de délégué. Les appelss de membres fonctionnels (§12.6.6) sont effectuées sur chacune des entités appelables dans la liste d’appel du délégué. Pour les entités appelables composées d’une instance et d’une méthode d’instance, l’instance pour l’appel est celle contenue dans l’entité appelable.
Veuillez consulter la section §20.6 pour plus de détails sur les listes d’appel multiples sans paramètres.
12.8.11 Expression d'invocation conditionnelle nulle
Une expression null_conditional_invocation_expression est syntaxiquement soit un accès null_conditional_member_access (§12.8.8) soit un accès null_conditional_element_access (§12.8.13) où l'accès dépendant final est une expression d'invocation (§12.8.10).
Une null_conditional_invocation_expression se produit dans le contexte d’une statement_expression (§13.7), d’un anonymous_function_body (§12.19.1), ou d’un method_body (§15.6.1).
Contrairement à la null_conditional_member_access ou à la null_conditional_element_access syntaxiquement équivalentes, une null_conditional_invocation_expression peut être classée comme rien.
null_conditional_invocation_expression
: null_conditional_member_access null_forgiving_operator? '(' argument_list? ')'
| null_conditional_element_access null_forgiving_operator? '(' argument_list? ')'
;
L'opérateur facultatif null_forgiving_operator peut être inclus si et seulement si le null_conditional_member_access ou le null_conditional_element_access a un delegate_type .
Une expression null_conditional_invocation_expression E
est de la formeP?A
; où A
est le reste du null_conditional_member_access ou null_conditional_element_access syntaxiquement équivalent, A
commencera donc par .
ou [
. Soit PA
la concaténation de P
et A
.
Lorsque E
apparaît en tant que statement_expression, la signification de E
est la même que celle de la statement :
if ((object)P != null) PA
sauf que P
est évalué une seule fois.
Lorsque E
apparaît en tant que anonymous_function_body ou method_body, la signification de E
dépend de sa classification :
Si
E
est classé comme rien, alors sa signification est identique à celle du block :{ if ((object)P != null) PA; }
sauf que
P
est évalué une seule fois.Sinon, la signification de
E
est la même que celle du block :{ return E; }
et, à son tour, la signification de ce block dépend du fait que
E
soit équivalent à une null_conditional_member_access du point du vue de la syntaxe (§12.8.8) ou une null_conditional_element_access (§12.8.13).
12.8.12 Accès aux éléments
12.8.12.1 Général
Un accès aux éléments se compose d'une expression primary_no_array_creation_expression, suivie d'un jeton « [
», suivi d'une argument_list, suivie d'un jeton « ]
». La argument_list se compose d’un ou plusieurs argument, séparés par des virgules.
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;
La argument_list d’un element_access ne peut pas contenir d’arguments out
ou ref
.
Une element_access est liée dynamiquement (§12.3.3) si au moins l'une des conditions suivantes est remplie :
- L'expression primary_no_array_creation_expression est de type compilatoire
dynamic
. - Au moins une expression de l'argument_list a le type compile-time
dynamic
et l'expression primary_no_array_creation_expression n'a pas de type tableau.
Dans ce cas, le compilateur classe le element_access comme une valeur de type dynamic
. Les règles ci-dessous pour déterminer la signification de l'element_access sont ensuite appliquées au moment de l'exécution, en utilisant le type d'exécution au lieu de ceux des types de compilation des expressions primary_no_array_creation_expression et argument_list qui ont le type de compilation dynamic
. Si le primary_no_array_creation_expression n’a pas le type de compilation dynamic
, alors l’accès par élément fait l’objet d’une vérification limitée à la compilation, comme décrit dans §12.6.5.
Si l'expression primary_no_array_creation_expression d'un accès_élément est une valeur d'un type tableau, l'accès_élément est un accès_tableau (§12.8.12.2). Sinon, le primary_no_array_creation_expression doit être une variable ou une valeur d’un type de classe, de struct ou d’interface qui possède un ou plusieurs membres indexeurs, auquel cas le element_access constitue un accès à un indexeur (§12.8.12.3).
12.8.12.2 Accès au tableau
Pour un accès par tableau, l'expression primary_no_array_creation_expression de l'element_access doit être une valeur array_type De plus, la argument_list d’un accès à un tableau ne peut pas contenir d’arguments nommés. Le nombre d’expressions dans la argument_list doit être identique au rang du array_type, et chaque expression doit être de type int
, uint
, long
ou ulong,
ou implicitement convertible en un ou plusieurs de ces types.
Le résultat de l’évaluation d’un accès à un tableau est une variable du type élément du tableau, à savoir l’élément du tableau sélectionné par la(les) valeur(s) des expression(s) dans la argument_list.
Le traitement au moment de l'exécution d'un accès à un tableau de la forme P[A]
, où P
est une expression primary_no_array_creation_expression d'un array_type et A
est une argument_list, se compose des étapes suivantes :
P
est évalué. Si cette évaluation provoque une exception, aucune étape supplémentaire n’est exécutée.- Les expressions d’index de la argument_list sont évaluées dans l’ordre, de gauche à droite. Après l'évaluation de chaque expression d'index, une conversion implicite (§10.2) vers l’un des types suivants est effectuée :
int
,uint
,long
,ulong
. Le premier type dans cette liste pour lequel une conversion implicite existe est choisi. Par exemple, si l’expression d’index est de typeshort
, une conversion implicite enint
est effectuée, puisque des conversions implicites deshort
enint
et deshort
enlong
sont possibles. Si l’évaluation d’une expression d’index ou la conversion implicite qui suit provoque une exception, alors aucune autre expression d’index n’est évaluée et aucune étape supplémentaire n’est exécutée. - La valeur de
P
est vérifiée pour être valide. Si la valeur deP
estnull
, unSystem.NullReferenceException
est lancé et aucune autre étape n'est exécutée. - La valeur de chaque expression dans la argument_list est vérifiée par rapport aux limites réelles de chaque dimension de l’instance de tableau référencée par
P
. Si une ou plusieurs valeurs sont hors de portée, unSystem.IndexOutOfRangeException
est levé et aucune étape supplémentaire n’est exécutée. - L’emplacement de l’élément du tableau déterminé par l’(les) expression(s) d’index est calculé, et cet emplacement devient le résultat de l’accès au tableau.
12.8.12.3 accès à l’indexeur
Pour un accès indexeur, la primary_no_array_creation_expression de l'element_access doit être une variable ou une valeur d’une classe, d’une structure ou d’un type d’interface, et ce type doit implémenter un ou plusieurs indexeurs qui sont applicables à la argument_list de l'element_access.
Le traitement du temps de liaison d'un accès à un indexeur de la forme P[A]
, où P
est une primary_no_array_creation_expression, d'une structure ou d'une interface de type T
, et A
est une argument_list, se compose des étapes suivantes :
- L'ensemble des indexeurs fournis par
T
est construit. L’ensemble se compose de tous les indexeurs déclarés dansT
ou dans un type de base deT
qui ne sont pas des déclarations de redéfinition et qui sont accessibles dans le contexte actuel (§7.5). - L’ensemble est réduit aux indexeurs applicables et non masqués par d’autres indexeurs. Les règles suivantes sont appliquées à chaque indexeur
S.I
de l’ensemble, oùS
est le type dans lequel l’indexeurI
est déclaré :- Si
I
n’est pas applicable par rapport àA
(§12.6.4.2), alorsI
est retiré de l’ensemble. - Si
I
est applicable par rapport àA
(§12.6.4.2), alors tous les indexeurs déclarés dans un type de base deS
sont retirés de l’ensemble. - Si
I
est applicable par rapport àA
(§12.6.4.2) et queS
est un type de classe autre queobject
, tous les indexeurs déclarés dans une interface sont retirés de l’ensemble.
- Si
- Si l’ensemble résultant d’indexeurs candidats est vide, alors aucun indexeur applicable n’existe, et une erreur de liaison survient.
- Le meilleur indexeur de l’ensemble des indexeurs candidats est identifié en utilisant les règles de résolution de surcharge de §12.6.4. Si le meilleur indexeur ne peut être identifié, l'accès à l'indexeur est ambigu et une erreur de liaison se produit.
- Les expressions d’index de la argument_list sont évaluées dans l’ordre, de gauche à droite. Le résultat du traitement de l'accès à l'indexeur est une expression classée comme accès à l'indexeur. L’expression d’accès à l’indexeur fait référence à l’indexeur déterminé à l’étape précédente, et possède une expression d’instance associée
P
ainsi qu’une liste d’arguments associéeA
, et un type associé qui est le type de l’indexeur. SiT
est un type de classe, le type associé est choisi à partir de la première déclaration ou du premier remplacement de l'indexeur trouvé en commençant parT
et en recherchant dans ses classes de base.
Selon le contexte dans lequel il est utilisé, un accès d’indexeur provoque l’appel de l’accesseur « get » ou de l’accesseur « set » de l’indexeur. Si l’accès à l’indexeur est la cible d’une assignation, l’accesseur de définition (set) est appelé pour affecter une nouvelle valeur (§12.21.2). Dans tous les autres cas, l’accesseur de lecture (get) est appelé pour obtenir la valeur actuelle (§12.2.2).
12.8.13 Accès à un élément conditionnel nul
Un accès null_conditional_element_access consiste en une expression primary_no_array_creation_expression suivie des deux jetons « ?
» et « [
», suivie d'une argument_list, suivie d'un jeton « ]
», suivie de zéro ou plusieurs dependent_accesses dont chacun peut être précédé d'un null_forgiving_operator.
null_conditional_element_access
: primary_no_array_creation_expression '?' '[' argument_list ']'
(null_forgiving_operator? dependent_access)*
;
Un null_conditional_element_access est une version conditionnelle de element_access (§12.8.12), et il s’agit d’une erreur de liaison si le type de résultat est void
. Pour une expression conditionnelle Null dont le type de résultat peut être void
, voir (§12.8.11).
Une expression null_conditional_element_accessE
est de la forme P?[A]B
; où B
sont les dependent_access, le cas échéant. Le sens de E
est déterminé comme suit :
Si le type de
P
est un type valeur pouvant être Null :Soit
T
le type de l’expressionP.Value[A]B
.Si
T
est un paramètre de type qui n’est pas connu pour être soit un type de référence, soit un type valeur pouvant être Null, une erreur de compilation survient.Si
T
est un type valeur pouvant être Null alors le type deE
estT?
, et le sens deE
est le même que celui de :((object)P == null) ? (T?)null : P.Value[A]B
Sauf que
P
est évalué une seule fois.Sinon, le type de
E
estT
, et le sens deE
est le même que celui de :((object)P == null) ? null : P.Value[A]B
Sauf que
P
est évalué une seule fois.
Sinon :
Soit
T
le type de l’expressionP[A]B
.Si
T
est un paramètre de type qui n’est pas connu pour être soit un type de référence, soit un type valeur pouvant être Null, une erreur de compilation survient.Si
T
est un type valeur pouvant être Null alors le type deE
estT?
, et le sens deE
est le même que celui de :((object)P == null) ? (T?)null : P[A]B
Sauf que
P
est évalué une seule fois.Sinon, le type de
E
estT
, et le sens deE
est le même que celui de :((object)P == null) ? null : P[A]B
Sauf que
P
est évalué une seule fois.
Remarque : dans une expression de la forme :
P?[A₀]?[A₁]
si
P
est évalué commenull
, niA₀
niA₁
ne sont évalués. Il en va de même si une expression est une séquence d'opérations null_conditional_element_access ou null_conditional_member_access§12.8.8.fin de la remarque
12.8.14 This access
Un accès this_access est constitué du mot-clé this
.
this_access
: 'this'
;
Un this_access est permis uniquement dans le block d’un constructeur d’instance, d’une méthode d’instance, d’un accesseur d’instance (§12.2.1), ou d’un finaliseur. Il a l’une des significations suivantes :
- Lorsque
this
est utilisé dans une primary_expression au sein d’un constructeur d’instance d’une classe, il est classé comme une valeur. Le type de la valeur est le type d’instance (§15.3.2) de la classe dans laquelle l’utilisation se produit, et la valeur est une référence à l’objet en cours de construction. - Lorsque
this
est utilisé dans une primary_expression au sein d’une méthode d’instance ou d’un accesseur d’instance d’une classe, il est classé comme une valeur. Le type de la valeur est le type d’instance (§15.3.2) de la classe dans laquelle l’utilisation se produit, et la valeur est une référence à l’objet pour lequel la méthode ou l’accesseur a été appelé. - Lorsque
this
est utilisé dans une primary_expression au sein d’un constructeur d’instance d’un struct, il est classé comme une variable. Le type de la variable est le type d’instance (§15.3.2) du struct dans lequel l’utilisation se produit, et la variable représente le struct en cours de construction.- Si la déclaration du constructeur n’a pas d’initialiseur de constructeur, la variable
this
se comporte exactement comme un paramètre de sortie du type struct. En particulier, cela signifie que la variable doit être assurément affectée dans chaque chemin d’exécution du constructeur d’instance. - Sinon, la variable
this
se comporte exactement comme un paramètreref
du type struct. En particulier, cela signifie que la variable est considérée comme initialement assignée.
- Si la déclaration du constructeur n’a pas d’initialiseur de constructeur, la variable
- Lorsque
this
est utilisé dans une primary_expression au sein d’une méthode d’instance ou d’un accesseur d’instance d’un struct, il est classé comme une variable. Le type de la variable est le type d’instance (§15.3.2) du struct dans lequel l’utilisation se produit.- Si la méthode ou l’accesseur n’est pas un itérateur (§15.14) ou une fonction async (§15.15), la variable
this
représente le struc pour lequel la méthode ou l’accesseur a été appelé.- Si le struct est un
readonly struct
, la variablethis
se comporte exactement comme un paramètre d’entrée du type struct - Sinon, la variable
this
se comporte exactement comme un paramètreref
du type struct
- Si le struct est un
- Si la méthode ou l’accesseur est un itérateur ou une fonction asynchrone, la variable
this
représente une copie du struct pour lequel la méthode ou l’accesseur a été appelé et se comporte exactement comme un paramètre de valeur du type struct.
- Si la méthode ou l’accesseur n’est pas un itérateur (§15.14) ou une fonction async (§15.15), la variable
L’utilisation de this
dans une primary_expression dans un contexte autre que ceux énumérés ci-dessus constitue une erreur de compilation. Plus spécifiquement, il n’est pas possible de faire référence à this
dans une méthode statique, un accesseur de propriété statique ou dans un variable_initializer d’une déclaration de champ.
12.8.15 Accès à la base
Un base_access se compose du mot-clé base
suivi d'un jeton «.
», d'un identificateur, et éventuellement d'une type_argument_list, ou d'une argument_list entre crochets :
base_access
: 'base' '.' identifier type_argument_list?
| 'base' '[' argument_list ']'
;
Un base_access est utilisé pour accéder aux membres de la classe de base qui sont masqués par des membres de même nom dans la classe ou la structure actuelle. Un base_access est autorisé uniquement dans le corps d’un constructeur d’instance, d’une méthode d’instance, d’un accesseur d’instance (§12.2.1) ou d’un finaliseur. Lorsque base.I
se produit dans une classe ou un struct, I
désigne un membre de la classe de base de cette classe ou de ce struct. De même, lorsque base[E]
apparaît dans une classe, un indexeur applicable doit exister dans la classe de base.
Au moment de la liaison, les expressions base_access de la forme base.I
et base[E]
sont évaluées exactement comme si elles étaient écrites ((B)this).I
et ((B)this)[E]
, où B
est la classe de base de la classe ou de la structure dans laquelle la construction se produit. base.I
et base[E]
correspondent ainsi à this.I
et this[E]
, sauf que this
est considéré comme une instance de la classe de base.
Lorsqu’un base_access fait référence à un membre fonctionnel virtuel (une méthode, une propriété ou un indexeur), la détermination du membre fonctionnel à appeler à l’exécution (§12.6.6) est modifiée. Le membre fonctionnel appelé est déterminé en trouvant l’implémentation la plus dérivée (§15.6.4) du membre fonctionnel par rapport à B
(au lieu de par rapport au type d’exécution de this
, comme cela serait habituel dans un accès non-base). Ainsi, dans le cadre d'un remplacement d'un membre de fonction virtuelle, un base_access peut être utilisé pour invoquer l'implémentation héritée du membre de fonction. Si le membre fonctionnel référencé par un base_access est abstrait, une erreur de liaison survient.
Remarque : contrairement à
this
,base
n’est pas une expression en soi. C’est un mot-clé utilisé uniquement dans le contexte d’un base_access ou d’un constructor_initializer (§15.11.2). fin de la remarque
12.8.16 Opérateurs suffixés d’incrémentation et de décrémentation
post_increment_expression
: primary_expression '++'
;
post_decrement_expression
: primary_expression '--'
;
L'opérande d'une opération d'incrémentation ou de décrémentation postfixe doit être une expression classée comme une variable, un accès à une propriété ou un accès à un indexeur. Le résultat de l’opération est une valeur du même type que l’opérande.
Si le primary_expression a le type de compilation dynamic
, alors l’opérateur est lié dynamiquement (§12.3.3), le post_increment_expression ou post_decrement_expression a le type de compilation dynamic
et les règles suivantes sont appliquées à l’exécution en utilisant le type d’exécution du primary_expression.
Si l'opérande d'une opération d'incrémentation ou de décrémentation postfixe est une propriété ou un indexeur, la propriété ou l'indexeur doit avoir un accesseur get et un accesseur set. Dans le cas contraire, une erreur de liaison survient.
La résolution de surcharge des opérateurs unaires (§12.4.4) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Des opérateurs prédéfinis ++
et --
existent pour les types suivants : sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
et tout type énuméré. Les opérateurs prédéfinis ++
renvoient la valeur produite en ajoutant 1
à l’opérande, et les opérateurs prédéfinis --
renvoient la valeur produite en soustrayant 1
à l’opérande. Dans un contexte vérifié, si le résultat de cette addition ou soustraction est en dehors de la plage du type de résultat et que le type de résultat est un type intégral ou un type enum, un System.OverflowException
est lancé.
Il doit exister une conversion implicite du type de retour de l’opérateur unaire sélectionné vers le type du primary_expression, sinon une erreur de compilation survient.
L'exécution d'une opération d'incrémentation ou de décrémentation postfixe de la forme x++
ou x--
comprend les étapes suivantes :
- Si
x
est classé comme une variable :-
x
est évalué pour produire la variable. - La valeur de
x
est enregistrée. - La valeur sauvegardée de
x
est convertie en type opérande de l’opérateur sélectionné et l’opérateur est appelé avec cette valeur comme argument. - La valeur renvoyée par l’opérateur est convertie en type de
x
et stockée à l’emplacement déterminé par l’évaluation antérieure dex
. - La valeur sauvegardée de
x
devient le résultat de l’opération.
-
- Si
x
est classé comme un accès de propriété ou d'indexeur :- L’expression d’instance (si
x
n’est passtatic
) et la liste d’arguments (six
est un accès à un indexeur) associées àx
sont évaluées, et les résultats sont utilisés dans les appels subséquents de l’accesseur get et set. - L’accesseur get de
x
est appelé et la valeur renvoyée est sauvegardée. - La valeur sauvegardée de
x
est convertie en type opérande de l’opérateur sélectionné et l’opérateur est appelé avec cette valeur comme argument. - La valeur renvoyée par l’opérateur est convertie en type de
x
et l’accesseur set dex
est appelé avec cette valeur comme argument de valeur. - La valeur sauvegardée de
x
devient le résultat de l’opération.
- L’expression d’instance (si
Les opérateurs ++
et --
prennent également en charge la notation préfixe (§12.9.6). Le résultat de x++
ou x--
est la valeur de x
avant l’opération, tandis que le résultat de ++x
ou --x
est la valeur de x
après l’opération. Dans les deux cas, x
conserve la même valeur après l’opération.
Une implémentation de l'opérateur ++
ou de l'opérateur --
peut être invoquée en utilisant soit la notation postfixe, soit la notation préfixe. Il n’est pas possible d’avoir des implémentations d’opérateurs distinctes pour les deux notations.
12.8.17 l’opérateur new
12.8.17.1 Général
L’opérateur new
est utilisé pour créer de nouvelles instances de types.
Il existe trois formes d’expressions new :
- Les expressions de création d’objets et les expressions de création d’objets anonymes sont utilisées pour créer de nouvelles instances de types de classe et de types valeur.
- Les expressions de création de tableaux sont utilisées pour créer de nouvelles instances de types de tableau.
- Les expressions de création de délégués sont utilisées pour obtenir des instances de types de délégués.
L’opérateur new
implique la création d’une instance d’un type, mais n’implique pas nécessairement l’allocation de mémoire. En particulier, les instances de types valeur ne nécessitent pas de mémoire supplémentaire au-delà des variables dans lesquelles elles résident, et aucune allocation n’a lieu lorsque new
est utilisé pour créer des instances de types valeur.
Remarque : les expressions de création de délégués ne créent pas toujours de nouvelles instances. Lorsque l’expression est traitée de la même manière qu’une conversion de groupe de méthodes (§10.8) ou une conversion de fonction anonyme (§10.7), cela peut entraîner la réutilisation d’une instance de délégué existante. fin de la remarque
12.8.17.2 expressions de création d’objets
Une object_creation_expression est utilisée pour créer une nouvelle instance d’un class_type ou d’un value_type.
object_creation_expression
: 'new' type '(' argument_list? ')' object_or_collection_initializer?
| 'new' type object_or_collection_initializer
;
object_or_collection_initializer
: object_initializer
| collection_initializer
;
Le type d’une object_creation_expression doit être un class_type, un value_type ou un type_parameter. Le type ne peut pas être un tuple_type ni un class_type abstrait ou statique.
L’argument_list optionnelle (§12.6.2) est autorisée uniquement si le type est un class_type ou un struct_type.
Une expression de création d’objet peut omettre la liste d’arguments du constructeur et les parenthèses si elle inclut un initialisateur d’objet ou un initialisateur de collection. Omettre la liste des arguments du constructeur et les parenthèses englobantes équivaut à spécifier une liste d’arguments vide.
Le traitement d’une expression de création d’objet qui inclut un initialiseur d’objet ou un initialiseur de collection consiste d’abord à traiter le constructeur d’instance, puis à traiter les initialisations de membres ou d’éléments spécifiées par l’initialiseur d’objet (§12.8.17.3) ou l’initialiseur de collection (§12.8.17.4).
Si l’un quelconque des arguments dans la argument_list optionnelle a le type de compilation dynamic
, alors l’object_creation_expression est liée dynamiquement (§12.3.3) et les règles suivantes sont appliquées à l’exécution en utilisant le type d’exécution de ces arguments de l’argument_list qui ont le type de compilation dynamic
. Cependant, la création d’objet subit une vérification limitée à la compilation comme décrit dans §12.6.5.
Le traitement au niveau de la liaison d’une object_creation_expression du formulaire new T(A)
, où T
est un class_type, ou un value_type, et A
est une argument_list facultative, se compose des étapes suivantes :
- Si
T
est un value_type et queA
n’est pas présent :- L’object_creation_expression est un appel du constructeur par défaut. Le résultat de l’object_creation_expression est une valeur de type
T
, à savoir la valeur par défaut pourT
telle que définie dans §8.3.3.
- L’object_creation_expression est un appel du constructeur par défaut. Le résultat de l’object_creation_expression est une valeur de type
- Sinon, si
T
est un type_parameter et queA
n’est pas présent :- Si aucune contrainte de type de valeur ou contrainte de constructeur (§15.2.5) n’a été spécifiée pour
T
, une erreur de temps de liaison se produit. - Le résultat de l’object_creation_expression est une valeur du type à l’exécution auquel le paramètre de type a été lié, c’est-à-dire le résultat de l’appel du constructeur par défaut de ce type. Le type d’exécution peut être un type de référence ou un type de valeur.
- Si aucune contrainte de type de valeur ou contrainte de constructeur (§15.2.5) n’a été spécifiée pour
- Sinon, si
T
est un class_type ou un struct_type :- Si
T
est un class_type abstrait ou statique, une erreur de compilation se produit. - Le constructeur d’instance à appeler est déterminé selon les règles de résolution de surcharge de §12.6.4. L’ensemble de constructeurs d’instances candidates se compose de tous les constructeurs d’instances accessibles déclarés dans
T
, qui s’appliquent àA
(§12.6.4.2). Si l’ensemble des constructeurs d’instance candidats est vide, ou si aucun constructeur d’instance optimal unique ne peut être identifié, une erreur de liaison se produit. - Le résultat de l’object_creation_expression est une valeur de type
T
, c’est-à-dire la valeur obtenue en appelant le constructeur d’instance déterminé à l’étape précédente. - Sinon, l’object_creation_expression est invalide, et une erreur de liaison se produit.
- Si
Même si l'expression de création d'objet est liée dynamiquement, le type compile-time est toujours T
.
Le traitement au moment de l'exécution d'une object_creation_expression de la forme new T(A)
, où T
est un type de classe ou un type de structure et A
est éventuellement une liste d'arguments , comprend les étapes suivantes :
- Si
T
est un class_type :- Une nouvelle instance de la classe
T
est allouée. Si la mémoire disponible est insuffisante pour allouer la nouvelle instance, uneSystem.OutOfMemoryException
est levée et aucune étape supplémentaire n’est exécutée. - Tous les champs de la nouvelle instance sont initialisés avec leurs valeurs par défaut (§9.3).
- Le constructeur d’instance est appelé selon les règles d’appel des membres fonctionnels (§12.6.6). Une référence à la nouvelle instance allouée est automatiquement transmise au constructeur d’instance et l’instance peut être accédée dans ce constructeur via this.
- Une nouvelle instance de la classe
- Si
T
est un struct_type :- Une instance de type
T
est créée en allouant une variable locale temporaire. Puisqu’un constructeur d’instance d’un struct_type doit impérativement affecter une valeur à chaque champ de l’instance en cours de création, aucune initialisation de la variable temporaire n’est nécessaire. - Le constructeur d’instance est appelé selon les règles d’appel des membres fonctionnels (§12.6.6). Une référence à la nouvelle instance allouée est automatiquement transmise au constructeur d’instance et l’instance peut être accédée dans ce constructeur via this.
- Une instance de type
12.8.17.3 Initialisateurs d’objets
Un object initializer spécifie des valeurs pour zéro ou plusieurs champs, propriétés ou éléments indexés d’un objet.
object_initializer
: '{' member_initializer_list? '}'
| '{' member_initializer_list ',' '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: initializer_target '=' initializer_value
;
initializer_target
: identifier
| '[' argument_list ']'
;
initializer_value
: expression
| object_or_collection_initializer
;
Un initialiseur d’objet se compose d’une séquence d’initialiseurs de membres, placés entre les jetons {
et }
et séparés par des virgules. Chaque member_initializer doit désigner une cible pour l’initialisation. Un identifier doit désigner un champ ou une propriété accessible de l’objet en cours d’initialisation, tandis qu’un argument_list encadré de crochets doit spécifier les arguments pour un indexeur accessible sur l’objet en cours d’initialisation. Il est incorrect qu'un initialiseur d'objet inclut plus d'un initialiseur membre pour le même champ ou la même propriété.
Remarque : bien qu’un object initializer ne soit pas autorisé à affecter plusieurs fois le même champ ou la même propriété, aucune restriction de ce type ne s’applique aux indexeurs. Un object initializer peut contenir plusieurs cibles d’initialisation faisant référence à des indexeurs, et peut même utiliser les mêmes arguments d’indexeur à plusieurs reprises. fin de la remarque
Chaque initialisateur_cible est suivi d'un signe égal et d'une expression, d'un initialisateur d'objet ou d'un initialisateur de collection. Il est impossible que les expressions au sein de l’object initializer se réfèrent à la nouvelle instance qu’il initialise.
Un initialisateur de membre qui spécifie une expression après le signe égal est traité de la même manière qu'une affectation (§12.21.2) à la cible.
Un member initializer qui spécifie un object initializer après le signe égal constitue un nested object initializer, c’est-à-dire une initialisation d’un objet imbriqué. Au lieu d’affecter une nouvelle valeur au champ ou à la propriété, les affectations dans le nested object initializer sont traitées comme des affectations aux membres du champ ou de la propriété. Les nested object initializers ne peuvent pas être appliqués aux propriétés de type valeur, ni aux champs en lecture seule de type valeur.
Un initialiseur de membre spécifiant un initialiseur de collection après le signe égal est une initialisation d’une collection imbriquée. Au lieu d’affecter une nouvelle collection au champ, à la propriété ou à l’indexeur cible, les éléments indiqués dans l’initialiseur sont ajoutés à la collection référencée par la cible. La cible doit être de type collection qui satisfait aux exigences spécifiées dans §12.8.17.4.
Lorsqu’une cible d’initialisation fait référence à un indexeur, les arguments de l’indexeur doivent toujours être évalués une seule fois. Ainsi, même si les arguments ne finissent pas par être utilisés (par exemple, en raison d’un nested initializer vide), ils sont évalués pour leurs effets secondaires.
Exemple : La classe suivante représente un point avec deux coordonnées :
public class Point { public int X { get; set; } public int Y { get; set; } }
Une instance de
Point
peut être créée et initialisée de la manière suivante :Point a = new Point { X = 0, Y = 1 };
Cela a le même effet que
Point __a = new Point(); __a.X = 0; __a.Y = 1; Point a = __a;
où
__a
est une variable temporaire autrement invisible et inaccessible.La classe suivante illustre un rectangle créé à partir de deux points, ainsi que la création et l’initialisation d’une instance de
Rectangle
:public class Rectangle { public Point P1 { get; set; } public Point P2 { get; set; } }
Une instance de
Rectangle
peut être créée et initialisée de la manière suivante :Rectangle r = new Rectangle { P1 = new Point { X = 0, Y = 1 }, P2 = new Point { X = 2, Y = 3 } };
Cela a le même effet que
Rectangle __r = new Rectangle(); Point __p1 = new Point(); __p1.X = 0; __p1.Y = 1; __r.P1 = __p1; Point __p2 = new Point(); __p2.X = 2; __p2.Y = 3; __r.P2 = __p2; Rectangle r = __r;
où
__r
,__p1
et__p2
sont des variables temporaires autrement invisibles et inaccessibles.Si le constructeur de
Rectangle
alloue les deux instances incorporées dePoint
, elles peuvent servir à initialiser les instances incorporées dePoint
au lieu de créer de nouvelles instances :public class Rectangle { public Point P1 { get; } = new Point(); public Point P2 { get; } = new Point(); }
la construction suivante peut être utilisée pour initialiser les instances
Point
imbriquées au lieu d’affecter de nouvelles instances :Rectangle r = new Rectangle { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } };
Cela a le même effet que
Rectangle __r = new Rectangle(); __r.P1.X = 0; __r.P1.Y = 1; __r.P2.X = 2; __r.P2.Y = 3; Rectangle r = __r;
exemple final
12.8.17.4 Initialisateurs de collection
Un initialiseur de collection spécifie les éléments d’une collection.
collection_initializer
: '{' element_initializer_list '}'
| '{' element_initializer_list ',' '}'
;
element_initializer_list
: element_initializer (',' element_initializer)*
;
element_initializer
: non_assignment_expression
| '{' expression_list '}'
;
expression_list
: expression
| expression_list ',' expression
;
Un initialiseur de collection se compose d'une séquence d'initialiseurs d'éléments, placée entre les jetons {
et }
et séparée par des virgules. Chaque initialiseur d’élément spécifie un élément à ajouter à l’objet de collection en cours d’initialisation et se compose d’une liste d’expressions placée entre les jetons {
et }
et séparée par des virgules. Un initialiseur d’élément à expression unique peut être rédigé sans accolades, mais ne peut alors pas être une expression d’affectation, afin d’éviter toute ambiguïté avec les member initializers. La production non_assignment_expression est définie au §12.22.
Exemple : Le qui suit est un exemple d’expression de création d’objet incluant un initialiseur de collection :
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
exemple final
L’objet collection auquel un initialiseur de collection est appliqué doit être d’un type qui implémente System.Collections.IEnumerable
, sinon une erreur de compilation se produit. Pour chaque élément spécifié, dans l’ordre de gauche à droite, la recherche normale de membre est appliquée afin de trouver un membre nommé Add
. Si le résultat de la recherche de membre n’est pas un groupe de méthodes, une erreur de compilation se produit. Sinon, la résolution de surcharge est appliquée avec la liste d’expressions de l’initialiseur d’élément comme liste d’arguments, et l’initialiseur de collection appelle la méthode résultante. Ainsi, l’objet collection doit contenir une méthode d’instance ou d’extension applicable portant le nom Add
pour chaque initialiseur d’élément.
Exemple: La classe suivante représente un contact avec un nom et une liste de numéros de téléphone, ainsi que la création et l’initialisation d’un
List<Contact>
:public class Contact { public string Name { get; set; } public List<string> PhoneNumbers { get; } = new List<string>(); } class A { static void M() { var contacts = new List<Contact> { new Contact { Name = "Chris Smith", PhoneNumbers = { "206-555-0101", "425-882-8080" } }, new Contact { Name = "Bob Harris", PhoneNumbers = { "650-555-0199" } } }; } }
ce qui a le même effet que
var __clist = new List<Contact>(); Contact __c1 = new Contact(); __c1.Name = "Chris Smith"; __c1.PhoneNumbers.Add("206-555-0101"); __c1.PhoneNumbers.Add("425-882-8080"); __clist.Add(__c1); Contact __c2 = new Contact(); __c2.Name = "Bob Harris"; __c2.PhoneNumbers.Add("650-555-0199"); __clist.Add(__c2); var contacts = __clist;
où
__clist
,__c1
et__c2
sont des variables temporaires autrement invisibles et inaccessibles.exemple final
12.8.17.5 Expressions de création de tableau
Une array_creation_expression est utilisée pour créer une nouvelle instance d’un array_type.
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier*
array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
Une expression de création de tableau de la première forme alloue une instance de tableau du type résultant de la suppression de chacune des expressions individuelles dans la liste d’expressions.
Exemple: l’expression de création de tableau
new int[10,20]
produit une instance de tableau de typeint[,]
, et l’expression de création de tableau newint[10][,]
produit une instance de tableau de typeint[][,]
. exemple final
Chaque expression dans la liste d’expressions doit être de type int
, uint
, long
ou ulong
, ou implicitement convertible en un ou plusieurs de ces types. La valeur de chaque expression détermine la taille de la dimension correspondante dans la nouvelle instance de tableau allouée. Puisque la taille d’une dimension de tableau doit être non négative, il s’agit d’une erreur de compilation d’avoir une expression constante avec une valeur négative dans la liste d’expressions.
Sauf dans un contexte non sécurisé (§23.2), la disposition des tableaux n’est pas spécifiée.
Si une expression de création de tableau de la première forme inclut un initialiseur de tableau, chaque expression dans la liste d’expressions doit être une constante et le rang ainsi que les tailles de dimension spécifiés dans la liste doivent correspondre à ceux de l’initialiseur de tableau.
Dans une expression de création de tableau de la deuxième ou troisième forme, le rang du type de tableau spécifié ou du spécificateur de rang doit correspondre à celui de l’initialiseur de tableau. Les tailles individuelles des dimensions sont déduites du nombre d’éléments à chaque niveau d’imbrication correspondant de l’initialiseur de tableau. Ainsi, l'expression initialisatrice de la déclaration suivante
var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};
correspond exactement à
var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};
Une expression de création de tableau de la troisième forme est appelée expression implicitly typed array-creation. Elle est similaire à la deuxième forme, sauf que le type des éléments du tableau n’est pas donné explicitement, mais déterminé comme le meilleur type commun (§12.6.3.15) de l’ensemble des expressions dans l’initialiseur de tableau. Pour un tableau multidimensionnel, c'est-à-dire dont le rank_specifier contient au moins une virgule, cet ensemble comprend toutes les expressions trouvées dans les array_initializers imbriqués.
Les initialisateurs de tableau sont décrits plus en détail dans §17.7.
Le résultat de l’évaluation d’une expression de création de tableau est classé comme une valeur, c’est-à-dire une référence à la nouvelle instance de tableau allouée. Le traitement à l’exécution d’une expression de création de tableau consiste en les étapes suivantes :
- Les expressions de taille de dimension de la expression_list sont évaluées dans l’ordre, de gauche à droite. Suite à l'évaluation de chaque expression, une conversion implicite (§10.2) en l’un des types suivants est effectuée :
int
,uint
,long
,ulong
. Le premier type dans cette liste pour lequel une conversion implicite existe est choisi. Si l’évaluation d’une expression ou la conversion implicite qui s’ensuit provoque une exception, alors aucune autre expression n’est évaluée et aucune étape supplémentaire n’est exécutée. - Les valeurs calculées pour les tailles de dimension sont validées comme suit : si l’une ou plusieurs des valeurs est inférieure à zéro, une
System.OverflowException
est levée et aucune étape supplémentaire n’est exécutée. - Une instance de tableau avec les tailles de dimension données est allouée. Si la mémoire disponible est insuffisante pour allouer la nouvelle instance, une
System.OutOfMemoryException
est levée et aucune étape supplémentaire n’est exécutée. - Tous les éléments de la nouvelle instance de tableau sont initialisés à leurs valeurs par défaut (§9.3).
- Si l’expression de création de tableau contient un initialiseur de tableau, alors chaque expression dans l’initialiseur de tableau est évaluée et assignée à l’élément de tableau correspondant. Les évaluations et affectations sont effectuées dans l’ordre où les expressions sont écrites dans l’initialiseur de tableau. En d’autres termes, les éléments sont initialisés dans l’ordre croissant des indices, la dimension la plus à droite augmentant en premier. Si l’évaluation d’une expression donnée ou l’affectation subséquente à l’élément de tableau correspondant provoque une exception, alors aucun élément supplémentaire n’est initialisé (et les éléments restants conserveront ainsi leurs valeurs par défaut).
Une expression de création de tableau permet l’instanciation d’un tableau dont les éléments sont de type tableau, mais les éléments d’un tel tableau doivent être initialisés manuellement.
Exemple: la déclaration
int[][] a = new int[100][];
crée un tableau unidimensionnel contenant 100 éléments de type
int[]
. La valeur initiale de chaque élément estnull
. Il n'est pas possible que la même expression de création de tableau instancie également les sous-réseaux, et l'instructionint[][] a = new int[100][5]; // Error
entraîne une erreur de compilation. L'instanciation des sous-ensembles peut être effectuée manuellement, comme dans l'instruction
int[][] a = new int[100][]; for (int i = 0; i < 100; i++) { a[i] = new int[5]; }
exemple final
Remarque: lorsqu’un tableau de tableaux a une forme « rectangulaire », c’est-à-dire lorsque les sous-tableaux ont tous la même longueur, il est plus efficace d’utiliser un tableau multidimensionnel. Dans l’exemple ci-dessus, l’instanciation du tableau de tableaux crée 101 objets : un tableau externe et 100 sous-tableaux. En revanche,
int[,] a = new int[100, 5];
elle ne crée qu’un seul objet, un tableau à deux dimensions, et réalise l’allocation en une seule instruction.
fin de la remarque
Exemple: les exemples suivants sont des expressions de création de tableau implicitement typées :
var a = new[] { 1, 10, 100, 1000 }; // int[] var b = new[] { 1, 1.5, 2, 2.5 }; // double[] var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,] var d = new[] { 1, "one", 2, "two" }; // Error
La dernière expression provoque une erreur de compilation car ni
int
nistring
ne sont implicitement convertibles l’un à l’autre, et il n’existe donc pas de meilleur type commun. Une expression de création de tableau explicitement typée doit être utilisée dans ce cas, par exemple en spécifiant que le type estobject[]
. Sinon, l’un des éléments peut être casté en un type de base commun, qui deviendra alors le type d’élément déduit.exemple final
Les expressions de création de tableau implicitement typées peuvent être combinées avec des initialisateurs d’objets anonymes (§12.8.17.7) pour créer des structures de données à type anonyme.
Exemple :
var contacts = new[] { new { Name = "Chris Smith", PhoneNumbers = new[] { "206-555-0101", "425-882-8080" } }, new { Name = "Bob Harris", PhoneNumbers = new[] { "650-555-0199" } } };
exemple final
12.8.17.6 Expressions de création de délégués
Une delegate_creation_expression est utilisée pour obtenir une instance d’un delegate_type.
delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;
L'argument d'une expression de création de délégué doit être un groupe de méthodes, une fonction anonyme ou une valeur du type compile-time dynamic
ou d'un delegate_type. Si l’argument est un groupe de méthodes, il identifie la méthode et, pour une méthode d’instance, l’objet pour lequel créer un délégué. Si l’argument est une fonction anonyme, il définit directement les paramètres et le corps de la méthode cible du délégué. Si l'argument est une valeur, il désigne une instance de délégué à partir de laquelle créer une copie.
Si l’expression a le type à la compilation dynamic
, la delegate_creation_expression est liée dynamiquement (§12.8.17.6), et les règles ci-dessous sont appliquées à l’exécution en utilisant le type à l’exécution de l’expression. Sinon, les règles sont appliquées à la compilation.
Le traitement au niveau de la liaison d'une delegate_creation_expression de la forme new D(E)
, où D
est un delegate_type et E
une expression, comprend les étapes suivantes :
Si
E
est un groupe de méthodes, l’expression de création de délégué est traitée de la même manière qu’une conversion de groupe de méthodes (§10.8) deE
versD
.Si
E
est une fonction anonyme, l’expression de création de délégué est traitée de la même manière qu’une conversion de fonction anonyme (§10.7) deE
versD
.Si
E
est une valeur,E
doit être compatible (§20.2) avecD
, et le résultat est une référence à un délégué nouvellement créé avec une liste d’appel à entrée unique qui appelleE
.
Le traitement de l'exécution d'une delegate_creation_expression de la forme new D(E)
, où D
est un delegate_type et E
une expression, comprend les étapes suivantes :
- Si
E
est un groupe de méthodes, l’expression de création de délégué est évaluée comme une conversion de groupe de méthodes (§10.8) deE
versD
. - Si
E
est une fonction anonyme, la création de délégué est évaluée comme une conversion de fonction anonyme deE
versD
(§10.7). - Si
E
est une valeur d’un delegate_type :E
est évalué. Si cette évaluation provoque une exception, aucune étape supplémentaire n’est exécutée.- Si la valeur de
E
estnull
, unSystem.NullReferenceException
est lancé et aucune autre étape n'est exécutée. - Une nouvelle instance du type délégué
D
est allouée. Si la mémoire disponible est insuffisante pour allouer la nouvelle instance, uneSystem.OutOfMemoryException
est levée et aucune étape supplémentaire n’est exécutée. - La nouvelle instance de délégué est initialisée avec une liste d'invocation à entrée unique qui invoque
E
.
La liste d’appel d’un délégué est déterminée lorsque le délégué est instancié, puis reste constante pendant toute la durée de vie du délégué. En d’autres termes, il n’est pas possible de modifier les entités appelables cibles d’un délégué après sa création.
Remarque: N’oubliez pas que lorsqu'on combine deux délégués ou que l'on en retire un d’un autre, un nouveau délégué est créé ; aucun délégué existant n’a son contenu modifié. fin de la remarque
Il n’est pas possible de créer un délégué qui fasse référence à une propriété, un indexeur, un opérateur défini par l’utilisateur, un constructeur d’instance, un finaliseur ou un constructeur statique.
Exemple : comme décrit ci-dessus, lorsqu’un délégué est créé à partir d’un groupe de méthodes, la liste de paramètres et le type de retour du délégué déterminent quelle méthode surchargée sera sélectionnée. Dans l'exemple
delegate double DoubleFunc(double x); class A { DoubleFunc f = new DoubleFunc(Square); static float Square(float x) => x * x; static double Square(double x) => x * x; }
le champ
A.f
est initialisé avec un délégué qui fait référence à la deuxième méthodeSquare
car cette méthode correspond exactement à la liste de paramètres et au type de retour deDoubleFunc
. Si la deuxième méthodeSquare
n’avait pas été présente, une erreur de compilation se serait produite.exemple final
12.8.17.7 Expressions de création d’objets anonymes
Une anonymous_object_creation_expression est utilisée pour créer un objet d’un type anonyme.
anonymous_object_creation_expression
: 'new' anonymous_object_initializer
;
anonymous_object_initializer
: '{' member_declarator_list? '}'
| '{' member_declarator_list ',' '}'
;
member_declarator_list
: member_declarator (',' member_declarator)*
;
member_declarator
: simple_name
| member_access
| null_conditional_projection_initializer
| base_access
| identifier '=' expression
;
Un initialiseur d’objet anonyme déclare un type anonyme et retourne une instance de ce type. Un type anonyme est un type classe sans nom qui hérite directement de object
. Les membres d’un type anonyme forment une séquence de propriétés en lecture seule déduites de l’initialiseur d’objet anonyme utilisé pour créer une instance de ce type. Plus précisément, un initialiseur d’objet anonyme de la forme
new {
p₁=
e₁,
p₂=
e₂,
… pᵥ=
eᵥ}
déclare un type anonyme de la forme
class __Anonymous1
{
private readonly «T1» «f1»;
private readonly «T2» «f2»;
...
private readonly «Tn» «fn»;
public __Anonymous1(«T1» «a1», «T2» «a2»,..., «Tn» «an»)
{
«f1» = «a1»;
«f2» = «a2»;
...
«fn» = «an»;
}
public «T1» «p1» { get { return «f1»; } }
public «T2» «p2» { get { return «f2»; } }
...
public «Tn» «pn» { get { return «fn»; } }
public override bool Equals(object __o) { ... }
public override int GetHashCode() { ... }
}
où chaque «Tx» est le type de l’expression correspondante «ex». L’expression utilisée dans un member_declarator doit avoir un type. Par conséquent, il s'agit d'une erreur de compilation dans le cas où une expression dans un member_declarator serait null
ou une fonction anonyme.
Les noms d’un type anonyme et du paramètre de sa méthode Equals
sont générés automatiquement par le compilateur et ne peuvent être référencés dans le texte du programme.
Dans un même programme, deux initialisateurs d’objet anonymes qui spécifient une séquence de propriétés ayant les mêmes noms et types à la compilation dans le même ordre produiront des instances du même type anonyme.
Exemple : Dans l'exemple
var p1 = new { Name = "Lawnmower", Price = 495.00 }; var p2 = new { Name = "Shovel", Price = 26.95 }; p1 = p2;
l'affectation de la dernière ligne est autorisée parce que
p1
etp2
sont du même type anonyme.exemple final
Les méthodes Equals
et GetHashcode
sur les types anonymes redéfinissent les méthodes héritées de object
, et sont définies en termes de Equals
et GetHashcode
des propriétés, de sorte que deux instances du même type anonyme sont égales si et seulement si toutes leurs propriétés sont égales.
Un member declarator peut être abrégé à un nom simple (§12.8.4), à un accès membre (§12.8.7), à un initialiseur de projection conditionnelle sur null §12.8.8 ou à un accès via base (§12.8.15). C'est ce qu'on appelle un initialisateur de projection et c'est un raccourci pour une déclaration et une affectation à une propriété portant le même nom. Plus précisément, les déclarateurs de membres de la forme
«identifier»
, «expr» . «identifier»
et «expr» ? . «identifier»
sont précisément équivalents aux suivants, respectivement :
«identifer» = «identifier»
, «identifier» = «expr» . «identifier»
et «identifier» = «expr» ? . «identifier»
Ainsi, dans un initialiseur de projection, l’identifiant sélectionne à la fois la valeur et le champ ou la propriété à laquelle la valeur est assignée. Intuitivement, un initialiseur de projection projette non seulement une valeur, mais aussi le nom de la valeur.
12.8.18 L'opérateur typeof
L’opérateur typeof
est utilisé pour obtenir l’objet System.Type
pour un type.
typeof_expression
: 'typeof' '(' type ')'
| 'typeof' '(' unbound_type_name ')'
| 'typeof' '(' 'void' ')'
;
unbound_type_name
: identifier generic_dimension_specifier?
| identifier '::' identifier generic_dimension_specifier?
| unbound_type_name '.' identifier generic_dimension_specifier?
;
generic_dimension_specifier
: '<' comma* '>'
;
comma
: ','
;
La première forme de typeof_expression se compose d’un mot-clé typeof
suivi d’un type entre parenthèses. Le résultat d’une expression de cette forme est l’objet System.Type
du type indiqué. Il n’existe qu’un seul objet System.Type
pour un type donné. Cela signifie que pour un type T
, typeof(T) == typeof(T)
est toujours vrai. Le type ne peut pas être dynamic
.
La deuxième forme de typeof_expression se compose d’un mot-clé typeof
suivi d’un unbound_type_name entre parenthèses.
Remarque: un unbound_type_name est très similaire à un type_name (§7.8) sauf qu’un unbound_type_name contient des generic_dimension_specifier alors qu’un type_name contient des type_argument_list. fin de la remarque
Lorsque l’opérande d’un typeof_expression est une séquence de tokens qui satisfait à la fois aux grammaires de unbound_type_name et de type_name, c’est-à-dire lorsqu’elle ne contient ni generic_dimension_specifier ni type_argument_list, la séquence de tokens est considérée comme un type_name. La signification d’un unbound_type_name est déterminée comme suit :
- Convertit la séquence de tokens en un type_name en remplaçant chaque generic_dimension_specifier par un type_argument_list ayant le même nombre de virgules et le mot-clé
object
que chaque argument_type. - Évaluez le type_name résultant, en ignorant toutes les contraintes de paramètres de type.
- L’unbound_type_name se résout en le type générique non lié associé au type construit résultant (§8.4).
Il est erroné que le nom du type soit un type de référence nul.
Le résultat du typeof_expression est l’objet System.Type
pour le type générique non lié résultant.
La troisième forme de typeof_expression se compose d’un mot-clé typeof
suivi d’un keyword void
entre parenthèses. Le résultat d’une expression de cette forme est l’objet System.Type
qui représente l’absence de type. L’objet type renvoyé par typeof(void)
est distinct de l’objet type renvoyé pour tout autre type.
Remarque : cet objet
System.Type
spécial est utile dans les bibliothèques de classes qui permettent la réflexion sur les méthodes du langage, où ces méthodes souhaitent disposer d’un moyen de représenter le type de retour de toute méthode, y compris les méthodesvoid
, à l’aide d’une instance deSystem.Type
. fin de la remarque
L’opérateur typeof
peut être utilisé sur un paramètre de type. Il s’agit d’une erreur de compilation si le nom du type est connu pour être un type de référence pouvant être Null. Le résultat est l’objet System.Type
pour le type à l’exécution qui a été lié au paramètre de type. Si le type d'exécution est un type de référence nullable, le résultat est le type de référence non nullable correspondant. L’opérateur typeof
peut également être utilisé sur un type construit ou un type générique non lié (§8.4.4). L’objet System.Type
pour un type générique non lié n’est pas le même que l’objet System.Type
du type d’instance (§15.3.2). Le type d’instance est toujours un type construit fermé à l’exécution, de sorte que son objet System.Type
dépend des arguments de type à l’exécution utilisés. Le type générique non lié, en revanche, n’a pas d’arguments de type, et renvoie le même objet System.Type
quel que soit les arguments de type à l’exécution.
Exemple: l’exemple
class X<T> { public static void PrintTypes() { Type[] t = { typeof(int), typeof(System.Int32), typeof(string), typeof(double[]), typeof(void), typeof(T), typeof(X<T>), typeof(X<X<T>>), typeof(X<>) }; for (int i = 0; i < t.Length; i++) { Console.WriteLine(t[i]); } } } class Test { static void Main() { X<int>.PrintTypes(); } }
produit la sortie suivante :
System.Int32 System.Int32 System.String System.Double[] System.Void System.Int32 X`1[System.Int32] X`1[X`1[System.Int32]] X`1[T]
Notez que
int
etSystem.Int32
sont du même type. Le résultat detypeof(X<>)
ne dépend pas de l’argument de type, mais le résultat detypeof(X<T>)
en dépend.exemple final
12.8.19 L'opérateur sizeof
L’opérateur sizeof
renvoie le nombre d’octets de 8 bits occupés par une variable d’un type donné. Le type spécifié comme opérande de sizeof doit être un unmanaged_type (§8.8).
sizeof_expression
: 'sizeof' '(' unmanaged_type ')'
;
Pour certains types prédéfinis, l’opérateur sizeof
renvoie une valeur constante int
comme indiqué dans le tableau ci-dessous:
Expression | Résultat |
---|---|
sizeof(sbyte) |
1 |
sizeof(byte) |
1 |
sizeof(short) |
2 |
sizeof(ushort) |
2 |
sizeof(int) |
4 |
sizeof(uint) |
4 |
sizeof(long) |
8 |
sizeof(ulong) |
8 |
sizeof(char) |
2 |
sizeof(float) |
4 |
sizeof(double) |
8 |
sizeof(bool) |
1 |
sizeof(decimal) |
16 |
Pour un type enum T
, le résultat de l’expression sizeof(T)
est une valeur constante égale à la taille de son type sous-jacent, comme indiqué ci-dessus. Pour tous les autres types d’opérande, l’opérateur sizeof
est spécifié dans §23.6.9.
12.8.20 Opérateurs vérifiés et non vérifiés
Les opérateurs checked
et unchecked
sont utilisés pour contrôler le contexte de vérification des dépassements pour les opérations arithmétiques et les conversions de type entier.
checked_expression
: 'checked' '(' expression ')'
;
unchecked_expression
: 'unchecked' '(' expression ')'
;
L’opérateur checked
évalue l’expression qu’il contient dans un contexte vérifié, et l’opérateur unchecked
évalue l’expression qu’il contient dans un contexte non vérifié. Une expression checked_expression ou unchecked_expression correspond exactement à une expression parenthesized_expression (§12.8.5), sauf que l'expression contenue est évaluée dans le contexte de vérification de dépassement donné.
Le contexte de vérification des dépassements peut également être contrôlé via les instructions checked
et unchecked
(§13.12).
Les opérations suivantes sont affectées par le contexte de contrôle de débordement établi par les opérateurs et les instructions contrôlés et non contrôlés :
- Les opérateurs prédéfinis
++
et--
(§12.8.16 et §12.9.6), lorsque l’opérande est de type integral ou enum. - L’opérateur unaire prédéfini
-
(§12.9.3), lorsque l’opérande est de type integral. - Les opérateurs binaires prédéfinis
+
,-
,*
, and/
(§12.10), lorsque les deux opérandes sont de types integral ou enum. - Conversions numériques explicites (§10.3.2) d’un type integral ou enum à un autre type integral ou enum, ou de
float
oudouble
à un type integral ou enum.
Lorsqu’une des opérations ci-dessus produit un résultat trop grand pour être représenté dans le type de destination, le contexte dans lequel l’opération est effectuée contrôle le comportement qui en résulte :
- Dans un contexte
checked
, si l’opération est une expression constante (§12.23), une erreur de compilation se produit. Sinon, lorsque l'opération est effectuée au moment de l'exécution, unSystem.OverflowException
est lancé. - Dans un contexte
unchecked
, le résultat est tronqué en supprimant tous les bits de poids fort qui ne correspondent pas au type de destination.
Pour les expressions non constantes (§12.23) (expressions évaluées au moment de l’exécution) qui ne sont pas entourées par des opérateurs checked
ou des instructions unchecked
, le contexte de vérification de dépassement de capacité par défaut est non vérifié, sauf si des facteurs externes (tels que les commutateurs du compilateur et la configuration de l’environnement d’exécution) impliquent une évaluation vérifiée.
Pour les expressions constantes (§12.23) (expressions pouvant être entièrement évaluées à la compilation), le contexte de vérification des dépassements par défaut est toujours vérifié. À moins qu'une expression constante ne soit explicitement placée dans un contexte unchecked
, les débordements qui se produisent pendant l'évaluation de l'expression au moment de la compilation provoquent toujours des erreurs au moment de la compilation.
Le corps d’une fonction anonyme n’est pas affecté par les contextes checked
ou unchecked
dans lesquels la fonction anonyme se trouve.
Exemple: Dans le code suivant
class Test { static readonly int x = 1000000; static readonly int y = 1000000; static int F() => checked(x * y); // Throws OverflowException static int G() => unchecked(x * y); // Returns -727379968 static int H() => x * y; // Depends on default }
aucune erreur de compilation n’est signalée puisque aucune des expressions ne peut être évaluée à la compilation. Au moment de l'exécution, la méthode
F
lance unSystem.OverflowException
et la méthodeG
renvoie -727379968 (les 32 bits inférieurs du résultat hors plage). Le comportement de la méthodeH
dépend du contexte de vérification des dépassements par défaut pour la compilation, mais il est soit identique àF
, soit identique àG
.exemple final
Exemple: Dans le code suivant
class Test { const int x = 1000000; const int y = 1000000; static int F() => checked(x * y); // Compile-time error, overflow static int G() => unchecked(x * y); // Returns -727379968 static int H() => x * y; // Compile-time error, overflow }
les débordements qui se produisent lors de l'évaluation des expressions constantes dans
F
etH
provoquent des erreurs compile-time car les expressions sont évaluées dans un contextechecked
. Un dépassement de capacité se produit également lors de l'évaluation de l'expression constante dansG
, mais comme l'évaluation a lieu dans un contexteunchecked
, le dépassement de capacité n'est pas signalé.exemple final
Les opérateurs checked
et unchecked
n’affectent le contexte de vérification des dépassements que pour les opérations textuellement contenues dans les guillemets « (
» et « )
». Les opérateurs n'ont aucun effet sur les membres de fonction qui sont invoqués à la suite de l'évaluation de l'expression contenue.
Exemple: Dans le code suivant
class Test { static int Multiply(int x, int y) => x * y; static int F() => checked(Multiply(1000000, 1000000)); }
l’utilisation de
checked
dans F n’affecte pas l’évaluation dex * y
dansMultiply
, doncx * y
est évalué dans le contexte de vérification des dépassements par défaut.exemple final
L’opérateur unchecked
est pratique pour écrire des constantes des types entiers signés en notation hexadécimale.
Exemple :
class Test { public const int AllBits = unchecked((int)0xFFFFFFFF); public const int HighBit = unchecked((int)0x80000000); }
Les deux constantes hexadécimales ci-dessus sont de type
uint
. Étant donné que les constantes se trouvent en dehors de la plage deint
, sans l'opérateurunchecked
, les conversions enint
produiraient des erreurs de compilation.exemple final
Remarque : les opérateurs et instructions
checked
etunchecked
permettent aux programmeurs de contrôler certains aspects de certains calculs numériques. Cependant, le comportement de certains opérateurs numériques dépend des types de données de leurs opérandes. Par exemple, la multiplication de deux décimales entraîne toujours une exception de dépassement, même lorsque l'opération se déroule dans une construction explicitement non vérifiée. De même, la multiplication de deux nombres flottants n'entraîne jamais d'exception en cas de dépassement de capacité, même dans le cadre d'une construction explicitement vérifiée. De plus, d’autres opérateurs ne sont jamais affectés par le mode de vérification, qu’il soit par défaut ou explicite. fin de la remarque
12.8.21 Expressions de valeur par défaut
Une expression de valeur par défaut est utilisée pour obtenir la valeur par défaut (§9.3) d’un type.
default_value_expression
: explictly_typed_default
| default_literal
;
explictly_typed_default
: 'default' '(' type ')'
;
default_literal
: 'default'
;
Un default_literal représente une valeur par défaut (§9.3). Il n’a pas de type, mais peut être converti en n’importe quel type par une conversion de littéral par défaut (§10.2.16).
Le résultat d'une expression de valeur par défaut est la valeur par défaut (§9.3) du type explicite dans une explictly_typed_default, ou le type cible de la conversion pour une default_value_expression.
Une default_value_expression est une expression constante (§12.23) si le type est l’un des suivants :
- un type de référence
- un paramètre de type dont on sait qu’il s’agit d’un type référence (§8.2) ;
- l’un des types valeur suivants :
sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,bool,
; ou - tout type d’énumération.
12.8.22 Allocation de la pile
Une expression d’allocation sur pile alloue un bloc de mémoire à partir de la pile d’exécution. La pile d’exécution est une zone de mémoire où sont stockées les variables locales. La pile d'exécution ne fait pas partie du tas géré. La mémoire utilisée pour le stockage des variables locales est automatiquement récupérée lorsque la fonction en cours se termine.
Les règles du contexte sécurisé pour une expression d’allocation sur pile sont décrites dans §16.4.12.7.
stackalloc_expression
: 'stackalloc' unmanaged_type '[' expression ']'
| 'stackalloc' unmanaged_type? '[' constant_expression? ']' stackalloc_initializer
;
stackalloc_initializer
: '{' stackalloc_initializer_element_list '}'
;
stackalloc_initializer_element_list
: stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
;
stackalloc_element_initializer
: expression
;
Le unmanaged_type (§8.8) indique le type des éléments qui seront stockés dans l’emplacement nouvellement alloué, et le expression indique le nombre de ces éléments. Pris ensemble, ceux-ci spécifient la taille d’allocation requise. Le type de expression doit être implicitement convertible en type int
.
Comme la taille d'une allocation de pile ne peut pas être négative, c'est une erreur compile-time de spécifier le nombre d'éléments sous la forme d'une constant_expression qui s'évalue à une valeur négative.
À l’exécution, si le nombre d’éléments à allouer est une valeur négative, le comportement est indéfini. S’il est zéro, aucune allocation n’est effectuée, et la valeur renvoyée est spécifique à l’implémentation. S'il n'y a pas assez de mémoire disponible pour allouer les éléments, un System.StackOverflowException
est lancé.
Lorsqu’un stackalloc_initializer est présent :
- Si le unmanaged_type est omis, il est déduit selon les règles du meilleur type commun (§12.6.3.15) pour l’ensemble des stackalloc_element_initializer.
- Si la constant_expression est omise, on en déduit qu'il s'agit du nombre d'initialisateurs de stackalloc_element_initializers.
- Si la constant_expression est présente, elle doit être égale au nombre de stackalloc_element_initializers.
Chaque stackalloc_element_initializer doit disposer d’une conversion implicite en unmanaged_type (§10.2). Les stackalloc_element_initializer initialisent les éléments dans la mémoire allouée dans l’ordre croissant, en commençant par l’élément à l’index zéro. En l’absence d’un stackalloc_initializer, le contenu de la mémoire nouvellement allouée est indéfini.
Si une stackalloc_expression apparaît directement en tant qu’expression d’initialisation d’une local_variable_declaration (§13.6.2), où le local_variable_type est soit un type pointeur (§23.3) soit déduit (var
), alors le résultat de la stackalloc_expression est un pointeur de type T*
(§23.9). Dans ce cas, la stackalloc_expression doit apparaître dans un code non sécurisé. Sinon, le résultat d’une stackalloc_expression est une instance de type Span<T>
, où T
est le unmanaged_type:
-
Span<T>
(§C.3) est un type ref struct (§16.2.3) qui présente un bloc de mémoire, ici le bloc alloué par la stackalloc_expression, comme une collection indexable d’éléments typés (T
). - La propriété
Length
du résultat renvoie le nombre d’éléments alloués. - L'indexeur du résultat (§15.9) renvoie une référence_variable (§9.5) à un élément du bloc alloué et sa portée est vérifiée.
Les initialisateurs d’allocation sur pile ne sont pas autorisés dans les blocs catch
ou finally
(§13.11).
Remarque : il n’existe aucun moyen de libérer explicitement la mémoire allouée en utilisant
stackalloc
. fin de la remarque
Tous les blocs de mémoire alloués à la pile et créés pendant l'exécution d'un membre de fonction sont automatiquement supprimés au retour de ce membre de fonction.
A l'exception de l'opérateur stackalloc
, C# ne fournit aucune construction prédéfinie pour gérer la mémoire non collectée. Ces services sont généralement fournis par des bibliothèques de classes de support ou importés directement depuis le système d’exploitation sous-jacent.
Exemple :
// Memory uninitialized Span<int> span1 = stackalloc int[3]; // Memory initialized Span<int> span2 = stackalloc int[3] { -10, -15, -30 }; // Type int is inferred Span<int> span3 = stackalloc[] { 11, 12, 13 }; // Error; result is int*, not allowed in a safe context var span4 = stackalloc[] { 11, 12, 13 }; // Error; no conversion from Span<int> to Span<long> Span<long> span5 = stackalloc[] { 11, 12, 13 }; // Converts 11 and 13, and returns Span<long> Span<long> span6 = stackalloc[] { 11, 12L, 13 }; // Converts all and returns Span<long> Span<long> span7 = stackalloc long[] { 11, 12, 13 }; // Implicit conversion of Span<T> ReadOnlySpan<int> span8 = stackalloc int[] { 10, 22, 30 }; // Implicit conversion of Span<T> Widget<double> span9 = stackalloc double[] { 1.2, 5.6 }; public class Widget<T> { public static implicit operator Widget<T>(Span<double> sp) { return null; } }
Dans le cas de
span8
,stackalloc
donne unSpan<int>
, qui est converti par un opérateur implicite enReadOnlySpan<int>
. De même, pourspan9
, leSpan<double>
résultant est converti en type défini par l’utilisateurWidget<double>
à l’aide de la conversion, comme indiqué. exemple final
12.8.23 L'opérateur nameof
Une nameof_expression est utilisée pour obtenir le nom d’une entité de programme sous forme de chaîne constante.
nameof_expression
: 'nameof' '(' named_entity ')'
;
named_entity
: named_entity_target ('.' identifier type_argument_list?)*
;
named_entity_target
: simple_name
| 'this'
| 'base'
| predefined_type
| qualified_alias_member
;
Parce que nameof
n’est pas un mot-clé, une nameof_expression est toujours syntaxiquement ambiguë avec un appel du nom simple nameof
. Pour des raisons de compatibilité, si une recherche de nom (§12.8.4) du nom nameof
réussit, l'expression est traitée comme une invocation_expression - que l'invocation soit valide ou non. Sinon, il s'agit d'un nameof_expression.
La recherche des noms simples et l’accès aux membres sont effectués sur la named_entity à la compilation, en suivant les règles décrites dans §12.8.4 et §12.8.7. Cependant, lorsque la recherche décrite dans §12.8.4 et §12.8.7 aboutit à une erreur parce qu’un membre d’instance a été trouvé dans un contexte statique, une nameof_expression ne produit pas cette erreur.
Le fait qu'une named_entity désignant un groupe de méthodes ait un type_argument_list constitue une erreur compile-time. C'est une erreur de compilation si une named_entity_target a le type dynamic
.
Une nameof_expression est une expression constante de type string
, et n’a aucun effet à l’exécution. Plus précisément, sa named_entity n’est pas évaluée, et est ignorée pour les besoins de l’analyse d’affectation définie (§9.4.4.22). Sa valeur est le dernier identifiant de la named_entity avant la type_argument_list finale optionnelle, transformée de la manière suivante :
- Le préfixe «
@
», s’il est utilisé, est supprimé. - Chaque unicode_escape_sequence est transformée en son caractère Unicode correspondant.
- Les formatting_characters éventuels sont supprimés.
Ce sont les mêmes transformations appliquées dans §6.4.3 lors de la vérification de l’égalité entre identifiants.
Exemple: Les résultats des diverses expressions
nameof
sont illustrés, en supposant un type génériqueList<T>
déclaré au sein de l’espace de nomsSystem.Collections.Generic
:using TestAlias = System.String; class Program { static void Main() { var point = (x: 3, y: 4); string n1 = nameof(System); // "System" string n2 = nameof(System.Collections.Generic); // "Generic" string n3 = nameof(point); // "point" string n4 = nameof(point.x); // "x" string n5 = nameof(Program); // "Program" string n6 = nameof(System.Int32); // "Int32" string n7 = nameof(TestAlias); // "TestAlias" string n8 = nameof(List<int>); // "List" string n9 = nameof(Program.InstanceMethod); // "InstanceMethod" string n10 = nameof(Program.GenericMethod); // "GenericMethod" string n11 = nameof(Program.NestedClass); // "NestedClass" // Invalid // string x1 = nameof(List<>); // Empty type argument list // string x2 = nameof(List<T>); // T is not in scope // string x3 = nameof(GenericMethod<>); // Empty type argument list // string x4 = nameof(GenericMethod<T>); // T is not in scope // string x5 = nameof(int); // Keywords not permitted // Type arguments not permitted for method group // string x6 = nameof(GenericMethod<Program>); } void InstanceMethod() { } void GenericMethod<T>() { string n1 = nameof(List<T>); // "List" string n2 = nameof(T); // "T" } class NestedClass { } }
Les parties potentiellement surprenantes de cet exemple sont la résolution de
nameof(System.Collections.Generic)
en « Generic » seulement au lieu du namespace complet, et denameof(TestAlias)
en « TestAlias » plutôt qu’en « String ». exemple final
12.8.24 Expressions de méthode anonyme
Une anonymous_method_expression est l’une des deux manières de définir une fonction anonyme. Celles-ci sont décrites plus en détail dans §12.19.
12.9 Opérateurs unaires
12.9.1 Général
Les opérateurs +
, -
, !
(la négation logique §12.9.4 uniquement), ~
, ++
, --
, cast et await
sont appelés les opérateurs unaires.
Note : L'opérateur postfixe d'annulation des nullités (§12.8.9),
!
, en raison de sa nature uniquement compile-time et non surchargeable, est exclu de la liste ci-dessus. fin de la remarque
unary_expression
: primary_expression
| '+' unary_expression
| '-' unary_expression
| logical_negation_operator unary_expression
| '~' unary_expression
| pre_increment_expression
| pre_decrement_expression
| cast_expression
| await_expression
| pointer_indirection_expression // unsafe code support
| addressof_expression // unsafe code support
;
pointer_indirection_expression (§23.6.2) et addressof_expression (§23.6.5) ne sont disponibles que dans un code non sécurisé (§23).
Si l'opérande d'une unary_expression a le type compile-time dynamic
, il est dynamiquement lié (§12.3.3). Dans ce cas, le type à la compilation de la unary_expression est dynamic
, et la résolution décrite ci-dessous aura lieu à l’exécution en utilisant le type à l’exécution de l’opérande.
12.9.2 Opérateur unaire plus
Pour une opération de la forme +x
, la résolution de surcharge de l’opérateur unaire (§12.4.4) est appliquée pour sélectionner une implémentation d’opérateur spécifique. L’opérande est converti en type du paramètre de l’opérateur sélectionné, et le type du résultat est le type de retour de l’opérateur. Les opérateurs unaire plus prédéfinis sont :
int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);
Pour chacun de ces opérateurs, le résultat est tout simplement la valeur de l’opérande.
Les formes augmentées (§12.4.8) des opérateurs unaires plus prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.9.3 Opérateur unaire moins
Pour une opération de la forme –x
, la résolution de surcharge de l’opérateur unaire (§12.4.4) est appliquée pour sélectionner une implémentation d’opérateur spécifique. L’opérande est converti en type du paramètre de l’opérateur sélectionné, et le type du résultat est le type de retour de l’opérateur. Les opérateurs unaire moins prédéfinis sont :
Négation entière :
int operator –(int x); long operator –(long x);
Le résultat est calculé en soustrayant
X
de zéro. Si la valeur deX
est la plus petite valeur représentable du type de l’opérande (−2³¹ pourint
ou −2⁶³ pourlong
), alors la négation mathématique deX
n’est pas représentable dans le type de l’opérande. Si cela se produit dans un contextechecked
, unSystem.OverflowException
est lancé ; si cela se produit dans un contexteunchecked
, le résultat est la valeur de l'opérande et le dépassement de capacité n'est pas signalé.Si l’opérande de l’opérateur de négation est de type
uint
, il est converti en typelong
, et le type du résultat estlong
. Une exception est la règle qui permet que la valeurint
−2147483648
(−2⁶³) soit écrite comme un littéral entier décimal (§6.4.5.3).Si l’opérande de l’opérateur de négation est de type
ulong
, une erreur au moment de la compilation se produit. Une exception est la règle qui permet que la valeurlong
−9223372036854775808
(−2⁶³) soit écrite comme un littéral entier décimal (§6.4.5.3)Négation en virgule flottante :
float operator –(float x); double operator –(double x);
Le résultat est la valeur de
X
avec son signe inversé. Six
estNaN
, le résultat est égalementNaN
.Négation décimale :
decimal operator –(decimal x);
Le résultat est calculé en soustrayant
X
de zéro. La négation décimale équivaut à utiliser l’opérateur unaire moins de typeSystem.Decimal
.
Les formes augmentées (§12.4.8) des opérateurs unaires moins prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.9.4 Opérateur de négation logique
Pour une opération de la forme !x
, la résolution de surcharge de l’opérateur unaire (§12.4.4) est appliquée pour sélectionner une implémentation d’opérateur spécifique. L’opérande est converti en type du paramètre de l’opérateur sélectionné, et le type du résultat est le type de retour de l’opérateur. Un seul opérateur de négation logique prédéfini existe :
bool operator !(bool x);
Cet opérateur calcule la négation logique de l’opérande : si l’opérande est true
, le résultat est false
. Si l’opérande est false
, le résultat est true
.
Les formes décalées (§12.4.8) de l'opérateur de négation logique prédéfini non décalé défini ci-dessus sont également prédéfinies.
Remarque : les opérateurs de négation logique préfixe et de null-forgiving postfixe (§12.8.9), bien que représentés par le même token lexical (!
), sont distincts. fin de la remarque
12.9.5 Opérateur complément binaire
Pour une opération de la forme ~x
, la résolution de surcharge de l’opérateur unaire (§12.4.4) est appliquée pour sélectionner une implémentation d’opérateur spécifique. L’opérande est converti en type du paramètre de l’opérateur sélectionné, et le type du résultat est le type de retour de l’opérateur. Les opérateurs de complément bit à bit prédéfinis sont :
int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);
Pour chacun de ces opérateurs, le résultat de l’opération est le complément bit à bit de x
.
Chaque type d’énumération E
fournit implicitement l’opérateur de complément bit à bit suivant :
E operator ~(E x);
Le résultat de l’évaluation de ~x
, où X
est une expression d’un type d’énumération E
avec un type sous-jacent U
, est exactement le même que l’évaluation de (E)(~(U)x)
, sauf que la conversion en E
est toujours effectuée comme si elle était dans un contexte unchecked
(§12.8.20).
Les formes levées (§12.4.8) des opérateurs de complément bitwise prédéfinis non levés définis ci-dessus sont également prédéfinies.
12.9.6 Opérateurs préfixés d’incrémentation et de décrémentation
pre_increment_expression
: '++' unary_expression
;
pre_decrement_expression
: '--' unary_expression
;
L’opérande d’une opération d’incrémentation ou de décrémentation préfixée doit être une expression classée comme une variable, un accès à une propriété ou un accès par indexeur. Le résultat de l’opération est une valeur du même type que l’opérande.
Si l’opérande d’une opération d’incrémentation ou de décrémentation de préfixe est une propriété ou un accès indexeur, la propriété ou l’indexeur doit avoir à la fois un accesseur 'get' et un accesseur 'set'. Dans le cas contraire, une erreur de liaison survient.
La résolution de surcharge des opérateurs unaires (§12.4.4) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Des opérateurs prédéfinis ++
et --
existent pour les types suivants : sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
et tout type énuméré. Les opérateurs prédéfinis ++
renvoient la valeur produite en ajoutant 1
à l’opérande, et les opérateurs prédéfinis --
renvoient la valeur produite en soustrayant 1
à l’opérande. Dans un contexte checked
, si le résultat de cette addition ou soustraction est en dehors de la plage du type de résultat et que le type de résultat est un type intégral ou un type enum, un System.OverflowException
est lancé.
Il doit exister une conversion implicite du type de retour de l’opérateur unaire sélectionné vers le type de la unary_expression, sinon une erreur de compilation se produit.
Le traitement à l’exécution d’une opération d’incrémentation ou de décrémentation préfixée de la forme ++x
ou --x
se compose des étapes suivantes :
- Si
x
est classé comme une variable :-
x
est évalué pour produire la variable. - La valeur de
x
est convertie en type de l’opérande de l’opérateur sélectionné et l’opérateur est appelé avec cette valeur comme argument. - La valeur renvoyée par l’opérateur est convertie en le type de
x
. La valeur résultante est stockée à l’emplacement donné par l’évaluation dex
et devient le résultat de l’opération.
-
- Si
x
est classé comme un accès de propriété ou d'indexeur :- L’expression d’instance (si
x
n’est passtatic
) et la liste d’arguments (six
est un accès à un indexeur) associées àx
sont évaluées, et les résultats sont utilisés dans les appels subséquents de l’accesseur get et set. - L'accesseur get de
x
est invoqué. - La valeur renvoyée par l’accesseur get est convertie en type de l’opérande de l’opérateur sélectionné et l’opérateur est appelé avec cette valeur comme argument.
- La valeur renvoyée par l’opérateur est convertie en le type de
x
. L’accesseur set dex
est appelé avec cette valeur en tant qu'argument de valeur. - Cette valeur devient également le résultat de l’opération.
- L’expression d’instance (si
Les opérateurs ++
et --
supportent également la notation suffixe (§12.8.16). Le résultat de x++
ou x--
est la valeur de x
avant l’opération, tandis que le résultat de ++x
ou --x
est la valeur de x
après l’opération. Dans les deux cas, x
conserve la même valeur après l’opération.
Une implémentation de l'opérateur ++
ou de l'opérateur --
peut être invoquée en utilisant soit la notation postfixe, soit la notation préfixe. Il n’est pas possible d’avoir des implémentations d’opérateurs distinctes pour les deux notations.
Les formes augmentées (§12.4.8) des opérateurs d'incrémentation et de décrémentation prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.9.7 Expressions coulées
Une cast_expression est utilisée pour convertir explicitement une expression en un type donné.
cast_expression
: '(' type ')' unary_expression
;
Une expression cast_expression de la forme (T)E
, où T
est un type et E
une expression unary_expression, effectue une conversion explicite (§10.3) de la valeur de E
vers le type T
. Si aucune conversion explicite n’existe de E
à T
, une erreur de liaison se produit. Sinon, le résultat est la valeur produite par la conversion explicite. Le résultat est toujours classé comme une valeur, même si E
désigne une variable.
La grammaire d’une cast_expression conduit à certaines ambiguïtés syntaxiques.
Exemple: L’expression
(x)–y
peut être interprétée soit comme une expression de conversion (une conversion de–y
vers le typex
), soit comme une expression additive combinée à une expression entre parenthèses (qui calcule la valeurx – y
). exemple final
Pour résoudre les ambiguïtés de cast_expression, la règle suivante existe : une séquence d’un ou de plusieurs tokens (§6.4) enfermée entre parenthèses est considérée comme le début d’une cast_expression uniquement si au moins l’une des conditions suivantes est vraie :
- La séquence de jetons est une grammaire correcte pour un type, mais pas pour une expression.
- La séquence de jetons est correcte pour un type, et le jeton immédiatement après la parenthèse fermante est le jeton «
~
», le jeton «!
», le jeton «(
», un identificateur (§6.4.3), un littéral (§6.4.5), ou tout mot clé (§6.4.4) saufas
etis
.
Le terme « grammaire correcte » ci-dessus signifie uniquement que la séquence d'éléments doit être conforme aux règles grammaticales spécifiques. Elle ne prend pas en compte le sens réel de tout identifiant constituant.
Exemple: si
x
ety
sont des identifiants, alorsx.y
est une grammaire correcte pour un type, même six.y
ne dénote pas réellement un type. exemple final
Note : de la règle de désambiguïsation, il résulte que, si
x
ety
sont des identificateurs,(x)y
,(x)(y)
et(x)(-y)
sont des cast_expressions, mais(x)-y
ne l'est pas, même six
identifie un type. Cependant, six
est un mot-clé qui identifie un type prédéfini (tel queint
), alors les quatre formes sont des cast_expression (car un tel mot-clé ne pourrait pas être une expression en lui-même). fin de la remarque
12.9.8 Expressions d'attente
12.9.8.1 Général
L’opérateur await
est utilisé pour suspendre l’évaluation de la fonction async englobante jusqu’à ce que l’opération asynchrone représentée par l’opérande soit terminée.
await_expression
: 'await' unary_expression
;
Une await_expression n'est autorisée que dans le corps d'une fonction asynchrone (§15.15). À l'intérieur de la fonction asynchrone englobante la plus proche, une await_expression ne doit pas apparaître à ces endroits :
- À l’intérieur d’une fonction anonyme imbriquée (non async)
- A l'intérieur du bloc d'un lock_statement.
- Dans une conversion de fonction anonyme en un type d’arbre d’expressions (§10.7.3)
- Dans un contexte non sécurisé
Note : Une expression await_expression ne peut pas apparaître dans la plupart des endroits à l'intérieur d'une expression query_expression, parce que celles-ci sont syntaxiquement transformées pour utiliser des expressions lambda non asynchrones. fin de la remarque
À l’intérieur d’une fonction async, await
ne doit pas être utilisé comme un available_identifier bien que l’identifiant verbatim @await
puisse être utilisé. Il n’existe donc aucune ambiguïté syntaxique entre les await_expression et diverses expressions impliquant des identifiants. En dehors des fonctions async, await
agit comme un identifiant normal.
L'opérande d'une await_expression est appelé tâche. Il représente une opération asynchrone qui peut être terminée ou non au moment de l’évaluation de l’await_expression. Le but de l’opérateur await
est de suspendre l’exécution de la fonction async englobante jusqu’à ce que la tâche en attente soit terminée, puis d’en obtenir le résultat.
12.9.8.2 Expressions attendues
La tâche d'une await_expression doit être attendue. Une expression t
est attendue si l'une des conditions suivantes est remplie :
t
est de type compile-timedynamic
.-
t
dispose d’une méthode d’instance ou d’extension accessible appeléeGetAwaiter
sans paramètres et sans paramètres de type, et d’un type de retourA
pour lequel toutes les conditions suivantes sont remplies :A
implémente l'interfaceSystem.Runtime.CompilerServices.INotifyCompletion
(appelé ci-aprèsINotifyCompletion
pour plus de simplicité)-
A
possède une propriété d’instance accessible et lisibleIsCompleted
de typebool
-
A
dispose d’une méthode d’instance accessibleGetResult
sans paramètres et sans paramètres de type
Le but de la méthode GetAwaiter
est d'obtenir un awaiter pour la tâche. Le type A
est appelé le type d'attente pour l'expression await.
Le but de la propriété IsCompleted
est de déterminer si la tâche est déjà terminée. Si c’est le cas, il n’est pas nécessaire de suspendre l’évaluation.
Le but de la méthode INotifyCompletion.OnCompleted
est d’inscrire une « continuation » à la tâche ; c’est-à-dire un délégué (de type System.Action
) qui sera appelé une fois la tâche terminée.
Le but de la méthode GetResult
est d’obtenir le résultat de la tâche une fois celle-ci terminée. Ce résultat peut être un achèvement réussi, éventuellement avec une valeur de résultat, ou il peut s'agir d'une exception qui est levée par la méthode GetResult
.
12.9.8.3 Classification des expressions await
L’expression await t
est classée de la même manière que l’expression (t).GetAwaiter().GetResult()
. Ainsi, si le type de retour de GetResult
est void
, l’await_expression n’a pas de classification de type. Si elle a un type de non-void
retour T
, l'expression await_expression est classée comme une valeur de type T
.
12.9.8.4 Évaluation à l'exécution des expressions await
À l’exécution, l’expression await t
est évaluée comme suit :
- Un awaiter
a
est obtenu en évaluant l'expression(t).GetAwaiter()
. - Un
bool
b
est obtenu en évaluant l'expression(a).IsCompleted
. - Si
b
estfalse
, l’évaluation dépend de sia
implémente l’interfaceSystem.Runtime.CompilerServices.ICriticalNotifyCompletion
(ci-après connue sous le nom deICriticalNotifyCompletion
pour faire court). Cette vérification est effectuée lors de la liaison ; c’est-à-dire à l’exécution sia
a le type de compilationdynamic
, et à la compilation dans le cas contraire. Soitr
le délégué de reprise (§15.15) :- Si
a
n’implémente pasICriticalNotifyCompletion
, alors l’expression((a) as INotifyCompletion).OnCompleted(r)
est évaluée. - Si
a
implémenteICriticalNotifyCompletion
, alors l’expression((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r)
est évaluée. - L’évaluation est ensuite suspendue et le contrôle est rendu à l’appelant actuel de la fonction async.
- Si
- Soit immédiatement après (si
b
étaittrue
), soit lors d’un appel ultérieur du resumption delegate (sib
étaitfalse
), l’expression(a).GetResult()
est évaluée. Si elle renvoie une valeur, cette valeur est le résultat de l’await_expression. Sinon, le résultat n’est rien.
L'implémentation des méthodes d'interface INotifyCompletion.OnCompleted
et ICriticalNotifyCompletion.UnsafeOnCompleted
par un awaiter doit entraîner l'appel du délégué r
au plus une fois. Sinon, le comportement de la fonction asynchrone englobante est indéfini.
12.10 opérateurs arithmétiques
12.10.1 Général
Les opérateurs *
, /
, %
, +
, -
sont appelés les opérateurs arithmétiques.
multiplicative_expression
: unary_expression
| multiplicative_expression '*' unary_expression
| multiplicative_expression '/' unary_expression
| multiplicative_expression '%' unary_expression
;
additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;
Si un opérande d’un opérateur arithmétique a le type à la compilation dynamic
, alors l’expression est liée dynamiquement (§12.3.3). Dans ce cas, le type à la compilation de l’expression est dynamic
, et la résolution décrite ci-dessous s’effectuera à l’exécution en utilisant le type d’exécution des opérandes ayant le type à la compilation dynamic
.
12.10.2 opérateur de multiplication
Pour une opération de la forme x * y
, la résolution de surcharge de l’opérateur binaire (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur.
Les opérateurs de multiplication prédéfinis sont listés ci-dessous. Tous les opérateurs calculent le produit de x
et y
.
Multiplication entière :
int operator *(int x, int y); uint operator *(uint x, uint y); long operator *(long x, long y); ulong operator *(ulong x, ulong y);
Dans un contexte
checked
, si le produit est en dehors de la plage du type de résultat, unSystem.OverflowException
est lancé. Dans un contexteunchecked
, les débordements ne sont pas signalés et tous les bits significatifs de poids fort situés en dehors de la plage du type de résultat sont rejetés.Multiplication en virgule flottante :
float operator *(float x, float y); double operator *(double x, double y);
Le produit est calculé selon les règles de l’arithmétique IEC 60559. Le tableau suivant répertorie les résultats de toutes les combinaisons possibles de valeurs finies non nulles, de zéros, d’infinis et de NaN. Dans le tableau,
x
ety
sont des valeurs finies positives.z
est le résultat dex * y
, arrondi à la valeur représentable la plus proche. Si l’amplitude du résultat est trop grande pour le type de destination,z
est un infini. En raison de l’arrondi,z
peut être nul même si nix
niy
ne sont nuls.+y
-y
+0
-0
+∞
-∞
NaN
+x
+z
-z
+0
-0
+∞
-∞
NaN
-x
-z
+z
-0
+0
-∞
+∞
NaN
+0
+0
-0
+0
-0
NaN
NaN
NaN
-0
-0
+0
-0
+0
NaN
NaN
NaN
+∞
+∞
-∞
NaN
NaN
+∞
-∞
NaN
-∞
-∞
+∞
NaN
NaN
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(Sauf indication contraire, dans les tables à virgule flottante dans §12.10.2–§12.10.6 l’utilisation de "
+
" signifie que la valeur est positive ; l’utilisation de «-
» signifie que la valeur est négative ; et le manque de signe signifie que la valeur peut être positive ou négative ou n’a aucun signe (NaN).)Multiplication décimale :
decimal operator *(decimal x, decimal y);
Si l’ampleur de la valeur résultante est trop grande pour être représentée au format décimal, l'erreur
System.OverflowException
est générée. À cause de l'arrondi, le résultat peut être zéro même si aucun des opérandes n'est zéro. L'échelle du résultat, avant tout arrondi, est la somme des échelles des deux opérandes. La multiplication décimale équivaut à utiliser l’opérateur de multiplication de typeSystem.Decimal
.
Les formes augmentées (§12.4.8) des opérateurs de multiplication prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.10.3 opérateur de division
Pour une opération de la forme x / y
, la résolution de surcharge de l’opérateur binaire (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur.
Les opérateurs de division prédéfinis sont listés ci-dessous. Tous les opérateurs calculent le quotient de x
et y
.
Division entière :
int operator /(int x, int y); uint operator /(uint x, uint y); long operator /(long x, long y); ulong operator /(ulong x, ulong y);
Si la valeur de l'opérande de droite est zéro, un
System.DivideByZeroException
est lancé.La division arrondit le résultat à zéro. Ainsi, la valeur absolue du résultat est le plus grand entier possible qui soit inférieur ou égal à la valeur absolue du quotient des deux opérandes. Le résultat est nul ou positif lorsque les deux opérandes ont le même signe et nul ou négatif lorsqu’ils ont des signes opposés.
Si l'opérande de gauche est la plus petite valeur
int
oulong
représentable et que l'opérande de droite est–1
, un dépassement de capacité se produit. Dans un contextechecked
, unSystem.ArithmeticException
(ou une sous-classe de celui-ci) est déclenché. Dans un contexteunchecked
, l'implémentation définit si unSystem.ArithmeticException
(ou une sous-classe de celui-ci) est lancé ou si le dépassement n'est pas signalé, la valeur résultante étant celle de l'opérande de gauche.Division en virgule flottante :
float operator /(float x, float y); double operator /(double x, double y);
Le quotient est calculé selon les règles de l’arithmétique IEC 60559. Le tableau suivant répertorie les résultats de toutes les combinaisons possibles de valeurs finies non nulles, de zéros, d’infinis et de NaN. Dans le tableau,
x
ety
sont des valeurs finies positives.z
est le résultat dex / y
, arrondi à la valeur représentable la plus proche.+y
-y
+0
-0
+∞
-∞
NaN
+x
+z
-z
+∞
-∞
+0
-0
NaN
-x
-z
+z
-∞
+∞
-0
+0
NaN
+0
+0
-0
NaN
NaN
+0
-0
NaN
-0
-0
+0
NaN
NaN
-0
+0
NaN
+∞
+∞
-∞
+∞
-∞
NaN
NaN
NaN
-∞
-∞
+∞
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
Division décimale :
decimal operator /(decimal x, decimal y);
Si la valeur de l'opérande de droite est zéro, un
System.DivideByZeroException
est lancé. Si l’ampleur de la valeur résultante est trop grande pour être représentée au format décimal, l'erreurSystem.OverflowException
est générée. En raison de l'arrondi, le résultat peut être nul même si le premier opérande n'est pas nul. L’échelle du résultat, avant tout arrondissement, est l'échelle la plus proche de l’échelle préférée qui préservera un résultat égal au résultat exact. L’échelle privilégiée est celle dex
moins celle dey
.La division décimale équivaut à utiliser l’opérateur de division de type
System.Decimal
.
Les formes augmentées (§12.4.8) des opérateurs de division prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.10.4 Opérateur de reste
Pour une opération de la forme x % y
, la résolution de surcharge de l’opérateur binaire (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur.
Les opérateurs de reste prédéfinis sont listés ci-dessous. Les opérateurs calculent tous le reste de la division entre x
et y
.
Reste entier :
int operator %(int x, int y); uint operator %(uint x, uint y); long operator %(long x, long y); ulong operator %(ulong x, ulong y);
Le résultat de
x % y
est la valeur produite parx – (x / y) * y
. Siy
est zéro, unSystem.DivideByZeroException
est lancé.Si l'opérande de gauche est la plus petite valeur
int
oulong
et que l'opérande de droite est–1
, unSystem.OverflowException
est lancé si et seulement six / y
lancerait une exception.Reste en virgule flottante :
float operator %(float x, float y); double operator %(double x, double y);
Le tableau suivant répertorie les résultats de toutes les combinaisons possibles de valeurs finies non nulles, de zéros, d’infinis et de NaN. Dans le tableau,
x
ety
sont des valeurs finies positives.z
est le résultat dex % y
et est calculé commex – n * y
, où n est le plus grand entier possible qui soit inférieur ou égal àx / y
. Cette méthode de calcul du reste est analogue à celle utilisée pour les opérandes entiers, mais diffère de la définition IEC 60559 (dans laquellen
est l’entier le plus proche dex / y
).+y
-y
+0
-0
+∞
-∞
NaN
+x
+z
+z
NaN
NaN
+x
+x
NaN
-x
-z
-z
NaN
NaN
-x
-x
NaN
+0
+0
+0
NaN
NaN
+0
+0
NaN
-0
-0
-0
NaN
NaN
-0
-0
NaN
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
Reste décimal :
decimal operator %(decimal x, decimal y);
Si la valeur de l'opérande de droite est zéro, un
System.DivideByZeroException
est lancé. Il est défini par l'implémentation lorsqu'unSystem.ArithmeticException
(ou une sous-classe de celui-ci) est lancé. Une implémentation conforme ne doit pas générer d’exception pourx % y
dans tout cas oùx / y
ne génère pas d’exception. L'échelle du résultat, avant tout arrondissement, est la plus grande des échelles des deux opérandes, et le signe du résultat, s'il est non nul, est identique à celui dex
.Le reste décimal est équivalent à l'utilisation de l'opérateur de reste de type
System.Decimal
.Remarque : ces règles garantissent que, pour tous les types, le résultat n’a jamais le signe opposé à celui de l’opérande de gauche. fin de la remarque
Les formes augmentées (§12.4.8) des opérateurs de reste prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.10.5 opérateur d’addition
Pour une opération de la forme x + y
, la résolution de surcharge de l’opérateur binaire (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur.
Les opérateurs d’addition prédéfinis sont listés ci-dessous. Pour les types numériques et d’énumération, les opérateurs d’addition prédéfinis calculent la somme des deux opérandes. Lorsque un ou les deux opérandes sont de type string
, les opérateurs d’addition prédéfinis concatènent la représentation en chaîne des opérandes.
Addition entière :
int operator +(int x, int y); uint operator +(uint x, uint y); long operator +(long x, long y); ulong operator +(ulong x, ulong y
Dans un contexte
checked
, si la somme est en dehors de la plage du type de résultat, unSystem.OverflowException
est émis. Dans un contexteunchecked
, les débordements ne sont pas signalés et tous les bits significatifs de poids fort situés en dehors de la plage du type de résultat sont rejetés.Addition en virgule flottante :
float operator +(float x, float y); double operator +(double x, double y);
La somme est calculée selon les règles de l’arithmétique IEC 60559. Le tableau suivant répertorie les résultats de toutes les combinaisons possibles de valeurs finies non nulles, de zéros, d’infinis et de NaN. Dans le tableau,
x
ety
sont des valeurs finies non nulles, etz
est le résultat dex + y
. Six
ety
ont la même grandeur, mais des signes opposés,z
est un zéro positif. Six + y
est trop grand pour être représenté dans le type de destination,z
est un infini avec le même signe quex + y
.y
+0
-0
+∞
-∞
NaN
x
z
x
x
+∞
-∞
NaN
+0
y
+0
+0
+∞
–∞
NaN
-0
y
+0
-0
+∞
-∞
NaN
+∞
+∞
+∞
+∞
+∞
NaN
NaN
-∞
-∞
-∞
-∞
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
Addition décimale :
decimal operator +(decimal x, decimal y);
Si l’ampleur de la valeur résultante est trop grande pour être représentée au format décimal, l'erreur
System.OverflowException
est générée. L’échelle du résultat, avant tout arrondissement, est la plus grande échelle des deux opérandes.L’addition décimale équivaut à utiliser l’opérateur d’addition de type
System.Decimal
.Addition par énumération. Chaque type d’énumération fournit implicitement les opérateurs prédéfinis suivants, où
E
est le type enum, etU
est le type sous-jacent deE
:E operator +(E x, U y); E operator +(U x, E y);
Au moment de l'exécution, ces opérateurs sont évalués exactement comme
(E)((U)x + (U)y
).Concaténation de chaînes :
string operator +(string x, string y); string operator +(string x, object y); string operator +(object x, string y);
Ces surcharges de l’opérateur binaire
+
effectuent la concaténation de chaînes. Si un opérande de concaténation de chaînes estnull
, une chaîne vide est substituée. Sinon, tout opérande nonstring
est converti en sa représentation sous forme de chaîne en appelant la méthode virtuelleToString
héritée du typeobject
. SiToString
renvoienull
, une chaîne vide est substituée.Exemple :
class Test { static void Main() { string s = null; Console.WriteLine("s = >" + s + "<"); // Displays s = >< int i = 1; Console.WriteLine("i = " + i); // Displays i = 1 float f = 1.2300E+15F; Console.WriteLine("f = " + f); // Displays f = 1.23E+15 decimal d = 2.900m; Console.WriteLine("d = " + d); // Displays d = 2.900 } }
Le résultat affiché dans les commentaires est celui typique d’un système US-English. Le résultat précis peut dépendre des paramètres régionaux de l’environnement d’exécution. L’opérateur de concaténation de chaînes lui-même se comporte de la même manière dans chaque cas, mais les méthodes
ToString
appelées implicitement lors de l’exécution peuvent être affectées par les paramètres régionaux.exemple final
Le résultat de l'opérateur de concaténation de chaînes de caractères est un
string
composé des caractères de l'opérande gauche suivis des caractères de l'opérande droit. L’opérateur de concaténation de chaînes ne renvoie jamais une valeurnull
. Une exceptionSystem.OutOfMemoryException
peut être levée s'il n'y a pas suffisamment de mémoire disponible pour allouer la chaîne résultante.Combinaison de délégués. Chaque type de délégué fournit implicitement l’opérateur prédéfini suivant, où
D
est le type de délégué :D operator +(D x, D y);
Si le premier opérande est
null
, le résultat de l’opération est la valeur du second opérande (même si celle-ci est égalementnull
). Sinon, si le second opérande estnull
, le résultat de l’opération est la valeur du premier opérande. Sinon, le résultat de l’opération est une nouvelle instance de délégué dont la liste d’appel se compose des éléments de la liste d’appel du premier opérande, suivis des éléments de la liste d’appel du second opérande. C'est-à-dire que la liste d'invocation du délégué résultant est la concaténation des listes d'invocation des deux opérandes.Remarque : pour des exemples de combinaison de délégués, veuillez consulter §12.10.6 et §20.6. Comme
System.Delegate
n’est pas un type de délégué, l’opérateur + n’est pas défini pour lui. end note
Les formes augmentées (§12.4.8) des opérateurs d'addition prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.10.6 Opérateur soustraction
Pour une opération de la forme x – y
, la résolution de surcharge de l’opérateur binaire (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur.
Les opérateurs de soustraction prédéfinis sont listés ci-dessous. Tous les opérateurs soustraient y
de x
.
Soustraction entière :
int operator –(int x, int y); uint operator –(uint x, uint y); long operator –(long x, long y); ulong operator –(ulong x, ulong y
Dans un contexte
checked
, si la différence se situe en dehors de la plage du type de résultat, unSystem.OverflowException
est émis. Dans un contexteunchecked
, les débordements ne sont pas signalés et tous les bits significatifs de poids fort situés en dehors de la plage du type de résultat sont rejetés.Soustraction en virgule flottante :
float operator –(float x, float y); double operator –(double x, double y);
La différence est calculée selon les règles de l’arithmétique IEC 60559. Le tableau suivant répertorie les résultats de toutes les combinaisons possibles de valeurs finies non nulles, de zéros, d’infinis et de NaN. Dans le tableau,
x
ety
sont des valeurs finies non nulles, etz
est le résultat dex – y
. Six
ety
sont égaux,z
est un zéro positif. Six – y
est trop grand pour être représenté dans le type de destination,z
est un infini avec le même signe quex – y
.y
+0
-0
+∞
-∞
NaN
x
z
x
x
-∞
+∞
NaN
+0
-y
+0
+0
-∞
+∞
NaN
-0
-y
-0
+0
-∞
+∞
NaN
+∞
+∞
+∞
+∞
NaN
+∞
NaN
-∞
-∞
-∞
-∞
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(Dans le tableau ci-dessus, les entrées
-y
désignent la négation dey
, et non le fait que la valeur soit négative.)Soustraction décimale :
decimal operator –(decimal x, decimal y);
Si l’ampleur de la valeur résultante est trop grande pour être représentée au format décimal, l'erreur
System.OverflowException
est générée. L’échelle du résultat, avant tout arrondissement, est la plus grande échelle des deux opérandes.La soustraction décimale équivaut à utiliser l’opérateur de soustraction de type
System.Decimal
.Soustraction d'énumération. Chaque type d’énumération fournit implicitement l’opérateur prédéfini suivant, où
E
est le type enum, etU
est le type sous-jacent deE
:U operator –(E x, E y);
Cet opérateur est évalué exactement comme
(U)((U)x – (U)y)
. En d’autres termes, l’opérateur calcule la différence entre les valeurs ordinales dex
ety
, et le type du résultat est le type sous-jacent de l’énumération.E operator –(E x, U y);
Cet opérateur est évalué exactement comme
(E)((U)x – y)
. Autrement dit, l’opérateur soustrait une valeur du type sous-jacent de l’énumération, produisant une valeur de l’énumération.Suppression d'un délégué. Chaque type de délégué fournit implicitement l’opérateur prédéfini suivant, où
D
est le type de délégué :D operator –(D x, D y);
La sémantique est la suivante :
- Si le premier opérande a la valeur
null
, le résultat de l’opération estnull
. - Sinon, si le second opérande est
null
, le résultat de l’opération est la valeur du premier opérande. - Sinon, les deux opérandes représentent des listes d'invocation non vides (§20.2).
- Si les listes sont comparables, comme déterminé par l'opérateur d'égalité des délégués (§12.12.9), le résultat de l'opération est
null
. - Sinon, le résultat de l’opération est une nouvelle liste d’appel constituée de la liste du premier opérande dont les entrées du second opérande ont été retirées, à condition que la liste du second opérande soit une sous-liste de celle du premier. (Pour déterminer l’égalité des sous-listes, les entrées correspondantes sont comparées comme pour l’opérateur d’égalité de délégués.) Si la liste du second opérande correspond à plusieurs sous-listes d’entrées contiguës dans celle du premier opérande, la dernière sous-liste correspondante d’entrées contiguës est supprimée.
- Sinon, le résultat de l'opération est la valeur de l'opérande de gauche.
- Si les listes sont comparables, comme déterminé par l'opérateur d'égalité des délégués (§12.12.9), le résultat de l'opération est
Aucune des listes des opérandes (le cas échéant) n’est modifiée dans le processus.
Exemple :
delegate void D(int x); class C { public static void M1(int i) { ... } public static void M2(int i) { ... } } class Test { static void Main() { D cd1 = new D(C.M1); D cd2 = new D(C.M2); D list = null; list = null - cd1; // null list = (cd1 + cd2 + cd2 + cd1) - null; // M1 + M2 + M2 + M1 list = (cd1 + cd2 + cd2 + cd1) - cd1; // M1 + M2 + M2 list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2); // M2 + M1 list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd2); // M1 + M1 list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd1); // M1 + M2 list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd1); // M1 + M2 + M2 + M1 list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2 + cd2 + cd1); // null } }
exemple final
- Si le premier opérande a la valeur
Les formes levées (§12.4.8) des opérateurs de soustraction prédéfinis non levés définis ci-dessus sont également prédéfinies.
12.11 Opérateurs de décalage
Les opérateurs <<
et >>
sont utilisés pour réaliser des opérations de décalage de bits.
shift_expression
: additive_expression
| shift_expression '<<' additive_expression
| shift_expression right_shift additive_expression
;
Si un opérande d'une expression shift_expression a le type compile-time dynamic
, l'expression est dynamiquement liée (§12.3.3). Dans ce cas, le type à la compilation de l’expression est dynamic
, et la résolution décrite ci-dessous s’effectuera à l’exécution en utilisant le type d’exécution des opérandes ayant le type à la compilation dynamic
.
Pour une opération de la forme x << count
ou x >> count
, la résolution de surcharge de l’opérateur binaire (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur.
Lors de la déclaration d'un opérateur shift surchargé, le type du premier opérande doit toujours être la classe ou la structure contenant la déclaration de l'opérateur, et le type du second opérande doit toujours être int
.
Les opérateurs de décalage prédéfinis sont listés ci-dessous.
Décalage vers la gauche :
int operator <<(int x, int count); uint operator <<(uint x, int count); long operator <<(long x, int count); ulong operator <<(ulong x, int count);
L’opérateur
<<
décalex
vers la gauche d’un nombre de bits calculé comme décrit ci-dessous.Les bits les plus significatifs en dehors de la plage du type de résultat de
x
sont supprimés, les bits restants sont décalés vers la gauche, et les positions de bits vides les moins significatifs sont remplies de zéros.Décalage vers la droite :
int operator >>(int x, int count); uint operator >>(uint x, int count); long operator >>(long x, int count); ulong operator >>(ulong x, int count);
L'opérateur
>>
déplacex
vers la droite par un certain nombre de bits calculés comme décrit ci-dessous.Lorsque
x
est de typeint
oulong
, les bits les moins significatifs dex
sont supprimés, les bits restants sont décalés vers la droite, et les positions de bits vides les plus significatifs sont mises à zéro six
est non négatif et mises à un six
est négatif.Lorsque
x
est de typeuint
ouulong
, les bits les moins significatifs dex
sont supprimés, les bits restants sont décalés vers la droite, et les positions de bits vides les plus significatifs sont remplies de zéros.
Pour les opérateurs prédéfinis, le nombre de bits à décaler est calculé comme suit:
- Lorsque le type de
x
estint
ouuint
, le nombre de décalages est donné par les cinq bits de poids faible decount
. En d'autres termes, le nombre de décalages est calculé à partir decount & 0x1F
. - Lorsque le type de
x
estlong
ouulong
, le nombre de décalages est donné par les six bits de poids faible decount
. En d'autres termes, le nombre de décalages est calculé à partir decount & 0x3F
.
Si le nombre de décalages résultant est nul, les opérateurs de décalage renvoient simplement la valeur de x
.
Les opérations de décalage ne provoquent jamais de dépassements de capacité et produisent les mêmes résultats dans les contextes vérifiés et non vérifiés.
Lorsque l'opérande gauche de l'opérateur >>
est de type intégral signé, l'opérateur effectue un décalage arithmétique vers la droite dans lequel la valeur du bit le plus significatif (le bit de signe) de l'opérande est propagée aux positions de bits vides de poids fort. Lorsque l'opérande gauche de l'opérateur >>
est de type intégral non signé, l'opérateur effectue un décalage logique vers la droite dans lequel les positions des bits vides de poids fort sont toujours mises à zéro. Pour effectuer l'opération opposée à celle déduite du type de l'opérande, il est possible d'utiliser des casts explicites.
Exemple : si
x
est une variable de typeint
, l’opérationunchecked ((int)((uint)x >> y))
effectue un décalage logique vers la droite dex
. exemple final
Les formes augmentées (§12.4.8) des opérateurs de décalage prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.12 opérateurs relationnels et de test de type
12.12.1 Général
Les opérateurs ==
, !=
, <
, >
, <=
, >=
, is
, et as
sont appelés les opérateurs relationnels et de test de type.
relational_expression
: shift_expression
| relational_expression '<' shift_expression
| relational_expression '>' shift_expression
| relational_expression '<=' shift_expression
| relational_expression '>=' shift_expression
| relational_expression 'is' type
| relational_expression 'is' pattern
| relational_expression 'as' type
;
equality_expression
: relational_expression
| equality_expression '==' relational_expression
| equality_expression '!=' relational_expression
;
Note : La recherche de l'opérande droit de l'opérateur
is
doit d'abord être testée en tant que type, puis en tant qu'expression qui peut s'étendre sur plusieurs tokens. Dans le cas où l'opérande est une expression, l'expression type doit avoir une priorité au moins égale à celle de shift_expression. fin de la remarque
L’opérateur is
est décrit dans §12.12.12 et l’opérateur as
est décrit dans §12.12.13.
Les opérateurs ==
, !=
, <
, >
, <=
et >=
sont des opérateurs de comparaison.
Si un default_literal (§12.8.21) est utilisé comme opérande d’un opérateur <
, >
, <=
ou >=
, une erreur de compilation se produit.
Si un default_literal est utilisé comme opérande des deux opérateurs ==
ou !=
, une erreur à la compilation se produit. Si un default_literal est utilisé comme opérande de gauche de l’opérateur is
ou as
, une erreur de compilation se produit.
Si un opérande d’un opérateur de comparaison a le type à la compilation dynamic
, alors l’expression est liée dynamiquement (§12.3.3). Dans ce cas, le type à la compilation de l’expression est dynamic
, et la résolution décrite ci-dessous s’effectuera à l’exécution en utilisant le type d’exécution des opérandes ayant le type à la compilation dynamic
.
Pour une opération de la forme x «op» y
, où «op» est un opérateur de comparaison, la résolution de surcharge (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur. Si les deux opérandes d'une equality_expression sont le littéral null
, la résolution de surcharge n'est pas effectuée et l'expression est évaluée à une valeur constante de true
ou false
selon que l'opérateur est ==
ou !=
.
Les opérateurs de comparaison prédéfinis sont décrits dans les sous-sections suivantes. Tous les opérateurs de comparaison prédéfinis renvoient un résultat de type bool, comme décrit dans le tableau suivant.
Fonctionnement | Résultat |
---|---|
x == y |
true si x est égal à y , false sinon |
x != y |
true if x n’est pas égal à y , false sinon |
x < y |
true si x est inférieur à y , false sinon |
x > y |
true si x est supérieur à y , false sinon |
x <= y |
true si x est inférieur ou égal à y , false sinon |
x >= y |
true si x est supérieur ou égal à y , false sinon |
12.12.2 opérateurs de comparaison entiers
Les opérateurs de comparaison entiers prédéfinis sont :
bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);
bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);
bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);
bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);
bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);
bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);
Chacun de ces opérateurs compare les valeurs numériques des deux opérandes entiers et renvoie une valeur bool
qui indique si la relation particulière est true
ou false
.
Les formes surélevées (§12.4.8) des opérateurs de comparaison d’entiers prédéfinis non surélevés définis ci-dessus sont également prédéfinies.
12.12.3 Opérateurs de comparaison en virgule flottante
Les opérateurs de comparaison en virgule flottante prédéfinis sont :
bool operator ==(float x, float y);
bool operator ==(double x, double y);
bool operator !=(float x, float y);
bool operator !=(double x, double y);
bool operator <(float x, float y);
bool operator <(double x, double y);
bool operator >(float x, float y);
bool operator >(double x, double y);
bool operator <=(float x, float y);
bool operator <=(double x, double y);
bool operator >=(float x, float y);
bool operator >=(double x, double y);
Les opérateurs comparent les opérandes selon les règles de la norme IEC 60559 :
Si l’un des opérandes est NaN, le résultat est false
pour tous les opérateurs sauf !=
, pour lesquels le résultat est true
. Pour deux opérandes quelconques, x != y
produit toujours le même résultat que !(x == y)
. Cependant, lorsque l'un ou les deux opérandes sont NaN, les opérateurs <
, >
, <=
et >=
ne produisent pas les mêmes résultats que la négation logique de l'opérateur opposé.
Exemple : si
x
ouy
est NaN, alorsx < y
estfalse
, mais!(x >= y)
esttrue
. exemple final
Lorsque aucun des opérandes n’est NaN, les opérateurs comparent les valeurs des deux opérandes à virgule flottante en fonction de l’ordre
–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞
où min
et max
sont les plus petites et les plus grandes valeurs finies positives pouvant être représentées dans le format en virgule flottante donné. Les effets notables de ce classement sont les suivants :
- Les zéros positifs et négatifs sont considérés égaux.
- Un infini négatif est considéré comme inférieur à toutes les autres valeurs, mais égal à un autre infini négatif.
- Un infini positif est considéré comme supérieur à toutes les autres valeurs, mais égal à un autre infini positif.
Les formes augmentées (§12.4.8) des opérateurs de comparaison en virgule flottante prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.12.4 opérateurs de comparaison décimaux
Les opérateurs de comparaison décimaux prédéfinis sont :
bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);
Chacun de ces opérateurs compare les valeurs numériques des deux opérandes décimaux et renvoie une valeur bool
qui indique si la relation particulière est true
ou false
. Chaque comparaison décimale équivaut à utiliser l’opérateur relationnel ou d’égalité correspondant de type System.Decimal
.
Les formes décalées (§12.4.8) des opérateurs de comparaison décimale prédéfinis non décalés définis ci-dessus sont également prédéfinies.
12.12.5 opérateurs d’égalité booléens
Les opérateurs d’égalité booléens prédéfinis sont :
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
Le résultat de ==
est true
si les deux x
et y
sont true
ou si les deux x
et y
sont false
. Sinon, le résultat est false
.
Le résultat de !=
est false
si les deux x
et y
sont true
ou si les deux x
et y
sont false
. Sinon, le résultat est true
. Lorsque les opérandes sont de type bool
, l’opérateur !=
produit le même résultat que l’opérateur ^
.
Les formes décalées (§12.4.8) des opérateurs d'égalité booléens prédéfinis non décalés définis ci-dessus sont également prédéfinies.
12.12.6 Opérateurs de comparaison d'énumération
Chaque type d’énumération fournit implicitement les opérateurs de comparaison prédéfinis suivants
bool operator ==(E x, E y);
bool operator !=(E x, E y);
bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);
Le résultat de l’évaluation de x «op» y
, où x et y sont des expressions d’un type d’énumération E
avec un type sous-jacent U
, et «op» est l’un des opérateurs de comparaison, est exactement le même que celui de l’évaluation de ((U)x) «op» ((U)y)
. En d’autres termes, les opérateurs de comparaison des types d’énumération comparent simplement les valeurs intégrales sous-jacentes des deux opérandes.
Les formes décalées (§12.4.8) des opérateurs de comparaison d'énumération prédéfinis non décalés définis ci-dessus sont également prédéfinies.
12.12.7 Opérateurs d'égalité de type de référence
Chaque type de classe C
fournit implicitement les opérateurs d’égalité de référence prédéfinis suivants :
bool operator ==(C x, C y);
bool operator !=(C x, C y);
sauf si des opérateurs d'égalité prédéfinis existent autrement pour C
(par exemple, lorsque C
est string
ou System.Delegate
).
Les opérateurs renvoient le résultat de la comparaison des deux références pour l’égalité ou la non-égalité. operator ==
retourne true
si et seulement si x
et y
font référence à la même instance ou sont tous deux null
, tandis que operator !=
retourne true
si et seulement si operator ==
avec les mêmes opérandes retournerait false
.
En plus des règles d’applicabilité normales (§12.6.4.2), les opérateurs d’égalité de référence prédéfinis nécessitent l’une des conditions suivantes pour être applicables :
- Les deux opérandes sont une valeur d'un type connu comme étant un reference_type ou le littéral
null
. En outre, une conversion d'identité ou de référence explicite (§10.3.5) existe à partir de l'un des opérandes vers le type de l'autre opérande. - L'un des opérandes est le littéral
null
, et l'autre opérande est une valeur de typeT
oùT
est un type_parameter qui n'est pas connu pour être un type de valeur, et qui n'a pas la contrainte de type de valeur.- Si, à l’exécution,
T
est un type valeur ne pouvant être Null, le résultat de==
estfalse
et le résultat de!=
esttrue
. - Si, à l’exécution,
T
est un type valeur pouvant être Null, le résultat est calculé à partir de la propriétéHasValue
de l’opérande, comme décrit dans (§12.12.10). - Si, à l’exécution,
T
est un type de référence, le résultat esttrue
si l’opérande estnull
, etfalse
dans le cas contraire.
- Si, à l’exécution,
Si l'une de ces conditions n'est pas remplie, une erreur de liaison se produit.
Remarque : Les implications notables de ces règles sont :
- Il s’agit d’une erreur de liaison d’utiliser les opérateurs d’égalité de référence prédéfinis pour comparer deux références connues comme différentes lors de la liaison. Par exemple, si les types liés par le temps des opérandes sont deux types de classe, et si aucun ne dérive de l’autre, il serait impossible pour les deux opérandes de faire référence au même objet. L'opération est donc considérée comme une erreur de liaison.
- Les opérateurs d’égalité de référence prédéfinis n’autorisent pas la comparaison des opérandes de type valeur (sauf lorsque des paramètres de type sont comparés à
null
, ce qui est traité de manière particulière).- Les opérandes des opérateurs d'égalité de type de référence prédéfini ne sont jamais encadrés. Il serait inutile d'effectuer de telles opérations de mise en boîte, car les références aux instances mises en boîte nouvellement allouées seraient nécessairement différentes de toutes les autres références.
Pour une opération de la forme
x == y
oux != y
, si unoperator ==
ouoperator !=
défini par l’utilisateur applicable existe, les règles de résolution de surcharge d’opérateur (§12.4.5) sélectionneront cet opérateur plutôt que l’opérateur d’égalité de référence prédéfini. Il est toujours possible de sélectionner l'opérateur d'égalité de type de référence prédéfini en coulant explicitement l'un ou les deux opérandes dans le typeobject
.fin de la remarque
Exemple: l’exemple suivant vérifie si un argument d’un paramètre de type non contraint est
null
.class C<T> { void F(T x) { if (x == null) { throw new ArgumentNullException(); } ... } }
Le construct
x == null
est autorisé même siT
pourrait représenter un type valeur ne pouvant pas être Null, et le résultat est simplement défini comme étantfalse
lorsqueT
est un type valeur ne pouvant pas être Null.exemple final
Pour une opération de la forme x == y
ou x != y
, si un operator ==
ou operator !=
applicable existe, les règles de résolution de surcharge d'opérateur (§12.4.5) sélectionneront cet opérateur plutôt que l'opérateur d'égalité de type de référence prédéfini.
Remarque : Il est toujours possible de sélectionner l’opérateur d’égalité de type de référence prédéfini en coulant explicitement l’un ou les deux opérandes dans le type
object
. fin de la remarque
Exemple: l’exemple
class Test { static void Main() { string s = "Test"; string t = string.Copy(s); Console.WriteLine(s == t); Console.WriteLine((object)s == t); Console.WriteLine(s == (object)t); Console.WriteLine((object)s == (object)t); } }
génère la sortie
True False False False
Les variables
s
ett
se réfèrent à deux instances de chaîne distinctes contenant les mêmes caractères. La première comparaison renvoieTrue
car l’opérateur d’égalité de chaînes prédéfini (§12.12.8) est sélectionné lorsque les deux opérandes sont de typestring
. Toutes les comparaisons restantes produisentFalse
, car la surcharge deoperator ==
dans le typestring
n'est pas applicable lorsque l'un des opérandes a un type de durée de liaison deobject
.Il est à noter que la technique ci-dessus n’a pas de sens pour les types valeur. L’exemple
class Test { static void Main() { int i = 123; int j = 123; Console.WriteLine((object)i == (object)j); } }
Il est recommandé de ne pas utiliser les sorties
False
car les castings créent des références à deux instances distinctes de valeursint
encadrées.exemple final
12.12.8 Opérateurs d'égalité de chaînes de caractères
Les opérateurs d’égalité de chaînes prédéfinis sont :
bool operator ==(string x, string y);
bool operator !=(string x, string y);
Deux valeurs string
sont considérées comme égales lorsque l’une des conditions suivantes est remplie :
- Les deux valeurs sont
null
. - Les deux valeurs sont des références non-
null
aux instances de chaîne qui ont des longueurs identiques et des caractères identiques à chaque position de caractère.
Les opérateurs d’égalité de chaînes comparent les valeurs des chaînes plutôt que les références aux chaînes. Lorsque deux instances distinctes de chaînes contiennent exactement la même séquence de caractères, les valeurs des chaînes sont égales, mais les références sont différentes.
Remarque : Comme décrit dans §12.12.7, les opérateurs d’égalité de type référence peuvent être utilisés pour comparer des références de chaîne plutôt que des valeurs de chaîne. fin de la remarque
12.12.9 Opérateurs d’égalité pour délégués
Les opérateurs d’égalité pour délégués prédéfinis sont :
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
Deux instances de délégués sont considérées comme égales de la manière suivante :
- Si l'une des instances de délégué est
null
, elles sont égales si et seulement si elles sont toutes deuxnull
. - Si les délégués ont des types d'exécution différents, ils ne sont jamais égaux.
- Si les deux instances de délégués possèdent une liste d’appels (§20.2), ces instances sont égales si et seulement si leurs listes d’appel sont de même longueur et que chaque élément de la liste de l’une est égal (comme défini ci-dessous) à l’élément correspondant, dans l’ordre, de la liste de l’autre.
Les règles suivantes régissent l'égalité des entrées de la liste d'invocation :
- Si deux éléments de liste d’appel se réfèrent tous deux à la même méthode statique, alors les éléments sont égaux.
- Si deux éléments de liste d’appel se réfèrent tous deux à la même méthode non statique sur le même objet cible (tel que défini par les opérateurs d’égalité de référence), alors les éléments sont égaux.
- Les éléments de liste d’appel issus de l’évaluation de fonctions anonymes sémantiquement identiques (§12.19) avec le même ensemble (éventuellement vide) d’instances de variables extérieures capturées sont autorisés (mais pas obligés) à être égaux.
Si la résolution de la surcharge de l'opérateur se résout en l'un ou l'autre des opérateurs d'égalité de délégués, et que les types de liaison des deux opérandes sont des types de délégués tels que décrits au §20 plutôt que System.Delegate
, et qu'il n'y a pas de conversion d'identité entre les types d'opérandes de liaison, une erreur de liaison se produit.
Remarque : cette règle empêche les comparaisons qui ne pourraient jamais considérer des valeurs non‑
null
comme égales en raison du fait qu’il s’agit de références à des instances de différents types de délégués. fin de la remarque
12.12.10 Opérateurs d’égalité entre types valeur pouvant être Null et le littéral Null
Les opérateurs ==
et !=
permettent à un opérande d'être une valeur d'un type de valeur nullable et à l'autre d'être le littéral null
, même si aucun opérateur prédéfini ou défini par l'utilisateur (sous forme non levée ou levée) n'existe pour l'opération.
Pour une opération de l'une des formes suivantes
x == null null == x x != null null != x
où x
est une expression d’un type valeur pouvant être Null, si la résolution de surcharge d’opérateur (§12.4.5) ne parvient pas à trouver un opérateur applicable, le résultat est calculé à partir de la propriété HasValue
de x
. Plus précisément, les deux premières formes sont traduites en !x.HasValue
, et les deux dernières en x.HasValue
.
12.12.11 Opérateurs d'égalité de tuple
Les opérateurs d'égalité de tuple sont appliqués par paire aux éléments des opérandes de tuple dans l'ordre lexical.
Si chaque opérande x
et y
d’un opérateur ==
ou !=
est classifiée soit comme un tuple, soit comme une valeur de type tuple (§8.3.11), alors l’opérateur est un opérateur d’égalité de tuple.
Si un opérande e
est classifié comme un tuple, les éléments e1...en
doivent être les résultats de l’évaluation des expressions composants du tuple. Dans le cas contraire, si e
est une valeur d’un type tuple, les éléments seront t.Item1...t.Itemn
où t
est le résultat de l’évaluation de e
.
Les opérandes x
et y
d'un opérateur d'égalité de tuple doivent avoir le même arité, sinon une erreur de compilation se produit. Pour chaque paire d’éléments xi
et yi
, le même opérateur d’égalité doit s’appliquer et génère un résultat de type bool
, dynamic
, un type qui a une conversion implicite en bool
, ou un type qui définit les opérateurs true
et false
.
L’opérateur d’égalité de tuple x == y
est évalué comme suit :
- L'opérande gauche
x
est évalué. - L'opérande de droite
y
est évalué. - Pour chaque paire d’éléments
xi
etyi
dans l’ordre lexical :- L’opérateur
xi == yi
est évalué, et un résultat de typebool
est obtenu de la manière suivante :- Si la comparaison a donné un
bool
, alors c’est le résultat. - Sinon, si la comparaison a donné un
dynamic
, alors l’opérateurfalse
est appelé dynamiquement sur celui‑ci, et la valeurbool
résultante est niée avec l’opérateur de négation logique (!
). - Sinon, si le type de la comparaison possède une conversion implicite vers
bool
, alors cette conversion est appliquée. - Sinon, si le type de la comparaison possède un opérateur
false
, cet opérateur est appelé et la valeurbool
résultante est niée avec l’opérateur de négation logique (!
).
- Si la comparaison a donné un
- Si le
bool
résultant estfalse
, alors aucune évaluation supplémentaire n’a lieu, et le résultat de l’opérateur d’égalité de tuple estfalse
.
- L’opérateur
- Si toutes les comparaisons d’éléments ont donné
true
, le résultat de l’opérateur d’égalité de tuple esttrue
.
L’opérateur d’égalité de tuple x != y
est évalué comme suit :
- L'opérande gauche
x
est évalué. - L'opérande de droite
y
est évalué. - Pour chaque paire d’éléments
xi
etyi
dans l’ordre lexical :- L’opérateur
xi != yi
est évalué, et un résultat de typebool
est obtenu de la manière suivante :- Si la comparaison a donné un
bool
, alors c’est le résultat. - ans le cas contraire, si la comparaison a donné un
dynamic
, alors l’opérateurtrue
est appelé dynamiquement sur celui‑ci, et la valeurbool
résultante en est le résultat. - Sinon, si le type de la comparaison possède une conversion implicite vers
bool
, alors cette conversion est appliquée. - Dans le cas contraire, si le type de la comparaison possède un opérateur
true
, cet opérateur est appelé et la valeurbool
résultante en est le résultat.
- Si la comparaison a donné un
- Si le
bool
résultant esttrue
, alors aucune évaluation supplémentaire n’a lieu, et le résultat de l’opérateur d’égalité de tuple esttrue
.
- L’opérateur
- Si toutes les comparaisons d’éléments ont donné
false
, le résultat de l’opérateur d’égalité de tuple estfalse
.
12.12.12 L'opérateur is
Il existe deux formes de l'opérateur is
. La première est l'opérateur is-type, qui comporte un type du côté droit. L'autre est l'opérateur is-pattern, qui a un motif sur le côté droit.
12.12.12.1 L'opérateur is-type
L’opérateur is-type est utilisé pour vérifier si le type d’exécution d’un objet est compatible avec un type donné. La vérification s’effectue à l’exécution. Le résultat de l’opération E is T
, où E
est une expression et T
est un type autre que dynamic
, est une valeur booléenne indiquant si E
n’est pas Null et peut être converti avec succès en type T
par une conversion de référence, une conversion boxing, une conversion unboxing, une conversion wrapping ou une conversion de unwrapping.
L’opération est évaluée de la manière suivante :
- Si
E
est un groupe de fonctions ou de méthodes anonyme, une erreur au moment de la compilation se produit. - Si
E
est le littéralnull
, ou si la valeur deE
estnull
, le résultat estfalse
. - Sinon :
- Soit
R
le type d’exécution deE
. - Soit
D
dérivé deR
de la manière suivante : - Si
R
est un type valeur pouvant être Null,D
est le type sous‑jacent deR
. - Sinon,
D
estR
. - Le résultat dépend de
D
etT
de la manière suivante : - Si
T
est un type référence, le résultat esttrue
si :- Il y a une conversion d’identité entre
D
etT
. -
D
est un type référence et une conversion de référence implicite deD
versT
existe, ou - Soit :
D
est un type de valeur et il existe une conversion de mise en boîte deD
versT
.
Ou :D
est un type valeur etT
est un type interface implémenté parD
.
- Il y a une conversion d’identité entre
- Si
T
est un type valeur pouvant être Null, le résultat esttrue
siD
est le type sous‑jacent deT
. - Si
T
est un type valeur ne pouvant pas être Null, le résultat esttrue
siD
etT
sont du même type. - Sinon, le résultat est
false
.
Les conversions définies par l’utilisateur ne sont pas prises en compte par l’opérateur is
.
Remarque: Comme l’opérateur
is
est évalué au moment de l’exécution, tous les arguments de type ont été substitués et il n'y a aucun type ouvert (§8.4.3) à prendre en compte. fin de la remarque
Remarque : l’opérateur
is
peut être compris en termes de types à la compilation et de conversions comme suit, oùC
est le type à la compilation deE
:
- Si le type de
e
au moment de la compilation est identique àT
, ou si une conversion de référence implicite (§10.2.8), une conversion par encapsulation (§10.2.9), une conversion par encapsulation (§10.6) ou une conversion explicite de désencapsulation (§10.6) existe du type de compilation deE
àT
:
- Si
C
est d’un type valeur non‑nullable, le résultat de l’opération esttrue
.- Sinon, le résultat de l’opération équivaut à évaluer
E != null
.- Dans le cas contraire, si une conversion de référence explicite (§10.3.5) ou une conversion de démontage (§10.3.7) existe de
C
àT
, ou siC
ouT
est un type ouvert (§8.4.3), les vérifications d’exécution mentionnées précédemment doivent être effectuées.- aSinon, aucune conversion de référence, de mise en boîte, d'emballage ou de déballage de
E
vers le typeT
n'est possible, et le résultat de l'opération estfalse
. Un compilateur peut mettre en œuvre des optimisations basées sur le type compile-time.fin de la remarque
12.12.12.2 L'opérateur is-pattern
L'opérateur is-pattern est utilisé pour vérifier si la valeur calculée par une expression correspond à un motif donné (§11). La vérification s’effectue à l’exécution. Le résultat de l’opérateur is-pattern est vrai si la valeur correspond au motif ; sinon, il est faux.
Pour une expression de la forme E is P
, où E
est une expression relationnelle de type T
et P
est un motif, il s’agit d’une erreur de compilation si l’une des conditions suivantes est remplie :
-
E
ne désigne pas une valeur ou n’a pas de type. - Le modèle
P
n’est pas applicable (§11.2) au typeT
.
12.12.13 L'opérateur as
L’opérateur as
est utilisé pour convertir explicitement une valeur en un type référence donné ou en un type valeur pouvant être Null. Contrairement à une expression cast (§12.9.7), l'opérateur as
ne lève jamais d'exception. Dans le cas contraire, si la conversion indiquée n’est pas possible, la valeur résultante est null
.
Dans une opération de la forme E as T
, E
doit être une expression et T
doit être un type référence, un paramètre de type connu pour être un type référence, ou un type valeur pouvant être Null. De plus, au moins l’une des conditions suivantes doit être True, sinon une erreur de compilation survient :
- Une conversion d'identité (§10.2.2), de nullable implicite (§10.2.6), de référence implicite (§10.2.8), de mise en boîte (§10.2.9), de nullable explicite (§10.3.4), de référence explicite (§10.3.5), ou de wrapping (§8.3.12) existe de
E
versT
. - Le type de
E
ou deT
est un type ouvert. -
E
est le littéralnull
.
Si le type à la compilation de E
n’est pas dynamic
, l’opération E as T
produit le même résultat que
E is T ? (T)(E) : (T)null
sauf que E
n’est évalué qu’une seule fois. On peut s’attendre à ce qu’un compilateur optimise E as T
pour effectuer au plus une vérification de type à l’exécution, contrairement aux deux vérifications de type à l’exécution impliquées par l’expansion ci-dessus.
Si le type à la compilation de E
est dynamic
, contrairement à l’opérateur de cast, l’opérateur as
n’est pas lié dynamiquement (§12.3.3). Donc, l’expansion dans ce cas est :
E is T ? (T)(object)(E) : (T)null
Notez que certaines conversions, telles que les conversions définies par l’utilisateur, ne sont pas possibles avec l’opérateur as
et doivent être effectuées à l’aide d’expressions de cast.
Exemple : Dans l'exemple
class X { public string F(object o) { return o as string; // OK, string is a reference type } public T G<T>(object o) where T : Attribute { return o as T; // Ok, T has a class constraint } public U H<U>(object o) { return o as U; // Error, U is unconstrained } }
le paramètre de type
T
deG
est connu pour être un type référence, car il possède la contrainte de classe. Le paramètre de typeU
deH
ne l’est pas cependant ; d’où l’interdiction d’utiliser l’opérateuras
dansH
.exemple final
12.13 Opérateurs logiques
12.13.1 Général
Les opérateurs &
, ^
et |
sont appelés opérateurs logiques.
and_expression
: equality_expression
| and_expression '&' equality_expression
;
exclusive_or_expression
: and_expression
| exclusive_or_expression '^' and_expression
;
inclusive_or_expression
: exclusive_or_expression
| inclusive_or_expression '|' exclusive_or_expression
;
Si une opérande d’un opérateur logique a pour type à la compilation dynamic
, alors l’expression est liée dynamiquement (§12.3.3). Dans ce cas, le type à la compilation de l’expression est dynamic
, et la résolution décrite ci-dessous s’effectuera à l’exécution en utilisant le type d’exécution des opérandes ayant le type à la compilation dynamic
.
Pour une opération de la forme x «op» y
, où «op» est l’un des opérateurs logiques, la résolution de surcharge (§12.4.5) est appliquée pour sélectionner une implémentation spécifique de l’opérateur. Les opérandes sont convertis en types de paramètre de l’opérateur sélectionné, et le type du résultat correspond au type de retour de l’opérateur.
Les opérateurs logiques prédéfinis sont décrits dans les sous‑sections suivantes.
12.13.2 Opérateurs logiques entiers
Les opérateurs logiques entiers prédéfinis sont :
int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);
int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);
int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);
L'opérateur &
calcule l'ET logique bit à bit des deux opérandes, l'opérateur |
calcule l'OU logique bit à bit des deux opérandes, et l'opérateur ^
calcule l'OU logique exclusif bit à bit des deux opérandes. Ces opérations ne peuvent provoquer aucun dépassement.
Les formes augmentées (§12.4.8) des opérateurs logiques entiers prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.13.3 Opérateurs logiques pour énumérations
Chaque type d’énumération E
fournit implicitement les opérateurs logiques prédéfinis suivants :
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
Le résultat de l’évaluation de x «op» y
, où x
et y
sont des expressions d’un type d’énumération E
avec un type sous‑jacent U
, et où «op» est l’un des opérateurs logiques, est exactement le même que l’évaluation de (E)((U)x «op» (U)y)
. En d’autres termes, les opérateurs logiques de type énumération effectuent simplement l’opération logique sur le type sous‑jacent des deux opérandes.
Les formes augmentées (§12.4.8) des opérateurs logiques d'énumération prédéfinis non augmentés définis ci-dessus sont également prédéfinies.
12.13.4 Opérateurs logiques booléens
Les opérateurs logiques booléens prédéfinis sont :
bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);
Le résultat de x & y
est true
si x
et y
sont true
. Sinon, le résultat est false
.
Le résultat de x | y
est true
si x
ou y
est true
. Sinon, le résultat est false
.
Le résultat de x ^ y
est true
si x
est true
et y
est false
, ou x
est false
et y
est true
. Sinon, le résultat est false
. Lorsque les opérandes sont de type bool
, l’opérateur ^
calcule le même résultat que l’opérateur !=
.
12.13.5 Opérateurs booléens nullables & et |
Le type booléen nullable bool?
peut représenter trois valeurs, true
, false
et null
.
Comme pour les autres opérateurs binaires, les formes levées des opérateurs logiques &
et |
(§12.13.4) sont également prédéfinies :
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);
La sémantique des opérateurs &
et |
abstraits est définie par le tableau suivant :
x |
y |
x & y |
x \| y |
---|---|---|---|
true |
true |
true |
true |
true |
false |
false |
true |
true |
null |
null |
true |
false |
true |
false |
true |
false |
false |
false |
false |
false |
null |
false |
null |
null |
true |
null |
true |
null |
false |
false |
null |
null |
null |
null |
null |
Remarque : le type
bool?
est conceptuellement similaire au type à trois valeurs utilisé pour les expressions booléennes en SQL. Le tableau ci-dessus suit la même sémantique que SQL, alors qu’appliquer les règles de §12.4.8 aux opérateurs&
et|
ne le ferait pas. Les règles du §12.4.8 fournissent déjà une sémantique de type SQL pour l'opérateur^
levé. fin de la remarque
12.14 Opérateurs logiques conditionnels
12.14.1 Général
Les opérateurs &&
et ||
sont appelés opérateurs logiques conditionnels. Ils sont également appelés opérateurs logiques de « court-circuitage ».
conditional_and_expression
: inclusive_or_expression
| conditional_and_expression '&&' inclusive_or_expression
;
conditional_or_expression
: conditional_and_expression
| conditional_or_expression '||' conditional_and_expression
;
Les opérateurs &&
et ||
sont des versions conditionnelles des opérateurs &
et |
:
- L’opération
x && y
correspond à l’opérationx & y
, sauf quey
est évalué uniquement six
n’est pasfalse
. - L’opération
x || y
correspond à l’opérationx | y
, sauf quey
est évalué uniquement six
n’est pastrue
.
Remarque : la raison pour laquelle le court‑circuit utilise les conditions « not true » et « not false » est de permettre aux opérateurs conditionnels définis par l’utilisateur de définir quand le court‑circuit s’applique. Les types définis par l’utilisateur pourraient être dans un état où
operator true
renvoiefalse
etoperator false
renvoiefalse
. Dans ces cas, ni&&
ni||
ne ferait de court‑circuit. fin de la remarque
Si une opérande d’un opérateur logique conditionnel a pour type à la compilation dynamic
, alors l’expression est liée dynamiquement (§12.3.3). Dans ce cas, le type à la compilation de l’expression est dynamic
, et la résolution décrite ci-dessous s’effectuera à l’exécution en utilisant le type d’exécution des opérandes ayant le type à la compilation dynamic
.
Une opération de la forme x && y
ou x || y
est traitée en appliquant la résolution de surcharge (§12.4.5) comme si l’opération était écrite x & y
ou x | y
. Ainsi,
- Si la résolution de surcharge ne parvient pas à trouver un opérateur optimal unique, ou si elle sélectionne l’un des opérateurs logiques entiers prédéfinis ou des opérateurs logiques booléens nullable prédéfinis (§12.13.5), une erreur de liaison survient.
- Sinon, si l’opérateur sélectionné est l’un des opérateurs logiques booléens prédéfinis (§12.13.4), l’opération est traitée comme décrit dans §12.14.2.
- Sinon, l’opérateur sélectionné est un opérateur défini par l’utilisateur, et l’opération est traitée comme décrit dans §12.14.3.
Il n’est pas possible de surcharger directement les opérateurs logiques conditionnels. Cependant, puisque les opérateurs logiques conditionnels sont évalués en fonction des opérateurs logiques classiques, les surcharges des opérateurs logiques classiques sont, sous certaines restrictions, également considérées comme des surcharges des opérateurs logiques conditionnels. Cela est décrit plus en détail dans §12.14.3.
12.14.2 Opérateurs logiques conditionnels booléens
Lorsque les opérandes de &&
ou ||
sont de type bool
, ou lorsque les opérandes sont de types qui ne définissent pas un operator &
ou operator |
applicable, mais définissent des conversions implicites vers bool
, l’opération est traitée comme suit :
- L’opération
x && y
est évaluée commex ? y : false
. Autrement dit,x
est d’abord évalué et converti en typebool
. Ensuite, six
esttrue
,y
est évalué et converti en typebool
, et ceci devient le résultat de l’opération. Sinon, le résultat de l’opération estfalse
. - L’opération
x || y
est évaluée commex ? true : y
. Autrement dit,x
est d’abord évalué et converti en typebool
. Puis, six
esttrue
, le résultat de l’opération esttrue
. Sinon,y
est évalué et converti en typebool
, et ceci devient le résultat de l’opération.
12.14.3 Opérateurs logiques conditionnels définis par l’utilisateur
Lorsque les opérandes de &&
ou ||
sont de types qui déclarent un operator &
ou operator |
défini par l’utilisateur applicable, les deux conditions suivantes doivent être remplies, où T
est le type dans lequel l’opérateur sélectionné est déclaré :
- Le type de retour et le type de chaque paramètre de l’opérateur sélectionné doivent être
T
. Autrement dit, l’opérateur doit calculer le ET logique ou le OU logique de deux opérandes de typeT
, et doit renvoyer un résultat de typeT
. -
T
doit contenir des déclarations deoperator true
etoperator false
.
Une erreur de liaison se produit si l'une de ces conditions n'est pas satisfaite. Sinon, l’opération &&
ou ||
est évaluée en combinant le operator true
ou operator false
défini par l’utilisateur avec l’opérateur défini par l’utilisateur sélectionné :
- L’opération
x && y
est évaluée commeT.false(x) ? x : T.&(x, y)
, oùT.false(x)
est un appel duoperator false
déclaré dansT
, etT.&(x, y)
est un appel duoperator &
sélectionné. Autrement dit,x
est d’abord évalué etoperator false
est appelé sur le résultat pour déterminer six
est définitivement False. Ensuite, six
est définitivement faux, le résultat de l’opération est la valeur précédemment calculée pourx
. Sinon,y
est évalué, et leoperator &
sélectionné est appelé sur la valeur précédemment calculée pourx
et la valeur calculée poury
afin de produire le résultat de l’opération. - L’opération
x || y
est évaluée commeT.true(x) ? x : T.|(x, y)
, oùT.true(x)
est un appel duoperator true
déclaré dansT
, etT.|(x, y)
est un appel duoperator |
sélectionné. Autrement dit,x
est d’abord évalué etoperator true
est appelé sur le résultat pour déterminer six
est définitivement True. Alors, six
est définitivement True le résultat de l’opération est la valeur précédemment calculée pourx
. Sinon,y
est évalué, et leoperator |
sélectionné est appelé sur la valeur précédemment calculée pourx
et la valeur calculée poury
afin de produire le résultat de l’opération.
Dans chacune de ces opérations, l’expression donnée par x
n’est évaluée qu’une seule fois, et l’expression donnée par y
n’est soit pas évaluée, soit évaluée exactement une fois.
12.15 L'opérateur de coalescence nulle
L’opérateur ??
est appelé l’opérateur de fusion Null.
null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
| throw_expression
;
Dans une expression de fusion Null de la forme a ?? b
, si a
est non‑null
, le résultat est a
; sinon, le résultat est b
. L’opération évalue b
uniquement si a
est null
.
L'opérateur de coalescence nulle est associatif à droite, ce qui signifie que les opérations sont groupées de la droite vers la gauche.
Exemple : une expression de la forme
a ?? b ?? c
est évaluée commea ?? (b ?? c)
. En termes généraux, une expression de la formeE1 ?? E2 ?? ... ?? EN
renvoie le premier des opérandes qui n’est pasnull
, ounull
si tous les opérandes sontnull
. exemple final
Le type de l’expression a ?? b
dépend des conversions implicites disponibles sur les opérandes. Dans l’ordre de préférence, le type de a ?? b
est A₀
, A
ou B
, où A
est le type de a
(à condition que a
a un type), B
est le type de b
(à condition que b
possède un type) et A₀
est le type sous-jacent de A
si A
est un type valeur nullable ou A
autrement. Plus précisément, a ?? b
est traité comme suit :
- Si
A
existe et n’est pas un type valeur pouvant être Null ou un type référence, une erreur de compilation survient. - Sinon, si
A
existe et queb
est une expression dynamique, le type de résultat estdynamic
. Au moment de l'exécution,a
est évalué en premier. Sia
n’est pasnull
,a
est converti endynamic
, et ceci devient le résultat. Sinon,b
est évalué et devient le résultat. - Sinon, si
A
existe et est un type valeur pouvant être Null et qu’une conversion implicite deb
versA₀
existe, le type de résultat estA₀
. Au moment de l'exécution,a
est évalué en premier. Sia
n'est pasnull
,a
est décomposé en typeA₀
et devient le résultat. Sinon,b
est évalué et converti en typeA₀
, et ceci devient le résultat. - Sinon, si
A
existe et qu’une conversion implicite deb
versA
existe, le type de résultat estA
. Au moment de l'exécution,a
est évalué en premier. Sia
n’est pasnull
,a
devient le résultat. Sinon,b
est évalué et converti en typeA
, et ceci devient le résultat. - Sinon, si
A
existe et est un type valeur pouvant être Null, queb
a un typeB
et qu’une conversion implicite deA₀
versB
existe, le type de résultat estB
. Au moment de l'exécution,a
est évalué en premier. Sia
n’est pasnull
,a
est déballé en typeA₀
et converti en typeB
, et ceci devient le résultat. Sinon,b
est évalué et devient le résultat. - Sinon, si
b
a un typeB
et qu’une conversion implicite existe dea
enB
, le type de résultat estB
. Au moment de l'exécution,a
est évalué en premier. Sia
n’est pasnull
,a
est converti en typeB
et devient le résultat. Sinon,b
est évalué et devient le résultat.
Sinon, a
et b
sont incompatibles et une erreur au moment de la compilation se produit.
12.16 L'opérateur d'expression throw
throw_expression
: 'throw' null_coalescing_expression
;
Une expression throw_expression jette la valeur produite par l'évaluation de l'expression null_coalescing_expression. L’expression doit être implicitement convertible en System.Exception
, et le résultat de l’évaluation de l’expression est converti en System.Exception
avant d’être lancé. Le comportement à l'exécution de l'évaluation d'une expression throw est le même que celui spécifié pour une instruction throw (§13.10.6).
Une throw_expression n’a pas de type. Une expression throw_expression est convertible en tout type par une conversion implicite throw.
Une expression throw ne se produit que dans les contextes syntaxiques suivants :
- En tant que deuxième ou troisième opérande d’un opérateur conditionnel ternaire (
?:
). - En tant que deuxième opérande d'un opérateur de coalescence null (
??
). - En tant que corps d'un lambda ou d'un membre à corps d'expression.
12.17 Expressions de déclaration
Une expression de déclaration déclare une variable locale.
declaration_expression
: local_variable_type identifier
;
local_variable_type
: type
| 'var'
;
Le simple_name_
est également considéré comme une expression de déclaration si la recherche du nom simple n’a pas trouvé de déclaration associée (§12.8.4). Lorsqu'il est utilisé en tant qu'expression de déclaration, _
est appelé un simple rejet. Il est sémantiquement équivalent à var _
, mais est autorisé dans davantage de contextes.
Une expression de déclaration ne peut apparaître que dans les contextes syntaxiques suivants :
- En tant que
out
valeur d'argument dans une liste d'arguments. - En tant que rejet simple
_
comprenant le côté gauche d'une affectation simple (§12.21.2). - En tant que tuple_element dans une ou plusieurs tuple_expressions emboîtées récursivement, dont la plus externe comprend le côté gauche d'une affectation déconstructrice. Une deconstruction_expression donne lieu à des expressions de déclaration à cet endroit, même si les expressions de déclaration ne sont pas présentes de manière syntaxique.
Remarque : cela signifie qu’une expression de déclaration ne peut pas être mise entre parenthèses. fin de la remarque
Une variable implicitement typée déclarée avec une declaration_expression est une erreur si elle est référencée dans l'argument_list où elle est déclarée.
C'est une erreur si une variable déclarée avec une declaration_expression est référencée dans l'affectation de déconstruction où elle apparaît.
Une expression de déclaration qui est un simple rejet ou dont le local_variable_type est l'identificateur var
est classée comme une variable implicitement typée. L’expression n’a pas de type, et le type de la variable locale est déduit en fonction du contexte syntaxique comme suit :
- Dans une argument_list, le type déduit de la variable est le type déclaré du paramètre correspondant.
- En tant que côté gauche d'une affectation simple, le type déduit de la variable est celui du côté droit de l'affectation.
- Dans un tuple_expression du côté gauche d'une affectation simple, le type inféré de la variable est le type de l'élément de tuple correspondant du côté droit (après déconstruction) de l'affectation.
Sinon, l’expression de déclaration est classée comme une variable à type explicite, et le type de l’expression ainsi que celui de la variable déclarée sera celui donné par le local_variable_type.
Une expression de déclaration avec l’identificateur _
est un discard (§9.2.9.2), et n’introduit pas de nom pour la variable. Une expression de déclaration portant un identificateur autre que _
introduit ce nom dans l'espace de déclaration de la variable locale immédiatement englobant (§7.3).
Exemple :
string M(out int i, string s, out bool b) { ... } var s1 = M(out int i1, "One", out var b1); Console.WriteLine($"{i1}, {b1}, {s1}"); // Error: i2 referenced within declaring argument list var s2 = M(out var i2, M(out i2, "Two", out bool b2), out b2); var s3 = M(out int _, "Three", out var _);
La déclaration de
s1
présente des expressions de déclaration typées à la fois explicitement et implicitement. Le type déduit deb1
estbool
car c’est le type du paramètre de sortie correspondant dansM1
. LeWriteLine
suivant est capable d'accéder ài1
etb1
, qui ont été introduits dans la portée englobante.La déclaration de
s2
montre une tentative d’utiliseri2
dans l’appel imbriqué àM
, ce qui est interdit, car la référence se produit dans la liste d’arguments oùi2
a été déclaré. En revanche, la référence àb2
dans l’argument final est autorisée, car elle intervient après la fin de la liste d’arguments imbriquée oùb2
a été déclaré.La déclaration de
s3
montre l'utilisation d'expressions de déclaration implicitement et explicitement typées qui sont des rejets. Comme les rejets ne déclarent pas de variable nommée, les multiples occurrences de l'identificateur_
sont autorisées.(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);
Cet exemple montre l'utilisation d'expressions de déclaration implicitement et explicitement typées pour les variables et les rejets dans une affectation de déconstruction. Le simple_name
_
est équivalent àvar _
lorsqu’aucune déclaration de_
n’est trouvée.void M1(out int i) { ... } void M2(string _) { M1(out _); // Error: `_` is a string M1(out var _); }
Cet exemple montre l'utilisation de
var _
pour fournir un rejet implicitement typé lorsque_
n'est pas disponible, parce qu'il désigne une variable dans la portée englobante.exemple final
12.18 Opérateur conditionnel
L’opérateur ?:
est appelé opérateur conditionnel. Il est parfois également appelé opérateur ternaire.
conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
| null_coalescing_expression '?' 'ref' variable_reference ':'
'ref' variable_reference
;
Une throw_expression (§12.16) n’est pas autorisée dans un opérateur conditionnel si ref
est présent.
Une expression conditionnelle de la forme b ? x : y
évalue d’abord la condition b
. Ensuite, si b
est true
, x
est évalué et devient le résultat de l’opération. Sinon, y
est évalué et devient le résultat de l’opération. Une expression conditionnelle n’évalue jamais à la fois x
et y
.
L'opérateur conditionnel est associatif à droite, ce qui signifie que les opérations sont groupées de droite à gauche.
Exemple : une expression de la forme
a ? b : c ? d : e
est évaluée commea ? b : (c ? d : e)
. exemple final
Le premier opérande de l’opérateur ?:
doit être une expression pouvant être implicitement convertie en bool
, ou une expression d’un type qui implémente operator true
. Si aucune de ces exigences n’est satisfaite, une erreur de compilation se produit.
Si ref
est présent :
- Une conversion d'identité doit exister entre les types des deux variable_reference, et le type du résultat peut être l'un ou l'autre. Si l’un des types est
dynamic
, l’inférence de type privilégiedynamic
(§8.7). Si l’un des types est un type tuple (§8.3.11), l’inférence de type inclut les noms des éléments lorsque les noms des éléments à la même position correspondent dans les deux tuples. - Le résultat est une référence variable, qui est inscriptible si les deux variable_reference sont inscriptibles.
Remarque : lorsque
ref
est présent, la conditional_expression retourne une référence de variable, qui peut être assignée à une variable référence à l’aide de l’opérateur= ref
ou passée en tant que paramètre référence/entrée/sortie. fin de la remarque
Si ref
n’est pas présent, les deuxième et troisième opérandes, x
et y
, de l’opérateur ?:
déterminent le type de l’expression conditionnelle :
- Si
x
a le typeX
ety
a le typeY
alors,- Si une conversion d’identité existe entre
X
etY
, le résultat est le type commun optimal d’un ensemble d’expressions (§12.6.3.15). Si l’un des types estdynamic
, l’inférence de type privilégiedynamic
(§8.7). Si l’un des types est un type tuple (§8.3.11), l’inférence de type inclut les noms des éléments lorsque les noms des éléments à la même position correspondent dans les deux tuples. - Sinon, si une conversion implicite (§10.2) existe de
X
versY
, mais pas deY
versX
, alorsY
est le type de l’expression conditionnelle. - Sinon, si une conversion d’énumération implicite (§10.2.4) existe de
X
versY
, alorsY
est le type de l’expression conditionnelle. - Sinon, si une conversion d’énumération implicite (§10.2.4) existe de
Y
versX
, alorsX
est le type de l’expression conditionnelle. - Sinon, si une conversion implicite (§10.2) existe de
Y
versX
, mais pas deX
versY
, alorsX
est le type de l’expression conditionnelle. - Sinon, aucun type d’expression ne peut être déterminé et une erreur de compilation se produit.
- Si une conversion d’identité existe entre
- Si seulement
x
ouy
a un type, et quex
ety
sont tous deux implicitement convertibles à ce type, alors c’est le type de l’expression conditionnelle. - Sinon, aucun type d’expression ne peut être déterminé et une erreur de compilation se produit.
Le traitement à l’exécution d’une expression conditionnelle ref de la forme b ? ref x : ref y
se compose des étapes suivantes :
- Premièrement,
b
est évalué, et la valeurbool
deb
est déterminée :- Si une conversion implicite du type de
b
enbool
existe, alors cette conversion implicite est effectuée pour produire une valeurbool
. - Sinon, le
operator true
défini par le type deb
est appelé pour produire une valeurbool
.
- Si une conversion implicite du type de
- Si la valeur
bool
produite à l’étape précédente esttrue
, alorsx
est évalué et la référence de variable résultante devient le résultat de l’expression conditionnelle. - Sinon,
y
est évalué et la référence de variable résultante devient le résultat de l’expression conditionnelle.
Le traitement à l’exécution d’une expression conditionnelle de la forme b ? x : y
se compose des étapes suivantes :
- Premièrement,
b
est évalué, et la valeurbool
deb
est déterminée :- Si une conversion implicite du type de
b
enbool
existe, alors cette conversion implicite est effectuée pour produire une valeurbool
. - Sinon, le
operator true
défini par le type deb
est appelé pour produire une valeurbool
.
- Si une conversion implicite du type de
- Si la valeur
bool
produite à l’étape précédente esttrue
, alorsx
est évalué et converti au type de l’expression conditionnelle, et cela devient le résultat de l’expression conditionnelle. - Sinon,
y
est évalué et converti au type de l’expression conditionnelle, et cela devient le résultat de l’expression conditionnelle.
12.19 Expressions de fonctions anonymes
12.19.1 Général
Une fonction anonyme est une expression qui représente une définition de méthode « en ligne ». Une fonction anonyme n’a pas de valeur ou de type en soi, mais elle peut être convertie en un délégué compatible ou en un type d’arbre d’expression. L’évaluation d’une conversion de fonction anonyme dépend du type cible de la conversion : s’il s’agit d’un type délégué, la conversion évalue à une valeur de délégué faisant référence à la méthode définie par la fonction anonyme. S’il s’agit d’un type d’arbre d’expression, la conversion évalue à un arbre d’expression qui représente la structure de la méthode sous forme de structure d’objet.
Remarque : pour des raisons historiques, il existe deux variantes syntaxiques de fonctions anonymes, à savoir les lambda_expression et les anonymous_method_expression. Dans la plupart des cas, les lambda_expression sont plus concises et plus expressives que les anonymous_method_expressions, qui restent dans le langage pour des raisons de compatibilité ascendante. fin de la remarque
lambda_expression
: 'async'? anonymous_function_signature '=>' anonymous_function_body
;
anonymous_method_expression
: 'async'? 'delegate' explicit_anonymous_function_signature? block
;
anonymous_function_signature
: explicit_anonymous_function_signature
| implicit_anonymous_function_signature
;
explicit_anonymous_function_signature
: '(' explicit_anonymous_function_parameter_list? ')'
;
explicit_anonymous_function_parameter_list
: explicit_anonymous_function_parameter
(',' explicit_anonymous_function_parameter)*
;
explicit_anonymous_function_parameter
: anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'ref'
| 'out'
| 'in'
;
implicit_anonymous_function_signature
: '(' implicit_anonymous_function_parameter_list? ')'
| implicit_anonymous_function_parameter
;
implicit_anonymous_function_parameter_list
: implicit_anonymous_function_parameter
(',' implicit_anonymous_function_parameter)*
;
implicit_anonymous_function_parameter
: identifier
;
anonymous_function_body
: null_conditional_invocation_expression
| expression
| 'ref' variable_reference
| block
;
Lors de la reconnaissance anonymous_function_body, si l'expression null_conditional_invocation_expression et les alternatives d'expression sont toutes deux applicables, c'est la première qui doit être choisie.
Remarque : le chevauchement et la priorité entre les alternatives ici sont uniquement pour des raisons de commodité descriptive ; les règles de grammaire pourraient être élaborées pour éliminer ce chevauchement. ANTLR et d’autres systèmes de grammaire adoptent la même commodité et donc anonymous_function_body a la sémantique spécifiée automatiquement. fin de la remarque
Remarque : lorsqu’elle est traitée comme une expression, une forme syntaxique telle que
x?.M()
serait une erreur si le type de résultat deM
estvoid
(§12.8.13). Mais lorsqu’il est traité comme un null_conditional_invocation_expression, il est permis que le type de résultat soitvoid
. fin de la remarque
Exemple : le type de résultat de
List<T>.Reverse
estvoid
. Dans le code suivant, le corps de l’expression anonyme est une null_conditional_invocation_expression, il n’y a donc pas d’erreur.Action<List<int>> a = x => x?.Reverse();
exemple final
L'opérateur =>
a la même priorité que l'affectation (=
) et est associatif à droite.
Une fonction anonyme avec le modificateur async
est une fonction async et suit les règles décrites dans §15.15.
Les paramètres d’une fonction anonyme sous la forme d’une lambda_expression peuvent être explicitement ou implicitement typés. Dans une liste de paramètres à type explicite, le type de chaque paramètre est clairement indiqué. Dans une liste de paramètres à type implicite, les types des paramètres sont déduits du contexte dans lequel apparaît la fonction anonyme — en particulier, lorsque la fonction anonyme est convertie en un type délégué compatible ou en un type d’arbre d’expression, ce type fournit les types des paramètres (§10.7).
Dans une lambda_expression avec un paramètre unique à type implicite, les parenthèses peuvent être omises dans la liste des paramètres. En d’autres termes, la liste des paramètres d’une fonction anonyme sous la forme suivante :
( «param» ) => «expr»
peut être abrégé en
«param» => «expr»
La liste de paramètres d’une fonction anonyme sous la forme d’un anonymous_method_expression est facultative. Si spécifié, les paramètres doivent être typés explicitement. Sinon, la fonction anonyme est convertible en un délégué avec n’importe quelle liste de paramètres ne contenant pas de paramètres de sortie.
Le bloc corps d’une fonction anonyme est toujours accessible (§13.2).
Exemple : voici quelques exemples de fonctions anonymes :
x => x + 1 // Implicitly typed, expression body x => { return x + 1; } // Implicitly typed, block body (int x) => x + 1 // Explicitly typed, expression body (int x) => { return x + 1; } // Explicitly typed, block body (x, y) => x * y // Multiple parameters () => Console.WriteLine() // No parameters async (t1,t2) => await t1 + await t2 // Async delegate (int x) { return x + 1; } // Anonymous method expression delegate { return 1 + 1; } // Parameter list omitted
exemple final
Le comportement des expressions lambda et des expressions de méthode anonyme est le même, à l’exception des points suivants :
- Les anonymous_method_expressions permettent d'omettre complètement la liste des paramètres, ce qui permet de convertir en types délégués n'importe quelle liste de paramètres de valeur.
- lambda_expressionpermet aux types de paramètres d’être omis et déduits ; tandis que les anonymous_method_expressionexigent que les types de paramètres soient explicitement déclarés.
- Le corps d’une lambda_expression peut être une expression ou un bloc tandis que le corps d’une anonymous_method_expression doit être un bloc.
- Seules les lambda_expression peuvent être converties en types d'arbres d'expression compatibles (§8.6).
12.19.2 Signatures de fonctions anonymes
La anonymous_function_signature d’une fonction anonyme définit les noms et, éventuellement, les types des paramètres de la fonction anonyme. La portée des paramètres de la fonction anonyme est le anonymous_function_body (§7.7). Avec la liste de paramètres (si elle est fournie), le corps de la méthode anonyme constitue un espace de déclaration (§7.3). Il s’agit donc d’une erreur de compilation que le nom d’un paramètre de la fonction anonyme coïncide avec le nom d’une variable locale, d’une constante locale ou d’un paramètre dont la portée inclut l’>anonymous_method_expression ou la lambda_expression.
Si une fonction anonyme possède une explicit_anonymous_function_signature, alors l’ensemble des types délégués compatibles et des types d’arbre d’expression est limité à ceux qui présentent les mêmes types de paramètres et modificateurs dans le même ordre (§10.7). Contrairement aux conversions de groupe de méthodes (§10.8), la contravariance des types de paramètres des fonctions anonymes n’est pas supportée. Si une fonction anonyme ne possède pas de anonymous_function_signature, alors l’ensemble des types délégués compatibles et des types d’arbre d’expression est limité à ceux qui ne comportent pas de paramètres de sortie.
Notez qu’une anonymous_function_signature ne peut inclure ni attributs ni un tableau de paramètres. Néanmoins, une anonymous_function_signature peut être compatible avec un type délégué dont la liste de paramètres contient un tableau de paramètres.
Notez également que la conversion en un type d’arbre d’expression, même si elle est compatible, peut échouer lors de la compilation (§8.6).
12.19.3 Corps de fonctions anonymes
Le corps (expression ou block) d’une fonction anonyme est soumis aux règles suivantes :
- Si la fonction anonyme inclut une signature, les paramètres spécifiés dans la signature sont disponibles dans le corps. Si la fonction anonyme n’a pas de signature, elle peut être convertie en un type délégué ou en un type d’expression comportant des paramètres (§10.7), mais les paramètres ne peuvent pas être accédés dans le corps.
- À l'exception des paramètres par référence indirecte spécifiés dans la signature (s'il y en a une) de la fonction anonyme englobante la plus proche, l'accès du corps à un paramètre par référence indirecte est une erreur compile-time.
- À l'exception des paramètres spécifiés dans la signature (le cas échéant) de la fonction anonyme englobante la plus proche, l'accès par le corps à un paramètre de type
ref struct
constitue une erreur compile-time. - Lorsque le type de
this
est un type struct, le corps commet une erreur compile-time s'il accède àthis
. Ceci est vrai que l’accès soit explicite (comme dansthis.x
) ou implicite (comme dansx
oùx
est un membre d’instance de la struct). Cette règle interdit simplement cet accès et n’affecte pas le fait que la recherche de membre aboutisse ou non à un membre de la struct. - Le corps a accès aux variables externes (§12.19.6) de la fonction anonyme. L’accès à une variable externe fera référence à l’instance de la variable qui est active au moment de l’évaluation de l' lambda_expression ou de l’anonymous_method_expression (§12.19.7).
- Il s'agit d'une erreur de compilation si le corps contient une instruction
goto
, une instructionbreak
ou une instructioncontinue
dont la cible se trouve en dehors du corps ou dans le corps d'une fonction anonyme. - Une instruction
return
dans le corps renvoie le contrôle à partir d'une invocation de la fonction anonyme englobante la plus proche, et non à partir de la fonction membre englobante.
Il est explicitement non spécifié s'il existe un moyen d'exécuter le bloc d'une fonction anonyme autrement que par l'évaluation et l'appel de lambda_expression ou anonymous_method_expression. En particulier, un compilateur peut choisir d’implémenter une fonction anonyme en synthétisant une ou plusieurs méthodes ou types nommés. Les noms de tous ces éléments synthétisés doivent être d’une forme réservée à l’usage du compilateur (§6.4.3).
12.19.4 Résolution de surcharge
Les fonctions anonymes dans une liste d’arguments participent à l’inférence de type et à la résolution de surcharge. Référez-vous à §12.6.3 et §12.6.4 pour voir les règles exactes.
Example : l’exemple suivant illustre l’effet des fonctions anonymes sur la résolution de surcharge.
class ItemList<T> : List<T> { public int Sum(Func<T, int> selector) { int sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } public double Sum(Func<T, double> selector) { double sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } }
La classe
ItemList<T>
possède deux méthodesSum
. Chacune prend un argumentselector
, qui extrait la valeur à additionner d’un élément de liste. La valeur extraite peut être soit unint
soit undouble
et la somme résultante est également soit unint
soit undouble
.Les méthodes
Sum
pourraient par exemple être utilisées pour calculer des sommes à partir d’une liste de lignes de détail dans une commande.class Detail { public int UnitCount; public double UnitPrice; ... } class A { void ComputeSums() { ItemList<Detail> orderDetails = GetOrderDetails( ... ); int totalUnits = orderDetails.Sum(d => d.UnitCount); double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount); ... } ItemList<Detail> GetOrderDetails( ... ) { ... } }
Dans le premier appel de
orderDetails.Sum
, les deux méthodesSum
sont applicables car la fonction anonymed => d.UnitCount
est compatible à la fois avecFunc<Detail,int>
etFunc<Detail,double>
. Cependant, la résolution de surcharge choisit la première méthodeSum
parce que la conversion versFunc<Detail,int>
est meilleure que la conversion versFunc<Detail,double>
.Dans le deuxième appel de
orderDetails.Sum
, seule la deuxième méthodeSum
est applicable car la fonction anonymed => d.UnitPrice * d.UnitCount
produit une valeur de typedouble
. Ainsi, la résolution de surcharge sélectionne la deuxième méthodeSum
pour cet appel.exemple final
12.19.5 Fonctions anonymes et liaison dynamique
Une fonction anonyme ne peut pas être récepteur, argument ou opérande d’une opération à liaison dynamique.
12.19.6 Variables externes
12.19.6.1 Général
Toute variable locale, paramètre par valeur ou tableau de paramètres dont la portée inclut la lambda_expression ou l’anonymous_method_expression est appelée une outer variable de la fonction anonyme. Dans un membre de fonction d’instance d’une classe, la valeur this est considérée comme un paramètre par valeur et est une variable externe de toute fonction anonyme contenue dans le membre de fonction.
12.19.6.2 Variables externes capturées
Lorsqu’une variable externe est référencée par une fonction anonyme, on dit que la variable externe a été captured par la fonction anonyme. Normalement, la durée de vie d’une variable locale est limitée à l’exécution du bloc ou de l’instruction auquel elle est associée (§9.2.9.1). Cependant, la durée de vie d'une variable externe capturée est prolongée au moins jusqu'à ce que le délégué ou l'arbre d'expression créé à partir de la fonction anonyme devienne éligible pour le ramassage des ordures.
Exemple : Dans l'exemple
delegate int D(); class Test { static D F() { int x = 0; D result = () => ++x; return result; } static void Main() { D d = F(); Console.WriteLine(d()); Console.WriteLine(d()); Console.WriteLine(d()); } }
la variable locale
x
est capturée par la fonction anonyme, et la durée de vie dex
est prolongée au moins jusqu'à ce que le délégué renvoyé parF
devienne éligible pour le ramassage des ordures. Puisque chaque appel de la fonction anonyme opère sur la même instance dex
, la sortie de l’exemple est :1 2 3
exemple final
Lorsqu’une variable locale ou un paramètre par valeur est capturé par une fonction anonyme, la variable locale ou le paramètre n’est plus considéré comme une variable fixe (§23.4), mais comme une variable déplaçable. Cependant, les variables externes capturées ne peuvent pas être utilisées dans un statement fixed
(§23.7), donc l’adresse d’une variable externe capturée ne peut être obtenue.
Remarque : contrairement à une variable non capturée, une variable locale capturée peut être exposée simultanément à plusieurs threads d’exécution. fin de la remarque
12.19.6.3 Instanciation de variables locales
Une variable locale est considérée comme instanciée lorsque l'exécution entre dans la portée de la variable.
Exemple : par exemple, lorsque la méthode suivante est appelée, la variable locale
x
est instanciée et initialisée trois fois : une fois pour chaque itération de la boucle.static void F() { for (int i = 0; i < 3; i++) { int x = i * 2 + 1; ... } }
Toutefois, le déplacement de la déclaration de
x
en dehors de la boucle entraîne ainsi une instanciation unique dex
:static void F() { int x; for (int i = 0; i < 3; i++) { x = i * 2 + 1; ... } }
exemple final
Lorsqu’elle n’est pas capturée, il n’y a aucun moyen d’observer exactement combien de fois une variable locale est instanciée : parce que la durée de vie des instanciations est disjointe, il est possible que chaque instanciation utilise simplement le même emplacement mémoire. Cependant, lorsqu’une fonction anonyme capture une variable locale, les effets de l’instanciation deviennent apparents.
Exemple: l’exemple
delegate void D(); class Test { static D[] F() { D[] result = new D[3]; for (int i = 0; i < 3; i++) { int x = i * 2 + 1; result[i] = () => Console.WriteLine(x); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
génère cette sortie :
1 3 5
Cependant, lorsque la déclaration de
x
est déplacée en dehors de la boucle :delegate void D(); class Test { static D[] F() { D[] result = new D[3]; int x; for (int i = 0; i < 3; i++) { x = i * 2 + 1; result[i] = () => Console.WriteLine(x); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
la sortie est :
5 5 5
Notez qu’un compilateur est autorisé (mais pas obligé) à optimiser les trois instanciations en une seule instance de délégué (§10.7.2).
exemple final
Si une boucle for déclare une variable d’itération, cette variable est elle-même considérée comme déclarée en dehors de la boucle.
Exemple : ainsi, si l’exemple est modifié pour capturer la variable d’itération elle-même :
delegate void D(); class Test { static D[] F() { D[] result = new D[3]; for (int i = 0; i < 3; i++) { result[i] = () => Console.WriteLine(i); } return result; } static void Main() { foreach (D d in F()) { d(); } } }
seule une instance de la variable d’itération est capturée, ce qui produit la sortie :
3 3 3
exemple final
Il est possible que les délégués de fonctions anonymes partagent certaines variables capturées tout en ayant des instances distinctes pour d’autres.
Exemple : par exemple, si
F
est modifié enstatic D[] F() { D[] result = new D[3]; int x = 0; for (int i = 0; i < 3; i++) { int y = 0; result[i] = () => Console.WriteLine($"{++x} {++y}"); } return result; }
les trois délégués capturent la même instance de
x
mais des instances distinctes dey
, et la sortie est :1 1 2 1 3 1
exemple final
Des fonctions anonymes distinctes peuvent capturer la même instance d’une variable externe.
Exemple : Dans l’exemple :
delegate void Setter(int value); delegate int Getter(); class Test { static void Main() { int x = 0; Setter s = (int value) => x = value; Getter g = () => x; s(5); Console.WriteLine(g()); s(10); Console.WriteLine(g()); } }
les deux fonctions anonymes capturent la même instance de la variable locale
x
, et elles peuvent ainsi « communiquer » via cette variable. La sortie de l’exemple est :5 10
exemple final
12.19.7 Évaluation des expressions de fonctions anonymes
Une fonction anonyme F
doit toujours être convertie en un type délégué D
ou en un type d’arbre d’expression E
, soit directement, soit par l’exécution d’une expression de création de délégué new D(F)
. Cette conversion détermine le résultat de la fonction anonyme, comme décrit dans §10.7.
12.19.8 Exemple d’implémentation
Cette sous-clause est informative.
Cette sous-clause décrit une implémentation possible des conversions de fonctions anonymes en termes d’autres constructions C#. L’implémentation décrite ici est basée sur les mêmes principes utilisés par un compilateur C# commercial, mais ce n’est pas un moyen d’implémentation obligatoire, ni le seul possible. Elle ne mentionne que brièvement les conversions en arbres d’expression, car leur sémantique exacte est en dehors du champ d’application de cette spécification.
Le reste de cette sous-clause présente plusieurs exemples de code contenant des fonctions anonymes avec différentes caractéristiques. Pour chaque exemple, une traduction correspondante en code utilisant uniquement d’autres constructions C# est fournie. Dans les exemples, on suppose que l’identificateur D
représente le type de délégué suivant :
public delegate void D();
La forme la plus simple d’une fonction anonyme est celle qui ne capture aucune variable externe :
delegate void D();
class Test
{
static void F()
{
D d = () => Console.WriteLine("test");
}
}
Cela peut être traduit en instanciation de délégué qui fait référence à une méthode statique générée par le compilateur dans laquelle le code de la fonction anonyme est placé :
delegate void D();
class Test
{
static void F()
{
D d = new D(__Method1);
}
static void __Method1()
{
Console.WriteLine("test");
}
}
Dans l’exemple suivant, la fonction anonyme fait référence aux membres d’instance de this
:
delegate void D();
class Test
{
int x;
void F()
{
D d = () => Console.WriteLine(x);
}
}
Cela peut être traduit en méthode d’instance générée par le compilateur contenant le code de la fonction anonyme :
delegate void D();
class Test
{
int x;
void F()
{
D d = new D(__Method1);
}
void __Method1()
{
Console.WriteLine(x);
}
}
Dans cet exemple, la fonction anonyme capture une variable locale :
delegate void D();
class Test
{
void F()
{
int y = 123;
D d = () => Console.WriteLine(y);
}
}
La durée de vie de la variable locale doit maintenant être étendue au moins à celle du délégué de la fonction anonyme. Cela peut être obtenu en élevant la variable locale dans un champ d’une classe générée par le compilateur. Instanciation de la variable locale (§12.19.6.3) correspond ensuite à la création d’une instance de la classe générée par le compilateur, et l’accès à la variable locale correspond à l’accès à un champ dans l’instance de la classe générée par le compilateur. De plus, la fonction anonyme devient une méthode d’instance de la classe générée par le compilateur :
delegate void D();
class Test
{
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}
class __Locals1
{
public int y;
public void __Method1()
{
Console.WriteLine(y);
}
}
}
Enfin, la fonction anonyme suivante capture this
ainsi que deux variables locales ayant des durées de vie différentes :
delegate void D();
class Test
{
int x;
void F()
{
int y = 123;
for (int i = 0; i < 10; i++)
{
int z = i * 2;
D d = () => Console.WriteLine(x + y + z);
}
}
}
Ici, une classe générée par le compilateur est créée pour chaque bloc dans lequel des variables locales sont capturées de sorte que les variables locales dans les différents blocs puissent avoir des durées de vie indépendantes. Une instance de __Locals2
, la classe générée par le compilateur pour le bloc interne, contient la variable locale z
et un champ qui fait référence à une instance de __Locals1
. Une instance de __Locals1
, la classe générée par le compilateur pour le bloc externe, contient la variable locale y
et un champ qui fait référence this
du membre de fonction englobant. Grâce à ces structures de données, il est possible d’accéder à toutes les variables externes capturées via une instance de __Local2
, et le code de la fonction anonyme peut ainsi être implémenté sous forme de méthode d’instance de cette classe.
delegate void D();
class Test
{
int x;
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++)
{
__Locals2 __locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}
class __Locals1
{
public Test __this;
public int y;
}
class __Locals2
{
public __Locals1 __locals1;
public int z;
public void __Method1()
{
Console.WriteLine(__locals1.__this.x + __locals1.y + z);
}
}
}
La même technique utilisée ici pour capturer des variables locales peut également être employée lors de la conversion de fonctions anonymes en arbres d’expression : les références aux objets générés par le compilateur peuvent être stockées dans l’arbre d’expression, et l’accès aux variables locales peut être représenté par des accès aux champs de ces objets. L’avantage de cette approche est qu’elle permet aux variables locales « lifted » d’être partagées entre les délégués et les arbres d’expression.
Fin du texte informatif.
12.20 Expressions de requête
12.20.1 Général
Expressions de requête offrent une syntaxe intégrée au langage pour les requêtes, similaire aux langages de requête relationnels et hiérarchiques tels que SQL et XQuery.
query_expression
: from_clause query_body
;
from_clause
: 'from' type? identifier 'in' expression
;
query_body
: query_body_clauses? select_or_group_clause query_continuation?
;
query_body_clauses
: query_body_clause
| query_body_clauses query_body_clause
;
query_body_clause
: from_clause
| let_clause
| where_clause
| join_clause
| join_into_clause
| orderby_clause
;
let_clause
: 'let' identifier '=' expression
;
where_clause
: 'where' boolean_expression
;
join_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression
;
join_into_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression 'into' identifier
;
orderby_clause
: 'orderby' orderings
;
orderings
: ordering (',' ordering)*
;
ordering
: expression ordering_direction?
;
ordering_direction
: 'ascending'
| 'descending'
;
select_or_group_clause
: select_clause
| group_clause
;
select_clause
: 'select' expression
;
group_clause
: 'group' expression 'by' expression
;
query_continuation
: 'into' identifier query_body
;
Une expression de requête commence par une clause from
et se termine soit par une clause select
, soit par une clause group
. La clause from
initiale peut être suivie de zéro ou plusieurs clauses from
, let
, where
, join
ou orderby
. Chaque clause from
est un générateur introduisant une variable de portée qui parcourt les éléments d’une séquence. Chaque clause let
introduit une variable de portée représentant une valeur calculée à l’aide des variables de portée précédentes. Chaque clause where
est un filtre qui exclut des éléments du résultat. Chaque clause join
compare les clés spécifiées de la séquence source avec celles d’une autre séquence, produisant des paires correspondantes. Chaque clause orderby
réordonne les éléments selon des critères spécifiés. La clause finale select
ou group
définit la forme du résultat en fonction des variables de portée. Enfin, une clause into
peut être utilisée pour « fusionner » des requêtes en considérant les résultats d’une requête comme un générateur dans une requête suivante.
12.20.2 Ambiguïtés dans les expressions de requête
Les expressions de requête utilisent un certain nombre de mots clés contextuels (§6.4.4) : ascending
, by
, descending
, equals
, from
, group
, into
, join
, let
, on
, orderby
, select
et where
.
Pour éviter les ambiguïtés pouvant découler de l’utilisation de ces identificateurs à la fois comme mots-clés et noms simples, ces identificateurs sont considérés comme des mots-clés partout dans une expression de requête, à moins qu’ils ne soient préfixés par « @
» (§6.4.4) auquel cas ils sont traités comme des identificateurs. À cet effet, une expression de requête est toute expression qui commence par « from
identifier » suivie de n’importe quel jeton sauf « ;
», « =
» ou « ,
».
12.20.3 Traduction des expressions de requête
12.20.3.1 Général
Le langage C# ne spécifie pas la sémantique d’exécution des expressions de requête. Au contraire, les expressions de requête sont traduites en appels de méthodes qui respectent le modèle des expressions de requête (§12.20.4). Plus précisément, les expressions de requête sont traduites en appels de méthodes nommées Where
, Select
, SelectMany
, Join
, GroupJoin
, OrderBy
, OrderByDescending
, ThenBy
, ThenByDescending
, GroupBy
, et Cast
. On s’attend à ce que ces méthodes possèdent des signatures et des types de retour particuliers, comme décrit dans §12.20.4. Ces méthodes peuvent être des méthodes d’instance de l’objet interrogé ou des méthodes d’extension externes à l’objet. Ces méthodes réalisent l’exécution effective de la requête.
La traduction des expressions de requête en appels de méthodes est une correspondance syntaxique qui intervient avant toute liaison de type ou résolution de surcharge. Après la traduction des expressions de requête, les appels de méthodes résultants sont traités comme des appels de méthodes ordinaires, ce qui peut à son tour révéler des erreurs à la compilation. Ces conditions d’erreur incluent, sans s’y limiter, des méthodes inexistantes, des arguments de types incorrects et des méthodes génériques pour lesquelles l’inférence de type échoue.
Une expression de requête est traitée en appliquant de manière répétée les traductions suivantes jusqu’à ce qu’il ne soit plus possible d’effectuer d’autres réductions. Les traductions sont énumérées dans l’ordre d’application : chaque section suppose que les traductions des sections précédentes ont été effectuées de manière exhaustive et, une fois épuisées, une section ne sera pas réexaminée ultérieurement lors du traitement de la même expression de requête.
Il s’agit d’une erreur de temps de compilation qu’une expression de requête inclut une affectation à une variable de portée, ou l’utilisation d’une variable de portée comme argument pour une référence ou un paramètre de sortie.
Certaines traductions injectent des variables de plage avec des identificateurs transparents dénotés par *. Celles-ci sont décrites plus en détail dans §12.20.3.8.
12.20.3.2 Expressions de requête avec continuations
Une expression de requête avec une continuation à la suite de son corps de requête
from «x1» in «e1» «b1» into «x2» «b2»
est traduite en
from «x2» in ( from «x1» in «e1» «b1» ) «b2»
Les traductions dans les sections suivantes supposent que les requêtes ne comportent pas de continuations.
Exemple: l’exemple :
from c in customers group c by c.Country into g select new { Country = g.Key, CustCount = g.Count() }
est traduit en :
from g in (from c in customers group c by c.Country) select new { Country = g.Key, CustCount = g.Count() }
la traduction finale de laquelle est :
customers. GroupBy(c => c.Country). Select(g => new { Country = g.Key, CustCount = g.Count() })
exemple final
12.20.3.3 Types explicites de variables de portée
Une clause from
qui spécifie explicitement un type de variable de portée
from «T» «x» in «e»
est traduite en
from «x» in ( «e» ) . Cast < «T» > ( )
Une clause join
qui spécifie explicitement un type de variable de portée
join «T» «x» in «e» on «k1» equals «k2»
est traduite en
join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»
Les traductions dans les sections suivantes supposent que les requêtes ne comportent pas de types explicites de variables de portée.
Exemple: l’exemple
from Customer c in customers where c.City == "London" select c
est traduite en
from c in (customers).Cast<Customer>() where c.City == "London" select c
la traduction finale de laquelle est
customers. Cast<Customer>(). Where(c => c.City == "London")
exemple final
Remarque : les types explicites de variables de portée sont utiles pour interroger des collections qui implémentent l’interface non générique
IEnumerable
, mais pas l’interface génériqueIEnumerable<T>
. Dans l'exemple ci-dessus, ce serait le cas si les clients soient de typeArrayList
. fin de la remarque
12.20.3.4 Expressions de requête dégénérées
Une expression de requête de la forme
from «x» in «e» select «x»
est traduite en
( «e» ) . Select ( «x» => «x» )
Exemple: l’exemple
from c in customers select c
est traduite en
(customers).Select(c => c)
exemple final
Une expression de requête dégénérée est celle qui sélectionne de manière triviale les éléments de la source.
Remarque: les phases ultérieures de la traduction (§12.20.3.6 et §12.20.3.7) suppriment les requêtes dégénérées introduites par d’autres étapes de traduction en les remplaçant par leur source. Il est toutefois important de s’assurer que le résultat d’une expression de requête ne soit jamais l’objet source lui-même. Dans le cas contraire, le renvoi du résultat d’une telle requête pourrait involontairement exposer des données privées (par exemple, un tableau d’éléments) à un appelant. Cette étape protège ainsi les requêtes dégénérées écrites directement dans le code source en appelant explicitement
Select
sur la source. Il appartient ensuite aux implémenteurs deSelect
et des autres opérateurs de requête de veiller à ce que ces méthodes ne renvoient jamais l’objet source lui-même. fin de la remarque
12.20.3.5 Clauses from, let, where, join et orderby
Une expression de requête avec une seconde clause from
suivie d’une clause select
from «x1» in «e1»
from «x2» in «e2»
select «v»
est traduite en
( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )
Exemple: l’exemple
from c in customers from o in c.Orders select new { c.Name, o.OrderID, o.Total }
est traduite en
(customers). SelectMany(c => c.Orders, (c,o) => new { c.Name, o.OrderID, o.Total } )
exemple final
Une expression de requête avec une seconde clause from
suivie d’un corps de requête Q
contenant un ensemble non vide de clauses de corps de requête :
from «x1» in «e1»
from «x2» in «e2»
Q
est traduite en
from * in («e1») . SelectMany( «x1» => «e2» ,
( «x1» , «x2» ) => new { «x1» , «x2» } )
Q
Exemple: l’exemple
from c in customers from o in c.Orders orderby o.Total descending select new { c.Name, o.OrderID, o.Total }
est traduite en
from * in (customers). SelectMany(c => c.Orders, (c,o) => new { c, o }) orderby o.Total descending select new { c.Name, o.OrderID, o.Total }
la traduction finale de laquelle est
customers. SelectMany(c => c.Orders, (c,o) => new { c, o }). OrderByDescending(x => x.o.Total). Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })
où
x
est un identificateur généré par le compilateur qui est autrement invisible et inaccessible.exemple final
Expression let
suivie de sa clause from
précédente :
from «x» in «e»
let «y» = «f»
...
est traduite en
from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )
...
Exemple: l’exemple
from o in orders let t = o.Details.Sum(d => d.UnitPrice * d.Quantity) where t >= 1000 select new { o.OrderID, Total = t }
est traduite en
from * in (orders).Select( o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }) where t >= 1000 select new { o.OrderID, Total = t }
la traduction finale de laquelle est
orders .Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }) .Where(x => x.t >= 1000) .Select(x => new { x.o.OrderID, Total = x.t })
où
x
est un identificateur généré par le compilateur qui est autrement invisible et inaccessible.exemple final
Expression where
suivie de sa clause from
précédente :
from «x» in «e»
where «f»
...
est traduite en
from «x» in ( «e» ) . Where ( «x» => «f» )
...
Une clause join
immédiatement suivie d’une clause select
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2»
select «v»
est traduite en
( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )
Exemple: l’exemple
from c in customers join o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }
est traduite en
(customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.Name, o.OrderDate, o.Total })
exemple final
Une clause join
suivie d’une clause de corps de requête :
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2»
...
est traduite en
from * in ( «e1» ) . Join(
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })
...
Une clause join
-into
immédiatement suivie d’une clause select
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2» into «g»
select «v»
est traduite en
( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «g» ) => «v» )
Une clause join into
suivie d’une clause de corps de requête
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2» into *g»
...
est traduite en
from * in ( «e1» ) . GroupJoin(
«e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...
Exemple: l’exemple
from c in customers join o in orders on c.CustomerID equals o.CustomerID into co let n = co.Count() where n >= 10 select new { c.Name, OrderCount = n }
est traduite en
from * in (customers).GroupJoin( orders, c => c.CustomerID, o => o.CustomerID, (c, co) => new { c, co }) let n = co.Count() where n >= 10 select new { c.Name, OrderCount = n }
la traduction finale de laquelle est
customers .GroupJoin( orders, c => c.CustomerID, o => o.CustomerID, (c, co) => new { c, co }) .Select(x => new { x, n = x.co.Count() }) .Where(y => y.n >= 10) .Select(y => new { y.x.c.Name, OrderCount = y.n })
où
x
ety
sont des identificateurs générés par le compilateur qui sont par ailleurs invisibles et inaccessibles.exemple final
Une clause orderby
et sa clause from
précédente :
from «x» in «e»
orderby «k1» , «k2» , ... , «kn»
...
est traduite en
from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...
Si une clause ordering
spécifie un indicateur de direction décroissante, un appel de OrderByDescending
ou ThenByDescending
est alors produit.
Exemple: l’exemple
from o in orders orderby o.Customer.Name, o.Total descending select o
a la traduction finale
(orders) .OrderBy(o => o.Customer.Name) .ThenByDescending(o => o.Total)
exemple final
Les traductions suivantes supposent qu’il n’existe pas de clauses let
, where
, join
ou orderby
, et qu’il n’y a pas plus d’une clause from
initiale dans chaque expression de requête.
12.20.3.6 Clauses de sélection
Une expression de requête de la forme
from «x» in «e» select «v»
est traduite en
( «e» ) . Select ( «x» => «v» )
sauf lorsque «v»
est l’identificateur «x»
, la traduction est simplement
( «e» )
Exemple: l’exemple
from c in customers.Where(c => c.City == "London") select c
est simplement traduite en
(customers).Where(c => c.City == "London")
exemple final
12.20.3.7 Clauses de regroupement
Une clause group
from «x» in «e» group «v» by «k»
est traduite en
( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )
sauf lorsque «v»
est l’identificateur «x»
, la traduction est
( «e» ) . GroupBy ( «x» => «k» )
Exemple: l’exemple
from c in customers group c.Name by c.Country
est traduite en
(customers).GroupBy(c => c.Country, c => c.Name)
exemple final
12.20.3.8 Identificateurs transparents
Certaines traductions injectent des variables de portée avec des identificateurs transparents dénotés par *
. Les identificateurs transparents n’existent que comme étape intermédiaire dans le processus de traduction des expressions de requête.
Lorsqu’une traduction d’expression de requête injecte un identificateur transparent, les étapes de traduction suivantes propagent cet identificateur transparent dans les fonctions anonymes et les initialisateurs d’objets anonymes. Dans ces contextes, les identificateurs transparents se comportent de la manière suivante :
- Lorsqu’un identificateur transparent est utilisé en tant que paramètre dans une fonction anonyme, les membres du type anonyme associé sont automatiquement disponibles dans le corps de la fonction anonyme.
- Lorsqu'un membre ayant un identificateur transparent est dans la portée, les membres de ce membre sont également dans la portée.
- Lorsqu’un identificateur transparent apparaît en tant que déclarateur de membre dans un initialisateur d’objet anonyme, il introduit un membre avec un identificateur transparent.
Dans les étapes de traduction décrites ci-dessus, les identificateurs transparents sont toujours introduits conjointement avec des types anonymes, dans le but de capturer plusieurs variables de portée en tant que membres d’un même objet. Une implémentation de C# est autorisée à utiliser un mécanisme différent des types anonymes pour regrouper plusieurs variables de portée. Les exemples de traduction suivants supposent que des types anonymes sont utilisés, et montrent une traduction possible des identificateurs transparents.
Exemple: l’exemple
from c in customers from o in c.Orders orderby o.Total descending select new { c.Name, o.Total }
est traduite en
from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o }) orderby o.Total descending select new { c.Name, o.Total }
qui est ensuite traduit en
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(* => o.Total) .Select(\* => new { c.Name, o.Total })
ce qui, une fois les identificateurs transparents effacés, est équivalent à
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(x => x.o.Total) .Select(x => new { x.c.Name, x.o.Total })
où
x
est un identificateur généré par le compilateur qui est autrement invisible et inaccessible.L’exemple
from c in customers join o in orders on c.CustomerID equals o.CustomerID join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
est traduite en
from * in (customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
qui se réduit encore à
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }) .Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { c.Name, o.OrderDate, p.ProductName })
la traduction finale de laquelle est
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d }) .Join(products, y => y.d.ProductID, p => p.ProductID, (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })
où
x
ety
sont des identificateurs générés par le compilateur qui sont par ailleurs invisibles et inaccessibles. exemple final
12.20.4 Le modèle des expressions de requête
Le modèle des expressions de requête établit un ensemble de méthodes que les types peuvent implémenter pour prendre en charge les expressions de requête.
Un type générique C<T>
prend en charge le modèle des expressions de requête si ses méthodes membres publiques et les méthodes d’extension accessibles publiquement pouvaient être remplacées par la définition de classe suivante. Les membres et les méthodes extensives accessibles sont appelés la « forme » d'un type générique C<T>
. Un type générique est utilisé afin d’illustrer les relations appropriées entre les types de paramètres et de retour, mais il est également possible d’implémenter le modèle pour des types non génériques.
delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);
class C
{
public C<T> Cast<T>() { ... }
}
class C<T> : C
{
public C<T> Where(Func<T,bool> predicate) { ... }
public C<U> Select<U>(Func<T,U> selector) { ... }
public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
Func<T,U,V> resultSelector) { ... }
public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
Func<T,E> elementSelector) { ... }
}
class O<T> : C<T>
{
public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}
class G<K,T> : C<T>
{
public K Key { get; }
}
Les méthodes ci-dessus utilisent les types de délégués génériques Func<T1, R>
et Func<T1, T2, R>
, mais elles auraient tout aussi bien pu utiliser d’autres types de délégués ou d’arbres d’expression avec les mêmes relations entre les types de paramètres et de retour.
Remarque : la relation recommandée entre
C<T>
etO<T>
garantit que les méthodesThenBy
etThenByDescending
ne sont disponibles que sur le résultat d’unOrderBy
ou d’unOrderByDescending
. fin de la remarque
Remarque : la forme recommandée du résultat de
GroupBy
: une séquence de séquences, où chaque séquence interne possède une propriétéKey
additionnelle. fin de la remarque
Remarque : étant donné que les expressions de requête sont traduites en appels de méthodes au moyen d’une correspondance syntaxique, les types disposent d’une grande flexibilité quant à la manière dont ils implémentent tout ou partie du modèle des expressions de requête. Par exemple, les méthodes du modèle peuvent être implémentées en tant que méthodes d’instance ou en tant que méthodes d’extension, puisque les deux possèdent la même syntaxe d’appel, et les méthodes peuvent demander des délégués ou des arbres d’expression, car les fonctions anonymes sont convertibles en l’un comme en l’autre. Les types n’implémentant qu’une partie du modèle des expressions de requête ne prennent en charge que les traductions d’expressions de requête qui correspondent aux méthodes supportées par ce type. fin de la remarque
Remarque : l’espace de noms
System.Linq
fournit une implémentation du modèle des expressions de requête pour tout type qui implémente l’interfaceSystem.Collections.Generic.IEnumerable<T>
. fin de la remarque
12.21 Opérateurs d’assignation
12.21.1 Général
Tous les opérateurs d’affectation, à l’exception d’un, attribuent une nouvelle valeur à une variable, une propriété, un événement ou un élément d’indexeur. L’exception, = ref
, assigne une référence de variable (§9.5) à une variable de référence (§9.7).
assignment
: unary_expression assignment_operator expression
;
assignment_operator
: '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
| right_shift_assignment
;
L’opérande gauche d’une affectation doit être une expression classifiée comme une variable ou, sauf pour = ref
, un accès à une propriété, un accès à un indexeur, un accès à un événement ou un tuple. Une expression de déclaration n'est pas directement autorisée en tant qu'opérande gauche, mais peut apparaître en tant qu'étape dans l'évaluation d'une affectation de déconstruction.
L’opérateur =
est appelé l’opérateur d’affectation simple. Elle affecte la ou les valeurs de l'opérande de droite à la variable, à la propriété, à l'élément d'indexation ou aux éléments de tuple donnés par l'opérande de gauche. L’opérande gauche de l’opérateur d’affectation simple ne doit pas être un accès à un événement (sauf tel que décrit dans §15.8.2). L’opérateur d’affectation simple est décrit dans §12.21.2.
L'opérateur = ref
est appelé opérateur d'affectation ref. Il fait de l'opérande de droite, qui doit être une variable_reference (§9.5), le référent de la variable de référence désignée par l'opérande de gauche. L'opérateur d'affectation ref est décrit au §12.21.3.
Les opérateurs d’affectation autres que =
et = ref
sont appelés les opérateurs d’affectation composés. Ces opérateurs effectuent l’opération indiquée sur les deux opérandes, puis affectent la valeur résultante à la variable, à la propriété ou à l’élément d’indexeur indiqué par l’opérande gauche. Les opérateurs d’affectation composés sont décrits dans §12.21.4.
Les opérateurs +=
et -=
avec une expression d'accès aux événements comme opérande gauche sont appelés les opérateurs d'assignation d'événement . Aucun autre opérateur d'affectation n'est valide avec un accès à un événement comme opérande gauche. Les opérateurs d’affectation pour les événements sont décrits dans §12.21.5.
Les opérateurs d'affectation sont associatifs à droite, ce qui signifie que les opérations sont groupées de droite à gauche.
Exemple : une expression de la forme
a = b = c
est évaluée commea = (b = c)
. exemple final
12.21.2 Assignation simple
L’opérateur =
est appelé l’opérateur d’affectation simple.
Si l’opérande gauche d’une affectation simple est de la forme E.P
ou E[Ei]
où E
a le type à la compilation dynamic
, alors l’affectation est liée dynamiquement (§12.3.3). Dans ce cas, le type à la compilation de l’expression d’affectation est dynamic
, et la résolution décrite ci-dessous s’effectuera à l’exécution en fonction du type à l’exécution de E
. Si l’opérande gauche est de la forme E[Ei]
où au moins un élément de Ei
a le type à la compilation dynamic
, et que le type à la compilation de E
n’est pas un tableau, l’accès à l’indexeur résultant est lié dynamiquement, mais avec une vérification limitée à la compilation (§12.6.5).
Une affectation simple où l'opérande de gauche est classé comme un tuple est également appelée affectation de déconstruction. Si l’un des éléments de tuple de l’opérande gauche a un nom d’élément, une erreur de compilation survient. Si l'un des éléments du tuple de l'opérande gauche est une declaration_expression et que tout autre élément n'est pas une declaration_expression ou un simple rejet, une erreur de compilation se produit.
Le type d’une affectation simple x = y
est le type d’une affectation à x
de y
, qui est déterminé récursivement comme suit :
- Si
x
est une expression de tuple(x1, ..., xn)
, et quey
peut être décomposé en une expression de tuple(y1, ..., yn)
comportantn
éléments (§12.7), et que chaque affectation àxi
deyi
a le typeTi
, alors l’affectation a le type(T1, ..., Tn)
. - Sinon, si
x
est classée comme une variable, que la variable n’est pasreadonly
, quex
a un typeT
, et quey
possède une conversion implicite versT
, alors l’affectation a le typeT
. - Sinon, si
x
est classée comme une variable à typage implicite (c’est-à-dire une expression de déclaration à typage implicite) et quey
a un typeT
, alors le type inféré de la variable estT
, et l’affectation a le typeT
. - Sinon, si
x
est classé comme un accès à une propriété ou à un indexeur, que la propriété ou l'indexeur possède un accesseur d'ensemble accessible, quex
est de typeT
et quey
possède une conversion implicite enT
, alors l'affectation est de typeT
. - Dans le cas contraire, l'affectation n'est pas valide et une erreur de liaison se produit.
Le traitement à l'exécution d'une affectation simple de la forme x = y
avec le type T
est effectué sous la forme d'une affectation à x
de y
avec le type T
, qui consiste en les étapes récursives suivantes :
x
est évalué s'il ne l'était pas déjà.- Si
x
est classée comme une variable,y
est évalué et, si nécessaire, converti enT
par une conversion implicite (§10.2).- Si la variable indiquée par
x
est un élément d’un tableau de reference_type, une vérification à l’exécution est effectuée pour s’assurer que la valeur calculée poury
est compatible avec l’instance du tableau dontx
est un élément. La vérification réussit siy
estnull
, ou si une conversion de référence implicite (§10.2.8) existe du type de l’instance référencée pary
vers le type réel des éléments de l’instance du tableau contenantx
. Sinon, une exceptionSystem.ArrayTypeMismatchException
est levée. - La valeur résultant de l’évaluation et de la conversion de
y
est stockée à l’emplacement déterminé par l’évaluation dex
, et est renvoyée en tant que résultat de l’affectation.
- Si la variable indiquée par
- Si
x
est classé comme un accès de propriété ou d'indexeur :-
y
est évalué et, si nécessaire, converti enT
par une conversion implicite (§10.2). - L'accesseur set de
x
est invoqué avec la valeur résultant de l'évaluation et de la conversion dey
comme argument de valeur. - La valeur résultant de l'évaluation et de la conversion de
y
est produite en tant que résultat de l'affectation.
-
- Si
x
est classé comme un tuple(x1, ..., xn)
de cardinalitén
:y
est déconstruit avecn
éléments en une expression de tuplee
.- un tuple résultant
t
est créé en convertissante
enT
à l’aide d’une conversion de tuple implicite. - pour chaque
xi
dans l’ordre de gauche à droite, une affectation àxi
det.Itemi
est effectuée, sauf que lesxi
ne sont pas réévalués. t
La valeur résultant de l'évaluation et de la conversion de XXX est fournie comme résultat de l'affectation.
Remarque : si le type à la compilation de
x
estdynamic
et qu’il existe une conversion implicite du type à la compilation dey
versdynamic
, aucune résolution à l’exécution n’est requise. fin de la remarque
Remarque: les règles de covariance des tableaux (§17.6) permettent qu’une valeur d’un type de tableau
A[]
soit une référence à une instance d’un type de tableauB[]
, à condition qu’une conversion de référence implicite existe deB
versA
. En raison de ces règles, l’affectation à un élément de tableau d’un reference_type nécessite une vérification à l’exécution pour s’assurer que la valeur assignée est compatible avec l’instance du tableau. Dans l'exemplestring[] sa = new string[10]; object[] oa = sa; oa[0] = null; // OK oa[1] = "Hello"; // OK oa[2] = new ArrayList(); // ArrayTypeMismatchException
la dernière assignation provoque un
System.ArrayTypeMismatchException
parce qu'une référence à unArrayList
ne peut pas être stockée dans un élément d'unstring[]
.fin de la remarque
Lorsqu’une propriété ou un indexeur déclaré dans un struct_type est la cible d’une affectation, l’expression d’instance associée à l’accès à la propriété ou à l’indexeur doit être classifiée comme une variable. Si l’expression d’instance est classifiée comme une valeur, une erreur de liaison se produit.
Remarque: en raison de §12.8.7, la même règle s’applique également aux champs. fin de la remarque
Exemple : Étant donné les déclarations :
struct Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } } struct Rectangle { Point a, b; public Rectangle(Point a, Point b) { this.a = a; this.b = b; } public Point A { get { return a; } set { a = value; } } public Point B { get { return b; } set { b = value; } } }
dans l'exemple
Point p = new Point(); p.X = 100; p.Y = 100; Rectangle r = new Rectangle(); r.A = new Point(10, 10); r.B = p;
les affectations à
p.X
,p.Y
,r.A
etr.B
sont permises parce quep
etr
sont des variables. Cependant, dans l’exempleRectangle r = new Rectangle(); r.A.X = 10; r.A.Y = 10; r.B.X = 100; r.B.Y = 100;
les affectations sont toutes invalides, car
r.A
etr.B
ne sont pas des variables.exemple final
12.21.3 Affectation ref
L'opérateur = ref
est connu sous le nom d'opérateur d'affectation ref.
L’opérande gauche doit être une expression liée à une variable de référence (§9.7), un paramètre de référence (autre que this
), un paramètre de sortie ou un paramètre d’entrée. L'opérande de droite est une expression qui produit une variable_reference désignant une valeur du même type que l'opérande de gauche.
Il y a une erreur de compilation si le contexte de sécurité de référence (§9.7.2) de l’opérande gauche est plus étendu que celui de l’opérande droit.
L'opérande de droite doit être définitivement affecté au moment de l'affectation ref.
Lorsque l’opérande gauche s'associe à un paramètre de sortie, c'est une erreur si ce paramètre de sortie n'a pas été concrètement affecté au début de l’opérateur d’affectation ref.
Si l’opérande gauche est une référence modifiable (c’est-à-dire qu’elle désigne autre chose qu’un paramètre local ou d’entrée ref readonly
), alors l’opérande droite doit être une variable_reference modifiable. Si la variable de l'opérande de droite est inscriptible, l'opérande de gauche peut être une référence inscriptible ou en lecture seule.
L'opération fait de l'opérande gauche un alias de la variable de l'opérande droit. L'alias peut être mis en lecture seule même si la variable de l'opérande droit est accessible en écriture.
L'opérateur d'affectation ref produit une variable_reference du type assigné. Il est accessible en écriture si l'opérande gauche est accessible en écriture.
L'opérateur d'affectation ref ne doit pas lire l'emplacement de stockage référencé par l'opérande de droite.
Exemple : voici quelques exemples d’utilisation de
= ref
:public static int M1() { ... } public static ref int M2() { ... } public static ref uint M2u() { ... } public static ref readonly int M3() { ... } public static void Test() { int v = 42; ref int r1 = ref v; // OK, r1 refers to v, which has value 42 r1 = ref M1(); // Error; M1 returns a value, not a reference r1 = ref M2(); // OK; makes an alias r1 = ref M2u(); // Error; lhs and rhs have different types r1 = ref M3(); // error; M3 returns a ref readonly, which r1 cannot honor ref readonly int r2 = ref v; // OK; make readonly alias to ref r2 = ref M2(); // OK; makes an alias, adding read-only protection r2 = ref M3(); // OK; makes an alias and honors the read-only r2 = ref (r1 = ref M2()); // OK; r1 is an alias to a writable variable, // r2 is an alias (with read-only access) to the same variable }
exemple final
Remarque : lors de la lecture d’un code utilisant un opérateur
= ref
, il peut être tentant de considérer que la partieref
fait partie de l’opérande. C’est particulièrement déroutant lorsque l’opérande est une expression conditionnelle?:
. Par exemple, lors de la lecture deref int a = ref b ? ref x : ref y;
, il est important de lire= ref
comme étant l'opérateur etb ? ref x : ref y
comme étant l'opérande de droite :ref int a = ref (b ? ref x : ref y);
. Il est important de noter que l'expressionref b
ne fait pas partie de cette instruction, même si cela semble être le cas à première vue. fin de la remarque
12.21.4 Assignation composée
Si l’opérande gauche d’une affectation composée est de la forme E.P
ou E[Ei]
où E
a le type à la compilation dynamic
, alors l’affectation est liée dynamiquement (§12.3.3). Dans ce cas, le type à la compilation de l’expression d’affectation est dynamic
, et la résolution décrite ci-dessous s’effectuera à l’exécution en fonction du type à l’exécution de E
. Si l’opérande gauche est de la forme E[Ei]
où au moins un élément de Ei
a le type à la compilation dynamic
, et que le type à la compilation de E
n’est pas un tableau, l’accès à l’indexeur résultant est lié dynamiquement, mais avec une vérification limitée à la compilation (§12.6.5).
Une opération de la forme x «op»= y
est traitée en appliquant la résolution de surcharge des opérateurs binaires (§12.4.5) comme si l’opération avait été écrite x «op» y
. Ainsi,
- Si le type de retour de l’opérateur sélectionné est implicitement convertible en le type de
x
, l’opération est évaluée commex = x «op» y
, sauf quex
est évalué une seule fois. - Sinon, si l’opérateur sélectionné est un opérateur prédéfini, si le type de retour de l’opérateur sélectionné est explicitement convertible en le type de
x
et siy
est implicitement convertible en le type dex
ou si l’opérateur est un opérateur de décalage, alors l’opération est évaluée commex = (T)(x «op» y)
, oùT
est le type dex
, sauf quex
est évalué une seule fois. - Sinon, l'affectation composée n'est pas valide et une erreur de temps de liaison se produit.
Le terme « evaluated only once » (évalué une seule fois) signifie que lors de l’évaluation de x «op» y
, les résultats de toutes les expressions constituantes de x
sont temporairement sauvegardés puis réutilisés lors de l’exécution de l’affectation à x
.
Exemple : Dans l'affectation
A()[B()] += C()
, oùA
est une méthode renvoyantint[]
, etB
etC
sont des méthodes renvoyantint
, les méthodes ne sont invoquées qu'une seule fois, dans l'ordreA
,B
,C
. exemple final
Lorsque l'opérande gauche d'une affectation composée est un accès à une propriété ou à un indexeur, la propriété ou l'indexeur doit avoir à la fois un accesseur get et un accesseur set. Dans le cas contraire, une erreur de liaison survient.
La deuxième règle ci-dessus permet d’évaluer x «op»= y
en tant que x = (T)(x «op» y)
dans certains contextes. La règle existe afin que les opérateurs prédéfinis puissent être utilisés comme opérateurs composés lorsque l’opérande de gauche est de type sbyte
, byte
, short
, ushort
, ou char
. Même lorsque les deux arguments sont de l’un de ces types, les opérateurs prédéfinis produisent un résultat de type int
, comme décrit dans §12.4.7.3. Ainsi, sans conversion, il ne serait pas possible d'affecter le résultat à l'opérande de gauche.
L’effet intuitif de la règle pour les opérateurs prédéfinis est simplement que x «op»= y
est autorisé si x «op» y
et x = y
le sont tous deux.
Exemple: Dans le code suivant
byte b = 0; char ch = '\0'; int i = 0; b += 1; // OK b += 1000; // Error, b = 1000 not permitted b += i; // Error, b = i not permitted b += (byte)i; // OK ch += 1; // Error, ch = 1 not permitted ch += (char)1; // OK
La raison intuitive pour chaque erreur est qu’une affectation simple correspondante aurait également été une erreur.
exemple final
Remarque : cela signifie également que les opérations d'affectation composées prennent en charge les opérateurs levés. Étant donné qu'une affectation composée
x «op»= y
est évaluée commex = x «op» y
oux = (T)(x «op» y)
, les règles d'évaluation couvrent implicitement les opérateurs levés. fin de la remarque
12.21.5 Assignation d'événement
Si l'opérande gauche de l'opérateur a += or -=
est classé comme accès à un événement, l'expression est évaluée comme suit :
- L'expression d'instance, s'il y en a une, de l'accès à l'événement est évaluée.
- L'opérande droit de l'opérateur
+=
ou-=
est évalué et, si nécessaire, converti au type de l'opérande gauche par une conversion implicite (§10.2). - Un accesseur d’événement est appelé, avec une liste d’arguments composée de la valeur calculée à l’étape précédente. Si l’opérateur était
+=
, l’accesseur d’ajout est appelé ; si l’opérateur était-=
, l’accesseur de suppression est appelé.
Une expression d’affectation d’événement ne produit pas de valeur. Ainsi, une expression d’affectation d’événement n’est valide que dans le contexte d’une statement_expression (§13.7).
12.22 Expression
Une expression est soit une non_assignment_expression, soit une affectation.
expression
: non_assignment_expression
| assignment
;
non_assignment_expression
: declaration_expression
| conditional_expression
| lambda_expression
| query_expression
;
12.23 Expressions constantes
Une expression constante est une expression qui doit être entièrement évaluée à la compilation.
constant_expression
: expression
;
Une expression constante doit avoir la valeur null
ou l’un des types suivants :
-
sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,bool
,string
; - un type d’énumération ; ou
- une expression de valeur par défaut (§12.8.21) pour un type référence.
Seuls les constructions suivantes sont autorisées dans les expressions constantes :
- Les littéraux (y compris le littéral
null
). - Les références aux membres
const
des types classe et structure. - Les références aux membres de types d’énumération.
- Les références aux constantes locales.
- Les sous-expressions entre parenthèses, qui sont elles-mêmes des expressions constantes.
- Les expressions Cast.
-
checked
et les expressionsunchecked
. - les expressions
nameof
. - Les opérateurs unaires prédéfinis
+
,-
,!
(négation logique) et~
. - Les opérateurs binaires prédéfinis
+
,-
,*
,/
,%
,<<
,>>
,&
,|
,^
,&&
,||
,==
,!=
,<
,>
,<=
et>=
. - L'opérateur conditionnel
?:
. - L'opérateur null-forgiving
!
(§12.8.9). - Les expressions
sizeof
, à condition que le type non managé soit l’un des types spécifiés dans §23.6.9 pour lesquelssizeof
renvoie une valeur constante. - Les expressions de valeur par défaut, à condition que le type soit l’un des types énumérés ci-dessus.
Les conversions suivantes sont autorisées dans les expressions constantes :
- Les conversions d’identité
- Les conversions numériques
- Conversions d'énumérations
- Conversions d'expressions constantes
- Les conversions implicites et explicites de références, à condition que la source des conversions soit une expression constante qui évalue à la valeur
null
.
Note : Les autres conversions, y compris la mise en boîte, la mise hors boîte et les conversions de référence implicites de non-
null
, ne sont pas autorisées dans les expressions constantes. fin de la remarque
Exemple: Dans le code suivant
class C { const object i = 5; // error: boxing conversion not permitted const object str = "hello"; // error: implicit reference conversion }
l'initialisation de
i
est une erreur car une conversion de type « boxing » est nécessaire. L’initialisation destr
est une erreur car une conversion implicite de référence à partir d’une valeur non-null
est requise.exemple final
Chaque fois qu’une expression remplit les exigences énumérées ci-dessus, elle est évaluée à la compilation. Ceci est vrai même si l’expression est une sous-expression d’une expression plus vaste qui contient des constructions non constantes.
L’évaluation à la compilation des expressions constantes utilise les mêmes règles que l’évaluation à l’exécution des expressions non constantes, sauf que, là où l’évaluation à l’exécution aurait levé une exception, l’évaluation à la compilation provoque une erreur de compilation.
À moins qu’une expression constante ne soit explicitement placée dans un contexte unchecked
, les dépassements de capacité qui surviennent lors d’opérations arithmétiques sur des types entiers et des conversions durant l’évaluation à la compilation de l’expression provoquent toujours des erreurs de compilation (§12.8.20).
Les expressions constantes sont requises dans les contextes énumérés ci-dessous et cela est indiqué dans la grammaire par l’utilisation de constant_expression. Dans ces contextes, une erreur de compilation survient si une expression ne peut être entièrement évaluée à la compilation.
- Les déclarations de constantes (§15.4)
- Déclarations des membres d'une énumération (§19.4)
- Les arguments par défaut des listes de paramètres (§15.6.2)
- les étiquettes
case
d'une instructionswitch
(§13.8.3). - les instructions
goto case
(§13.10.4) - Les longueurs des dimensions dans une expression de création de tableau (§12.8.17.5) qui inclut un initialisateur.
- Les attributs (§22)
- Dans un constant_pattern (§11.2.3)
Une conversion implicite d’expression constante (§10.2.11) permet de convertir une expression constante de type int
en sbyte
, byte
, short
, ushort
, uint
ou ulong
, à condition que la valeur de l’expression constante soit comprise dans l’intervalle du type de destination.
12.24 Expressions booléennes
Une boolean_expression est une expression qui produit un résultat de type bool
; soit directement, soit par l’application de operator true
dans certains contextes comme spécifié ci-après :
boolean_expression
: expression
;
L'expression conditionnelle de contrôle d’un if_statement (§13.8.2), while_statement (§13.9.2), do_statement (§13.9.3) ou for_statement (§13.9.4) est une boolean_expression. L’expression conditionnelle de contrôle de l’opérateur ?:
(§12.18) suit les mêmes règles qu’une boolean_expression, mais, pour des raisons de priorité des opérateurs, est classée comme une null_coalescing_expression.
Une boolean_expressionE
doit être capable de produire une valeur de type bool
, comme suit :
- Si E est implicitement convertible en
bool
, alors cette conversion implicite est appliquée à l’exécution. - Sinon, la résolution de surcharge de l’opérateur unaire (§12.4.4) est utilisée pour trouver une implémentation optimale unique de
operator true
surE
, et cette implémentation est appliquée à l’exécution. - Si aucun opérateur de ce type n’est trouvé, une erreur de liaison survient.
ECMA C# draft specification