IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
{
    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 :

 
Sélectionnez
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 :

 
Sélectionnez
ubyte a = 1;
ubyte b = 2;
writeln(typeof(a + b).stringof);  // l'addition ne se fait pas en ubyte

La sortie :

 
Sélectionnez
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 :

  1. Si l'une des valeurs est real, alors l'autre valeur est convertie en real ;
  2. Sinon, si l'une des valeurs est double, alors l'autre valeur est convertie en double ;
  3. Sinon, si l'une des valeurs est float, alors l'autre valeur est convertie en float ;
  4. 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 :

    1. Si les deux types sont les mêmes, alors aucune autre étape n'est nécessaire,
    2. Si le type signé est plus large que le type non signé, alors la valeur non signée est convertie vers le type signé,
    3. Sinon le type signé est converti vers le type non signé.

Malheureusement, cette dernière règle peut être à l'origine de bogues subtils :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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é :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
to!TypeDestination(valeur)

Le programme suivant essaie de convertir une valeur double vers short et une chaîne vers int :

 
Sélectionnez
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() :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

précédentsommairesuivant