Comprendre le transtypage avec C#

Ce qui suit s'intéresse aux raisons de certaines opérations de transtypage en C#, mais touche aussi légèrement à la question de la signature des fonctions, mais en surface seulement, sans l'approfondir. Les fonctions constituent un sujet très important en programmation, et méritent un traitement de meilleure qualité que ce qui suit.

Soit le programme suivant, qui a pour rôle de lire une base , un exposant , puis de calculer et d'afficher , le tout exprimé d'abord en pseudocode, puis en code C# « naïf » (au sens de « trop simple pour être vrai ») :

Pseudocode Code C# (naïf)
Lire base
Lire exposant
résultat ← base^exposant
Écrire résultat
//
// Fichier : Exponentiation.cs
// Auteur : votre chic prof
// Rôle : calculer et afficher le fruit d'une exponentiation
// Date : 2 septembre 2013
//
using System;

float valBase;
float valExposant;
float résultat;
Console.Write("Base? ");
valBase = float.Parse(Console.ReadLine());
Console.Write("Exposant? ");
valExposant = float.Parse(Console.ReadLine());
résultat = Math.Pow(valBase, valExposant);
Console.WriteLine("{0}^{1}=={2}", valBase, valExposant, résultat);

Tout cela semble simple et évident, or si le pseudocode est tout à fait convenable, le code C# nous met face à la réalité de la technique et refuse de compiler, produisant le message d'erreur suivant :

Impossible de convertir implicitement le type 'double' en 'float'. Une conversion explicite existe (un cast est-il manquant ?)

L'erreur est rapportée pour la ligne 21 du programme, plus précisément à l'appel de Math.Pow().

Comprendre le message

Si nous examinons la signature de Math.Pow(), nous constatons ce qui suit :

public static double Pow(double x, double y)
                         ^^^^^^^^  ^^^^^^^^

Sans décortiquer cette signature dans le détail (nous y reviendrons), constatons :

Remarquons que notre programme, lui, utilise des float (nombres à virgule flottante, simple précision) pour les paramètres utilisés pour appeler la fonction. Ainsi, l'écriture suivante :

Math.Pow(valBase, valExposant);

... demande au compilateur C# de prendre nos deux float et d'en faire des double. Il se trouve que, pour C#, cette opération est implicitement légale : pour C#, il s'agit d'une promotion, où les valeurs à l'appel gagnent en précision lorsque l'appel se réalise.

Exprimé en mots, on demande ici à C# de prendre valBase et de le transtyper de float en double, puis de prendre valExposant et de le transtyper lui aussi de float en double. Ce transtypage est implicite car aux yeux du compilateur, il n'est pas dangereux. Ainsi, ce n'est pas la raison du problème.

La signature ne se termine pas là. Réexaminons-la à nouveau :

public static double Pow(double x, double y)
              ^^^^^^

Constatons donc le mot double placé avant le nom de la fonction (Pow). Ce mot indique le type du résultat de l'exécution de la fonction, c'est-à-dire ce que nous obtiendrons suite à un appel de la fonction. Ici, la fonction nous indique qu'elle retournera un double, or si nous réexaminons notre programme C# :

Code C# (naïf)
//
// Fichier : Exponentiation.cs
// Auteur : votre chic prof
// Rôle : calculer et afficher le fruit d'une exponentiation
// Date : 2 septembre 2013
//
using System;

float valBase;
float valExposant;
float résultat;
// ^^^^^^^^^^^
Console.Write("Base? ");
valBase = float.Parse(Console.ReadLine());
Console.Write("Exposant? ");
valExposant = float.Parse(Console.ReadLine());
résultat = Math.Pow(valBase, valExposant);
// ^^^^^
Console.WriteLine("{0}^{1}=={2}", valBase, valExposant, résultat);

... nous pouvons constater que la variable qui recevra le fruit du calcul réalisé par Math.Pow() est de type float. Ceci signifie que notre programme cherche à déposer un double, donc un nombre à virgule flottante de double précision, dans une variable dont le type est float, donc un nombre à virgule flottante de simple précision. Aux yeux de C#, cette opération est risquée, car elle entraîne manifestement une perte d'information.

Nous venons de trouver notre coupable!

Les solutions qui s'offrent à nous

Deux grandes options s'offrent à nous :

Examinons brièvement chacune de ces deux options.

Changer le type de la variable

Si nous changeons le type de la variable résultat, alors le programme devient :

