50. Conversions de Types, cast ▲
Les variables doivent être compatibles avec les expressions dans lesquelles elles apparaissent. Comme vous l'avez peut-être déjà remarqué dans les programmes que nous avons vus jusqu'à maintenant, D est un langage statiquement typé, ce qui veut dire que la compatibilité des types est vérifiée lors de la compilation.
Toutes les expressions que nous avons écrites jusqu'à maintenant avaient des types compatibles parce que dans le cas contraire le code aurait été rejeté par le compilateur. Ce qui suit est un exemple de code qui a des types incompatibles :
char
[] tranche;
writeln
(
tranche +
5
); // ← ERREUR de compilation
Le compilateur rejette le code à cause de l'incompatibilité des types char[] et int pour l'opération d'addition :
Error: incompatible types for
((
tranche) + (
5
)): 'char[]'
and 'int'
Des types différents ne sont pas nécessairement incompatibles ; des types différents peuvent dans certains cas être utilisés dans les expressions en toute sécurité. Par exemple, une variable int peut être utilisée à la place d'une valeur double sans problème :
double somme =
1
.25;
int increment =
3;
somme +=
increment;
Même si somme et increment sont de types différents, ce code est correct parce qu'incrémenter une variable double avec une valeur int est légal.
50-1. Conversion automatique des types▲
Les conversions automatiques de types sont aussi appelées conversions implicites de types.
Même si double et int sont des types compatibles dans l'expression précédente, l'opération d'addition doit toujours être évaluée comme un type spécifique au niveau du microprocesseur. Comme nous l'avons vu dans le chapitre sur les virgules flottantesTypes à virgule flottante, le type 64 bits double est plus large que le type 32 bits int. De plus, toute valeur qui peut être représentée dans une variable int peut être représentée dans une variable double.
Quand le compilateur rencontre une expression qui implique des types incompatibles, il convertit d'abord les parties de l'expression vers un type commun et évalue ensuite l'expression entière. Les conversions automatiques qui sont effectuées par le compilateur se font dans une direction qui évite toute perte de données. Par exemple, double peut contenir toute valeur que int peut contenir mais l'inverse n'est pas vrai. Dans l'exemple précédent, l'opération += peut fonctionner parce que toute valeur int peut être convertie vers double en toute sécurité.
La valeur résultant d'une conversion automatique est toujours une variable anonyme et elle est souvent temporaire. La valeur originale ne change pas. Par exemple, dans notre cas la conversion automatique lors de l'opération += ne change pas le type de increment ; celui-ci reste un int. Par contre, une valeur temporaire de type double est construite à partir de la valeur d'increment. La conversion qui se déroule en arrière-plan est équivalente au code suivant :
{
double
une_valeur_double_anonyme =
increment;
somme +=
une_valeur_double_anonyme;
}
Le compilateur convertit la valeur int vers une valeur temporaire double et utilise cette valeur dans l'opération. Dans cet exemple, la variable temporaire vit uniquement pendant l'opération +=.
Les conversions automatiques ne sont pas limitées aux opérations arithmétiques. Il y a d'autres cas où les types sont convertis vers d'autres types automatiquement. Tant que les conversions de types sont correctes, le compilateur en profite pour utiliser les valeurs dans les expressions. Par exemple, une valeur byte peut être passée en argument pour un paramètre int :
void
fonc
(
int
nombre)
{
// ...
}
void
main
(
)
{
byte
petiteValeur =
7
;
fonc
(
petiteValeur); // conversion automatique
}
Dans ce code, une valeur int temporaire est d'abord construite et ensuite la fonction est appelée avec cette valeur.
50-1-1. Promotions entières▲
Les valeurs des types qui sont dans la colonne de gauche du tableau suivant ne font jamais partie d'expressions arithmétiques en tant que telles. Chaque type est d'abord promu vers le type de la colonne à droite du tableau.
De |
Vers |
||
bool |
int |
||
byte |
int |
||
ubyte |
int |
||
short |
int |
||
ushort |
int |
||
char |
int |
||
wchar |
Int |
||
dchar |
uint |
Les promotions entières sont également appliquées aux valeurs enum.
Les raisons d'être des promotions entières sont aussi historiques (où les règles viennent du C) que techniques, le type arithmétique naturel du processeur étant int. Par exemple, même si les deux variables suivantes sont ubyte, l'opération d'addition n'est effectuée qu'après que les deux valeurs ont été promues en int :
ubyte
a =
1
;
ubyte
b =
2
;
writeln
(
typeof
(
a +
b).stringof); // l'addition ne se fait pas en ubyte
La sortie :
int
Notez que les types respectifs des variables a et b ne changent pas ; seules leurs valeurs sont temporairement promues en int pour la durée de l'opération d'addition.
50-1-2. Conversions arithmétiques▲
Il y a d'autres règles de conversions qui sont appliquées pour les opérations arithmétiques. En général, les conversions arithmétiques automatiques sont appliquées dans la direction sûre : du type le plus étroit au type le plus large. Même si cette règle est facile à retenir et est correcte dans la plupart des cas, les règles de conversions automatiques sont très compliquées et, dans les cas de conversion signé vers non signé, sont de potentielles causes de bogues.
Les règles de conversions arithmétiques sont les suivantes :
- Si l'une des valeurs est real, alors l'autre valeur est convertie en real ;
- Sinon, si l'une des valeurs est double, alors l'autre valeur est convertie en double ;
- Sinon, si l'une des valeurs est float, alors l'autre valeur est convertie en float ;
-
Sinon, les promotions entières sont appliquées selon le tableau de la section précédente, et les règles suivantes sont ensuite appliquées :
- Si les deux types sont les mêmes, alors aucune autre étape n'est nécessaire,
- Si le type signé est plus large que le type non signé, alors la valeur non signée est convertie vers le type signé,
- Sinon le type signé est converti vers le type non signé.
Malheureusement, cette dernière règle peut être à l'origine de bogues subtils :
int
a =
0
;
int
b =
1
;
size_t c =
0
;
writeln
(
a -
b +
c); // Résultat surprenant !
Étonnement, le résultat n'est pas -1 mais size_t.max :
0
Même si l'on s'attendrait à ce que 0 - 1 + 0 donne -1, selon les règles ci-avant, le type de l'expression entière est size_t et non int ; et comme size_t ne peut pas représenter des valeurs négatives, le résultat soupasse et devient size_t.max.
50-1-3. Conversions const ▲
Comme nous l'avons vu précédemment dans le chapitre sur les paramètres de fonctionsLes paramètres de fonction. les types référence peuvent être automatiquement convertis vers le const du même type. Les conversions vers const sont sûres parce que la largeur du type ne change pas et const est une promesse de ne pas modifier la variable :
dchar
[] entre_accolades
(
const
dchar
[] texte)
{
return
"{"
~
texte ~
"}"
;
}
void
main
(
)
{
dchar
[] bienvenue;
bienvenue ~=
"bonjour le monde"
;
entre_accolades
(
bienvenue);
}
La variable mutable bienvenue est automatiquement convertie vers un const dchar[] lorsqu'elle est passée à la fonction entre_accolades.
Comme nous l'avons aussi vu précédemment, la conversion inverse n'est pas automatique. Une référence const n'est pas automatiquement convertie vers une référence mutable :
dchar
[] entre_accolades
(
const
dchar
[] texte)
{
dchar
[] argument =
texte; // ERREUR de compilation
// ...
}
Notez que cette partie concerne uniquement les références ; comme les variables de types valeur sont copiées, il n'est de toute façon pas possible d'assigner l'original à travers la copie :
const
int
nombreDeCoins =
4
;
int
laCopie =
nombreDeCoins; // compile (type valeur)
Cette conversion de const vers mutable est légale parce que la copie n'est pas une référence vers l'original.
50-1-4. Conversions immutables ▲
immutable indiquant qu'une variable ne peut jamais changer, aucune conversion depuis ou vers immutable n'est automatique :
string a =
"salut"
; // caractères immuables
char
[] b =
a; // Erreur de compilation
string c =
b; // Erreur de compilation
Comme pour la section précédente (conversions de const), cette partie ne concerne que les types référence. Comme les variables de types valeur sont de toute façon copiées, les conversions depuis et vers immutable sont valides :
immutable a =
10
;
int
b =
a; // compile (type valeur)
50-1-5. Conversions enum ▲
Comme nous l'avons vu dans le chapitre sur les énumérationsLes énumérations (enum) enum définit des constantes nommées :
enum
Couleur {
pique, coeur, carreau, trefle }
Souvenez-vous que comme aucune valeur n'est spécifiée explicitement, les valeurs des membres de l'énumération commencent par zéro et sont automatiquement incrémentées de 1 en 1. Ainsi, la valeur de Couleur.trefle est 3.
Les valeurs enum sont automatiquement converties vers des types entiers. Par exemple, la valeur de Couleur.coeur est comprise comme 1 dans le calcul suivant et le résultat est 11 :
int
resultat =
10
+
Couleur.coeur;
assert
(
resultat ==
11
);
La conversion inverse n'est pas automatique : les valeurs entières ne sont pas automatiquement converties vers les valeurs enum correspondantes. Par exemple, on pourrait s'attendre que la variable couleur ci-dessous prenne pour valeur Couleur.carreau, mais le code ne peut pas être compilé :
Couleur couleur =
2; // ERREUR de compilation
Comme nous le verrons juste après, les conversions depuis les entiers vers les valeurs enum sont néanmoins possibles, mais elles doivent être explicites.
50-1-6. Conversions booléennes▲
false et true sont automatiquement convertis vers 0 et 1, respectivement :
int
a =
false
;
assert
(
a ==
0
);
int
b =
true
;
assert
(
b ==
1
);
La conversion inverse est également automatique, mais seulement pour deux valeurs spéciales. Les littéraux 0 et 1 sont convertis automatiquement vers false et true, respectivement :
bool a =
0
;
assert
(!
a); // false
bool b =
1
;
assert
(
b); // true
Les autres valeurs littérales ne peuvent pas être converties vers bool automatiquement :
bool b =
2
; // ERREUR de compilation
50-1-7. Conversions automatiques vers bool dans les instructions conditionnelles▲
Certaines instructions utilisent des expressions logiques : if, while, etc. Pour les expressions logiques de telles instructions, en plus de bool, la plupart des autres types peuvent également être utilisés. La valeur zéro est automatiquement convertie vers false et les valeurs non nulles sont automatiquement converties vers true.
int
i;
// ...
if
(
i) {
// ← valeur int utilisée comme une expression logique
// ... 'i' ne vaut pas zéro
}
else
{
// ... 'i' vaut zero
}
De manière similaire, les références nulles (null) sont automatiquement converties vers false et les références non nulles sont automatiquement converties vers true. Cela facilite la vérification qu'une référence n'est pas nulle avant de s'en servir :
int
[] a;
// ...
if
(
a) {
// ← conversion booléenne automatique
// ... non null; 'a' peut être utilisé...
}
else
{
// ... null; 'a' ne peut pas être utilisé...
}
50-1-8. Conversions de types explicites▲
Comme nous venons de le voir, il y a des cas où les conversions automatiques ne sont pas possibles :
- les conversions depuis des types plus larges vers des types plus restreints ;
- les conversions depuis const vers mutable ;
- les conversions immutables ;
- les conversions depuis les entiers vers les valeurs enum.
S'il sait qu'une telle conversion est sûre, le programmeur peut explicitement demander une conversion de types avec l'une des trois méthodes suivantes :
- en appelant la fonction std.conv.to ;
- en appelant la fonction std.exception.assumeUnique ;
- en utilisant l'opérateur cast.
50-1-8-1. La fonction to pour la plupart des conversions▲
La fonction to, que nous avons déjà utilisée surtout pour convertir des valeurs vers string, peut en fait être utilisée pour beaucoup d'autres types. Sa syntaxe complète est la suivante :
to!(
TypeDestination)(
valeur)
Puisque c'est un modèle, on peut utiliser la fonction to avec la notation raccourcie des paramètres de modèles : quand le type de destination ne consiste qu'en un seul token (en général, qu'un seul mot), elle peut être appelée sans la première paire de parenthèses :
to!
TypeDestination
(
valeur)
Le programme suivant essaie de convertir une valeur double vers short et une chaîne vers int :
void
main
(
)
{
double
d =
-
1
.75
;
short
s =
d; // ERREUR de compilation
int
i =
"42"
; // ERREUR de compilation
}
Comme toutes les valeurs double ne peuvent pas être représentées par short et comme toute chaîne ne peut pas être représentée comme un entier, ces conversions ne sont pas automatiques. Quand le programmeur sait que les conversions sont en fait sûres ou que les conséquences d'une mauvaise conversion sont acceptables, les types peuvent être convertis par to() :
import
std.conv;
void
main
(
)
{
double
d =
-
1
.75
;
short
s =
to!
short
(
d);
assert
(
s ==
-
1
);
int
i =
to!
int
(
"42"
);
assert
(
i ==
42
);
}
Notez que puisque short ne peut pas représenter les valeurs fractionnaires, la valeur convertie est -1.
La fonction to est sûre : elle lève une exception quand une conversion n'est pas possible.
50-1-8-2. assumeUnique() pour des conversions immutables rapides▲
La fonction to peut également effectuer des conversions immutables :
int
[] tranche =
[ 10
, 20
, 30
];
auto
trancheImmuable =
to!(
immutable int
[])(
tranche);
Pour garantir que les éléments de trancheImmuable ne changeront jamais, elle ne peut pas partager ses éléments avec tranche. Pour cette raison, la fonction to crée une tranche nouvelle avec des éléments immutables. Sinon, les modifications des éléments de tranche entraîneraient également la modification des éléments de trancheImmuable. Ce comportement est le même avec la propriété .idup des tableaux.
Nous pouvons voir que les éléments de trancheImmuable sont en fait des copies des éléments de tranche en regardant l'adresse de leurs premiers éléments :
assert
(&(
tranche[0
]) !=
&(
trancheImmuable[0
]));
Parfois, cette copie n'est pas nécessaire et peut ralentir le programme de façon négligeable dans certains cas. Par exemple, considérons la fonction suivante qui prend une tranche immutable :
void
calculer
(
immutable int
[] coordonnees)
{
// ...
}
void
main
(
)
{
int
[] nombres;
nombres ~=
10
;
// ... autres diverses modifications ...
nombres[0
] =
42
;
calculer
(
nombres); // ERREUR de compilation
}
Ce programme ne peut pas être compilé parce que l'appelant ne passe pas un argument immutable à la fonction calculer. Comme nous l'avons vu auparavant, une tranche immutable peut être créée par la fonction to :
import
std.conv;
// ...
auto
nombresImmuables =
to!(
immutable int
[])(
nombres);
calculer
(
nombresImmuables); // ← maintenant, compile
Cependant, si on n'a besoin de nombres que pour produire cet argument et qu'elle ne sera jamais utilisée après l'appel de la fonction, copier ses éléments vers nombresImmuables n'est pas nécessaire. La fonction assumeUnique rend les éléments d'une tranche immuables sans les copier :
import
std.exception;
// ...
auto
nombresImmuables =
assumeUnique
(
nombres);
calculer
(
nombresImmuables);
assert
(
nombres is
null
); // La tranche originale devient nulle
assumeUnique retourne une nouvelle tranche qui fournit un accès immuable aux éléments existants. Elle rend aussi la tranche originale null pour empêcher les éléments d'être accidentellement modifiés.
50-1-8-3. L'opérateur cast ▲
Les fonctions to() et assumeUnique() utilisent toutes deux l'opérateur de conversion cast, qui est également disponible pour le programmeur.
L'opérateur cast prend le type de destination entre parenthèses :
cast
(
TypeDestination)valeur
cast est utile pour les conversions que la fonction to() ne peut pas effectuer de façon sûre. to() ne réalise pas les conversions suivantes :
Couleur couleur =
to!
Couleur
(
2
); // ERREUR de compilation
bool b =
to!
bool
(
2
); //ERREUR de compilation
Parfois, seul le programmeur peut savoir si une valeur entière correspond à une valeur enum valide ou s'il fait sens de traiter une valeur entière comme un booléen. L'opérateur cast peut être utilisé quand on sait que la conversion est correcte selon la logique du programme :
Couleur couleur =
cast
(
Couleur)2
;
assert
(
couleur ==
Couleur.carreau);
bool b =
cast
(
bool)2
;
assert
(
b);
Bien que cela arrive rarement, certaines interfaces de bibliothèques C demandent de stocker une valeur de pointeur dans un type non-pointeur. S'il est garanti que la conversion préserve la bonne valeur, cast peut aussi effectuer la conversion entre des types pointeur et non-pointeur :
size_t valeurPointeurSauvée =
cast
(
size_t) p;
// ...
int
*
p2 =
cast
(
int
*
)valeurPointeurSauvée;
50-1-9. Résumé▲
- Les conversions de types automatiques sont principalement celles qui se font dans le sens sûr : du type le plus restreint au type le plus large et de mutable vers const.
- Cependant, les conversions vers les types non signés peuvent avoir des effets surprenants parce que les types non signés ne peuvent pas avoir de valeur négative.
- Les types enum peuvent automatiquement être convertis vers des valeurs entières, mais la conversion inverse n'est pas automatique.
- false et true sont automatiquement convertis vers 0 et 1, respectivement. De manière similaire, les valeurs nulles (dans le sens « zéro ») sont automatiquement converties vers false et les valeurs non nulles sont automatiquement converties vers true.
- Les références nulles sont automatiquement converties vers false et les références non nulles sont automatiquement converties vers true.
- La fonction to couvre la plupart des conversions explicites.
- La fonction assumeUnique convertit vers immutable sans copier.
- L'opérateur cast est l'outil de conversion le plus puissant.