C'est un comportement étrange que je ne peux pas comprendre. Dans mon exemple, j'ai une classe Sample<T>
et un opérateur de conversion implicite de T
à Sample<T>
.
private class Sample<T>
{
public readonly T Value;
public Sample(T value)
{
Value = value;
}
public static implicit operator Sample<T>(T value) => new Sample<T>(value);
}
Le problème se produit lors de l’utilisation d’un type de valeur nullable pour T
tel que int?
.
{
int? a = 3;
Sample<int> sampleA = a;
}
Voici la partie clé:
À mon avis, cela ne devrait pas être compilé car Sample<int>
définit une conversion de int
en Sample<int>
mais pas de int?
à Sample<int>
. Mais il compile et fonctionne avec succès! (J'entends par là l'opérateur de conversion invoqué et 3
sera assigné au champ readonly
.)
Et ça devient encore pire. Ici, l'opérateur de conversion n'est pas appelé et sampleB
sera défini sur null
:
{
int? b = null;
Sample<int> sampleB = b;
}
Une bonne réponse serait probablement divisée en deux parties:
Vous pouvez voir comment le compilateur réduit ce code:
int? a = 3;
Sample<int> sampleA = a;
dans this :
int? nullable = 3;
int? nullable2 = nullable;
Sample<int> sample = nullable2.HasValue ? ((Sample<int>)nullable2.GetValueOrDefault()) : null;
Car Sample<int>
est une classe dont l'instance peut être affectée d'une valeur null et, avec un tel opérateur implicite, le type sous-jacent d'un objet nullable peut également être affecté. Donc, les affectations comme celles-ci sont valables:
int? a = 3;
int? b = null;
Sample<int> sampleA = a;
Sample<int> sampleB = b;
Si Sample<int>
serait un struct
, cela donnerait évidemment une erreur.
EDIT: Alors pourquoi est-ce possible? Je ne pouvais pas le trouver dans les spécifications, car il s'agit d'une violation délibérée des spécifications, qui n'est conservée que pour des raisons de compatibilité ascendante. Vous pouvez lire à ce sujet dans code :
VIOLATION SPECIFIQUE DELIBEREE:
Le compilateur natif permet une conversion "levée" même lorsque le type de retour de la conversion n'est pas un type de valeur non nullable. Par exemple, si nous avons une conversion de struct S en chaîne, alors une conversion "levée" de S? to string est considéré par le compilateur natif comme existant, avec la sémantique de "s.HasValue? (string) s.Value: (string) null". Le compilateur Roslyn perpétue cette erreur pour des raisons de compatibilité ascendante.
Voilà comment cette "erreur" est implémentée dans Roslyn:
Sinon, si le type de retour de la conversion est un type de valeur nullable, un type de référence ou un type de pointeur P, nous l'abaissons comme suit:
temp = operand temp.HasValue ? op_Whatever(temp.GetValueOrDefault()) : default(P)
Donc, selon spec pour un opérateur de conversion défini par l'utilisateur donné T -> U
_ il existe un opérateur levé T? -> U?
où T
et U
sont des types de valeur non nullables. Cependant, une telle logique est également implémentée pour un opérateur de conversion où U
est un type de référence pour la raison ci-dessus.
PARTIE 2 Comment empêcher la compilation du code dans ce scénario? Eh bien, il y a un moyen. Vous pouvez définir un opérateur implicite supplémentaire spécifiquement pour un type nullable et le décorer avec un attribut Obsolete
. Cela nécessiterait que le paramètre de type T
soit limité à struct
:
public class Sample<T> where T : struct
{
...
[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(T? value) => throw new NotImplementedException();
}
Cet opérateur sera choisi comme premier opérateur de conversion pour le type nullable car il est plus spécifique.
Si vous ne pouvez pas faire une telle restriction, vous devez définir chaque opérateur pour chaque type de valeur séparément (si vous êtes vraiment vous pouvez tirer parti de la réflexion et de la génération de code à l'aide de modèles):
[Obsolete("Some error message", error: true)]
public static implicit operator Sample<T>(int? value) => throw new NotImplementedException();
Cela donnerait une erreur s'il était référencé n'importe où dans le code:
Erreur CS0619 'L'opérateur Sample.implicit, Sample (int?)' Est obsolète: 'Un message d'erreur'
Je pense qu'il est levé opérateur de conversion en action. La spécification dit que:
Étant donné un opérateur de conversion défini par l'utilisateur qui convertit un type de valeur S non nullable en un type de valeur T non nullable, il existe un opérateur de conversion en hauteur qui convertit à partir de S? à T ?. Cet opérateur de conversion levé effectue un dévoilement de S? to S suivi de la conversion définie par l'utilisateur de S à T suivi d'un retour à la ligne de T à T ?, sauf qu'une valeur nulle S? convertit directement en une valeur nulle T ?.
Il semble que ce ne soit pas applicable ici, car bien que le type S
soit le type valeur ici (int
), le type T
n'est pas le type valeur (Sample
classe) . Cependant ce problème dans le référentiel Roslyn indique qu'il s'agit en fait d'un bogue dans la spécification. Et Roslyn code la documentation le confirme:
Comme mentionné ci-dessus, nous nous écartons ici de la spécification, de deux manières. Premièrement, nous vérifions uniquement si la forme levée est inapplicable pour la forme levée. Deuxièmement, nous ne sommes censés appliquer la sémantique de levage que si les paramètres de conversion et les types de retour sont les deux types de valeur non nullables.
En fait, le compilateur natif détermine s'il convient de rechercher un formulaire levé sur la base de:
- Le type que nous convertissons en fin de compte est-il un type de valeur nullable?
- Le type de paramètre de la conversion est-il un type de valeur non nullable?
- Le type que nous convertissons en fin de compte est-il un type de valeur nullable, un type de pointeur ou un type de référence?
Si la réponse à toutes ces questions est "oui", nous passons à nullable et voyons si l'opérateur résultant est applicable.
Si le compilateur suivait la spécification, cela produirait une erreur dans ce cas comme prévu (et dans certaines versions antérieures, ce fut le cas), mais ce n'est plus le cas.
Donc, pour résumer: je pense que le compilateur utilise la forme levée de votre opérateur implicite, ce qui devrait être impossible selon la spécification, mais le compilateur diverge de la spécification ici, car:
Comme décrit dans la première citation décrivant le fonctionnement de l'opérateur levé (en plus du fait que nous permettons à T
d'être un type de référence) - vous pouvez noter qu'il décrit exactement ce qui se passe dans votre cas. null
valorisé S
(int?
) est assigné directement à T
(Sample
) sans opérateur de conversion, et non nul est décompressé en int
et exécuté par votre opérateur (encapsulation à T?
n'est évidemment pas nécessaire si T
est un type de référence).
Pourquoi le code du premier extrait est-il compilé?
Un exemple de code d'un code source de Nullable<T>
Qui peut être trouvé ici :
[System.Runtime.Versioning.NonVersionable]
public static explicit operator T(Nullable<T> value) {
return value.Value;
}
[System.Runtime.Versioning.NonVersionable]
public T GetValueOrDefault(T defaultValue) {
return hasValue ? value : defaultValue;
}
La structure Nullable<int>
A un opérateur explicite remplacé ainsi que la méthode GetValueOrDefault
. Le compilateur utilise l'une de ces deux méthodes pour convertir int?
En T
.
Après cela, il lance la implicit operator Sample<T>(T value)
.
Voici une image approximative de ce qui se passe:
Sample<int> sampleA = (Sample<int>)(int)a;
Si nous imprimons typeof(T)
à l'intérieur de l'opérateur implicite Sample<T>
, Il affichera: System.Int32
.
Dans votre deuxième scénario, le compilateur n'utilise pas le implicit operator Sample<T>
Et attribue simplement null
à sampleB
.