Code C# (avec type de résultat modifié)
//
// Fichier : Exponentiation.cs
// Auteur : votre chic prof
// Rôle : calculer et afficher le fruit d'une exponentiation
// Date : 2 septembre 2013
//
using System;

float valBase;
float valExposant;
double résultat;
// ^^^^^^^^^^^^
Console.Write("Base? ");
valBase = float.Parse(Console.ReadLine());
Console.Write("Exposant? ");
valExposant = float.Parse(Console.ReadLine());
résultat = Math.Pow(valBase, valExposant);
Console.WriteLine("{0}^{1}=={2}", valBase, valExposant, résultat);

Ce programme compile sans peine, et utilise une variable résultat dont la taille est deux fois celle qui était choisie à l'origine. Ce choix est simple, fait le travail, mais il requiert trop d'espace pour nos besoins, à moins bien sûr d'être intéressé(e)s par la quinzième décimale du calcul.

Forcer le résultat du calcul au type souhaité

Ce qui est sans doute plus près de nos intentions est de nos besoins initiaux est de faire en sorte que le double résultant du calcul fait par Math.Pow() soit « forcé » à entrer dans un float, avec les pertes de décimales que ceci implique.

Une telle conversion se nomme un transtypage (on dit souvent Cast, ou Type Cast, selon l'appellation anglaise).

Avec un transtypage, la programmeuse ou le programmeur impose ses vues au compilateur; dans notre exemple, elle ou il lui indique au compilateur que le résultat du calcul fait par la fonction Math.Pow() doit être converti en float, de telle sorte que le fruit de cette conversion puisse être affecté sans problème à la variable résultat, elle-même float.

Le code résultant devient :

Code C# (avec transtypage)
//
// Fichier : Exponentiation.cs
// Auteur : votre chic prof
// Rôle : calculer et afficher le fruit d'une exponentiation
// Date : 2 septembre 2013
//
using System;

float valBase;
float valExposant;
float résultat;
Console.Write("Base? ");
valBase = float.Parse(Console.ReadLine());
Console.Write("Exposant? ");
valExposant = float.Parse(Console.ReadLine());
résultat = (float) Math.Pow(valBase, valExposant);
//         ^^^^^^^
Console.WriteLine("{0}^{1}=={2}", valBase, valExposant, résultat);

Un transtypage permet au programme d'indiquer explicitement au compilateur le besoin de certaines conversions de types que le compilateur, de lui-même, n'aurait pas fait. Toute situation où la conversion implique, selon le langage C#, une perte d'information ou un risque implique un transtypage dans le programme.

La syntaxe est relativement simple :

Sans transtypageAvec transtypage
résultat = Math.Pow(valBase,valExposant);
résultat = (float) Math.Pow(valBase,valExposant);

De manière générale, en C#, écrire (T)expr signifie transtyper le résultat de l'expression expr dans le type T. Notez que cela ne change rien à l'expression expr en soi; seule l'utilisation qui en est faite est influencée par le transtypage en question.

Bref sur les risques du transtypage

En bref, transtyper signifie mentir. Un transtypage est un mensonge volontaire fait par le programme envers le système de types du langage, or le système de types est là pour nous protéger, pour éviter des erreurs bêtes, réduire les risques de pertes d'information, de précision, et réduire les probabilités d'opérations dangereuses pour le programme.

Conséquemment, transtypez si nécessaire, pas de manière frivole, et documentez les transtypages pour les expliquer. Les commentaires sont particulièrement à propos dans un tel cas : lorsque quiconque constatera qu'un transtypage a été fait ici ou là, les explications soutenant ce choix seront là, juste à côté.

Par exemple :

Code C# (avec transtypage)
//
// Fichier : Exponentiation.cs
// Auteur : votre chic prof
// Rôle : calculer et afficher le fruit d'une exponentiation
// Date : 2 septembre 2013
//
using System;

float valBase;
float valExposant;
float résultat;
Console.Write("Base? ");
valBase = float.Parse(Console.ReadLine());
Console.Write("Exposant? ");
valExposant = float.Parse(Console.ReadLine());
résultat = (float) Math.Pow(valBase, valExposant); // une simple précision suffit pour nos besoins
Console.WriteLine("{0}^{1}=={2}", valBase, valExposant, résultat);

Voilà. Le transtypage est un outil précieux, qui permet à la programmeuse et au programmeur d'imposer sa volonté au compilateur de manière pointue et à des moments choisis. C'est un outil auquel il faut avoir recours de manière judicieuse, et dont il ne faut pas abuser.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !