Aller au contenu

VB, C#, F#, au delà des différences syntaxiques


Messages recommandés

Salut,

 

J'avais coutume de dire que les différences entre C# et VB relevaient essentiellement de la syntaxe. Si ce n'est pas entièrement faux, je pense qu'il est important de connaitre certaines différences qui dépassent la pure syntaxe et peuvent poser problème lors de la conversion de codes d'un langage vers un autre.

Historiquement, C# est le langage implémenté spécifiquement pour .NET alors que VB.net se veut aussi être une continuation de Visual Basic (VB6 pour la dernière version). Pour pouvoir offrir à la fois la possibilité de retrouver comportements de l'ancien VB et celle d'écrire le code dans un style plus rigoureux (souvent préférable dans une environnement fortement typé) VB utilise des options compilation.

 

Conversions implicites

 

Les conversions de type implicites utilisées dans l'ancien VB (comme VBA) peuvent toujours être utilisées en VB.net ou pas suivant la valeur de l'option de compilation Strict. Si l'option Strict = Off (valeur par défaut) les conversions implicite de l'ancien VB sont acceptées, si Strict = On ces conversions devront être explicites (comme en C#).

Par exemple, avec l'option Strict = Off, la comparaison entre une chaîne et un nombre est autorisée, le nombre étant implicitement convertit en chaîne.

"12" = 3 * 4

retourne True alors qu'elle ne compilera pas avec Strict = Off (ni en C# ou F#)

Dans la même veine, les Booléens True et False peuvent être implicitement convertis en, respectivement -1 et 0 uniquement si Strict = Off.

 

Il existe quand même des conversions implicites avec l'option Strict = On comme il en existe en C#. Il est possible dans les deux cas de concaténer une chaîne avec un nombre.

VB :

"toto" & 12

C#

"toto" + 12

Retournent tous deux : "toto12"

Cette expression ne compilera pas en F# qui est beaucoup plus strict.

En résumé, avec l'option Strict = On le code VB est plus proche du code C# (et les conversions seront plus facile), mais, comme nous allons le voir, d'autres différences sont à prendre en compte.

 

Plus subtil (et aussi plus vicieux) : les nombres entiers et réels

 

C# et VB (comme LISP) convertissent un opérande de type int en double si l'autre opérande est de type double et retourne dans ce cas un résultat de type double.

F# retourne toujours un résultat du type des opérandes qui doivent être du même type.

1.5 * 2 retourne 3.0 de type double en C# et VB et provoque une erreur de compilation en F#.

1.5 * 2.0 retourne 3.0 de type double en C# et F#.

 

VB.net (à l'instar de VB6) convertit implicitement les deux opérandes entiers d'une division en nombre réels, le type du résultat est donc toujours Double.

Cette différence influe sur le résultat de l'opération avec la division.

En VB, 5 / 3 retourne 1.6666667

En C# ou F# (comme en LISP) 5 / 3 retourne 1 soit la partie entière de la division.

Pour avoir la partie entière d'une division en VB, il faut utiliser un autre opérateur (\) : 5 \ 3 retourne 1.

En C# (comme en LISP), pour que le résultat de la division de deux nombres soit un réel, il suffit qu'un des deux opérandes soit un réel :

5.0 / 3 ou 5 / 3.0 retournent 1.6666667.

Ces deux dernières expressions ne compileront pas en F# qui n'autorise aucune opération entre nombres de type différents.

5.0 / 3.0 compilera et retournera 1.6666667 dans tous les langages tout en évitant au compilateur d'avoir à faire des conversions, je conseillerais donc, comme me l'avait conseillé Alexander Rivilis, de ne pas céder à la paresse et de toujours écrire les nombres réels explicitement sous la forme de nombre réels même s'il représentent des valeurs entières.

 

Conversions entier réels

 

Comme on l'a vu, C# comme VB (quelle que soit la valeur de Strict) autorisent la conversion implicite d'entiers en réels (il n'y a pas de perte de données) alors qu'avec F# toutes les conversions doivent être explicites.

Dans l'autre sens (réel -> entier) où il peut y avoir perte de données, on peut noter quelques différences suivant les méthodes utilisées.

En VB, on utilise la méthode CInt() pour convertir un réel en entier, l'équivalent en C# et F# est Convert.ToInt32().

Ces méthodes arrondissent le nombre réel au nombre entier le plus proche :

CInt(3.6) ou Convert.ToInt32(3.6) retournent 4.

 

Mais en C# ou F# on peut aussi utiliser une conversion explicite : en C# avec l'opérateur (), en F# directement avec l'alias du type qui est considéré comme une fonction. Dans ces deux cas la valeur retournée est le nombre réel tronqué.

(int)3.6 en C# ou int 3.6 en F# retournent 3.

L'équivalent en VB nécessite d'utiliser la méthode statique Fix() avant la conversion en entier : CInt(Fix(3.6)), la fonction Int() retourne la partie entière inférieure ou égale ce qui change le résultat avec les nombre négatifs. Ces deux fonctions retournent un résultat du type de l'argument d'où la nécessité d'une conversion explicite pour obtenir un entier

 

Voilà, en espérant avoir été clair, et que ceci puisse éviter des erreurs lors de conversions de codes d'un langage vers un autre.

 

http://dl.dropbox.com/u/14851751/CsFsVB.png

Gilles Chanteau - gileCAD -
Développements sur mesure pour AutoCAD
ADSK_Expert_Elite_Icon_S_Color_Blk_125.png

Lien vers le commentaire
Partager sur d’autres sites

Coucou

 

toujours très intéressante la prose de (Giles).

 

une question me taraude et je vais tout de même la poser :

 

jusqu'où s'arrêtera-t'il ? (hihi)

 

une dernière chose, je sais que mon site n'est pas très actif

mais pourrais-tu poster ce sujet tout de même.

encore merci

 

amicalement

Lien vers le commentaire
Partager sur d’autres sites

Merci didier,

 

Je travaille actuellement à la migration de (gros) projets VBA en C# et, ayant été confronté à quelques "pièges", je pensais que partager mon expérience pourrait servir à d'autres. .NET supportant plusieurs langages on est souvent amené à traduire du code dans un sens ou dans l'autre et les traducteurs automatique ne font pas (encore) de vérifications arithmétiques.

 

La liste ci dessus n'est certainement pas exhaustive et d'autres subtiles différences existent certainement.

Gilles Chanteau - gileCAD -
Développements sur mesure pour AutoCAD
ADSK_Expert_Elite_Icon_S_Color_Blk_125.png

Lien vers le commentaire
Partager sur d’autres sites

  • 2 semaines après...

Liaison tardive vs liaison anticipée

 

Quand on déclare une variable en spécifiant son type réel (typage fort), on permet au compilateur de vérifier que les méthodes et propriétés appliquées à cette variable s'appliquent bien au type de la variable et Visual Studio peut donc aider le programmeur avec une aide contextuelle (intellisense). C'est ce qu'on appelle la liaison anticipée (early binding).

 

La liaison tardive (late binding), consiste à déclarer la variable avec le type Object pour retarder la détermination du type réel de la variable au moment de l'exécution, il n'y a donc plus, dans ce cas, de possibilité de contrôle au moment de la compilation et si la méthode ou la propriété appliquée à la variable n'est pas valable pour son type réel, cela provoquera une erreur à l'exécution.

De plus, l'utilisation de la liaison tardive n'est pas sans impact sur les performances, la détermination du type réel et un transtypage (cast) étant nécessaire à chaque exécution.

 

La liaison tardive n'est pas pour autant dénuée d'intérêt, le principal (à mon sens) étant de pouvoir utiliser des interfaces de programmation dépendantes des versions ou des plateformes comme COM Automation sans avoir à référencer de bibliothèque et rendre ainsi l'application compatible avec toutes les versions. C'est donc intéressant quand on utilise l'API COM d'AutoCAD ou Excel.

De nombreux langages de programmation utilisent la liaison tardive, renommée de façon moins péjorative : typage dynamique (LISP, Python, Ruby, etc.).

 

VB.net, toujours dans un souci de compatibilité avec VB6 (et VBA) qui faisait grand usage de la liason tardive (notamment avec les variants), permet d'utiliser implicitement la liaison tardive si l'option Stict est Off. On peut donc, pour appeler la méthode ZoomExtents de l'API COM AutoCAD, écrire directement (sans l'aide de l'intellisense bien sûr) :

Dim AutoCAD As Object = Application.AcadApplication
acad.ZoomExtents

On ne peut faire la même chose avec C# que depuis le Framework 4.0 avec le type dynamic.

dynamic AutoCAD = Application.AcadApplication;
acad.ZoomExtents;

Mais si l'intérêt de la liaison tardive est de rendre une application plus compatible en s'affranchissant des versions, il serais dommage de se lier à une version du Framework encore suffisamment nouvelle aujourd'hui pour ne pas être largement répandue.

 

Il existe une autre façon d'utiliser la liaison tardive qui ne nécessite pas la dernière version du Framework et qui fonctionne avec C#, F# (et VB quand l'option Strict est On) qui utilise l'espace de nom System.Reflection. Cette méthode consiste à déterminer le type réel de l'objet avec GetType() puis d'appeler un membre du type avec InvokeMember().

InvokeMemeber() requiert (au minimum) 5 arguments et les expressions ci-dessus s'écrivent alors :

VB

acad.GetType().InvokeMember("ZoomExtents", BindingFlags.InvokeMethod, Nothing, acad, Nothing)

C# ou F#

acad.GetType().InvokeMember("ZoomExtents", BindingFlags.InvokeMethod, null, acad, null);

Le dernier argument (ici null ou Nothing) est un tableau d'objets contenant les arguments de la méthode ou de la propriété invoquée.

Du code écrit de cette façon deviendrait rapidement imbuvable...

 

Comme on utilise principalement les BindingFlags InvokeMethod, Getproperty et SetProperty, on peut écrire trois méthodes qui permettront d'écrire le code de manière plus lisible (les Lispeurs reconnaitront les fonctions vlax-get, vlax-put et vlax-invoke).

L'utilisation de param (C#) ou ParamArray (VB) permet d'éviter de construire un tableau pour les paramètres. F# n'a pas d'équivalent, mais la construction d'un tableau y est beaucoup plus aisée.

C#

public object GetProperty(object obj, string propName, params object[] parameter)
{
   return obj.GetType().InvokeMember(propName, BindingFlags.GetProperty, null, obj, parameter);
}

public void SetProperty(object obj, string propName, params object[] parameter)
{
   obj.GetType().InvokeMember(propName, BindingFlags.SetProperty, null, obj, parameter);
}

public object Invoke(object obj, string methName, params object[] parameter)
{
   return obj.GetType().InvokeMember(methName, BindingFlags.InvokeMethod, null, obj, parameter);
}

 

F#

let GetProperty (obj, prop, param) =
   obj.GetType().InvokeMember(prop, BindingFlags.GetProperty, null, obj, param)

let SetProperty (obj, prop, param) =
   obj.GetType().InvokeMember(prop, BindingFlags.SetProperty, null, obj, param) |> ignore

let Invoke (obj, meth, param) =
   obj.GetType().InvokeMember(meth, BindingFlags.InvokeMethod, null, obj, param)

 

VB (option Strict = On)

Public Function GetProperty(ByVal obj As Object, ByVal propName As String, ByVal ParamArray parameter As Object()) As Object
   Return obj.GetType().InvokeMember(propName, BindingFlags.GetProperty, Nothing, obj, parameter)
End Function

Public Sub SetProperty(ByVal obj As Object, ByVal propName As String, ByVal ParamArray parameter As Object())
   obj.GetType().InvokeMember(propName, BindingFlags.SetProperty, Nothing, obj, parameter)
End Sub

Public Function Invoke(ByVal obj As Object, ByVal methName As String, ByVal ParamArray parameter As Object()) As Object
   Return obj.GetType().InvokeMember(methName, BindingFlags.InvokeMethod, Nothing, obj, parameter)
End Function

 

On peut maintenant écrire, pour changer la taille du curseur, par exemple :

C#

object pref = Application.Preferences;
object disp = GetProperty(pref, "Display");
SetProperty(disp, "CursorSize", 100);

F#

let pref = Application.Preferences
let disp = GetProperty(pref, "Display", null)
SetProperty(disp, "CursorSize", [| 100 |])

VB

Dim pref As Object = Application.Preferences
Dim disp As Object = GetProperty(pref, "Display")
SetProperty(disp, "CursorSize", 100)

ou, de manière plus concise en C# ou VB (sans le point virgule)

SetProperty(GetProperty(Application.Preferences, "Display"), "CursorSize", 100);

F#

SetProperty(GetProperty(Application.Preferences, "Display", null), "CursorSize", [| 100 |])

Les imbrications et parenthèses dans la dernière expression ne sont pas sans rappeler la syntaxe du LISP.

Nous verrons bientôt qu'il est possible de faire mieux (du moins en C# et F#)...

Gilles Chanteau - gileCAD -
Développements sur mesure pour AutoCAD
ADSK_Expert_Elite_Icon_S_Color_Blk_125.png

Lien vers le commentaire
Partager sur d’autres sites

Les méthodes d'extension

 

Les méthodes d'extension permettent d'ajouter des fonctions à des classes dont on ne dispose pas du code et/ou dont on ne peut hériter. Ces méthodes peuvent être appelées comme des méthodes d'instance sur les objets de la classe étendue.

Pour définir des méthodes d'extension, en C# il faut créer une classe static dans laquelle on implémente des méthodes elles aussi qualifiées static dont le premier argument doit être un objet de classe à étendre précédé du mot clé this.

Avec VB, les méthodes doivent être définies dans un modules et précédées de l'attribut : System.Runtime.CompilerServices.Extension.

Dans les deux cas, le premier argument des méthode doit être du type de classe étendue.

Avec F#, il est tout simplement possible d'étendre n'importe quelle classe existante pour lui ajouter, non seulement des méthodes, mais aussi des propriétés.

 

Un exemple avec une méthode (propriété en F#) EffectiveName qui retourne le nom effectif d'une référence de bloc dynamique ou non (comme la propriété COM). Pour ne pas alourdir les codes, les méthodes sont prévues pour être appelée dans une transaction.

C#

public static class Extension
{
   public static string GetEffectiveName(this BlockReference br)
   {
       return br.IsDynamicBlock ?
           (BlockTableRecord)br.DynamicBlockTableRecord.GetObject(OpenMode.ForRead)).Name :
           br.Name;
   }
}

VB

Module Extensions
   <System.Runtime.CompilerServices.Extension()> _
   Public Function EffectiveName(ByVal br As BlockReference) As String
       Return CStr(IIf(br.IsDynamicBlock, _
                        DirectCast(br.DynamicBlockTableRecord.GetObject(OpenMode.ForRead), BlockTableRecord).Name, _
                        br.Name))
   End Function
End Module

F#

type BlockReference with
   member br.EffectiveName = 
       match br.IsDynamicBlock with
       | false -> br.Name
       | _ -> (br.DynamicBlockTableRecord.GetObject(OpenMode.ForRead) :?> BlockTableRecord).Name

 

Une fois ces méthodes définies, on peut appeler EffectiveName directement sur une instance de BlockReference (blk, par exemple) :

blk.Effectivename()

Sans les parenthèses en F#EffectiveName a été définie comme une propriété étendue.

D'autres exemples de méthodes d'extension (en C# uniquement) ici.

 

Mais revenons au sujet abordé précédemment, à savoir essayer de rendre la syntaxe de la liaison tardive plus digeste.

Avec VB, il n'est pas possible d'utiliser les méthodes d'extension avec le type Object (à cause des risques conflits avec la liaison tardive implicite) mais on a vu qu'avec l'option Strict = Off on peut écrire le code comme en liaison anticipée.

Avec C#, on reprend les méthodes définies dans le sujet précédent en les qualifiant static dans une classe static elle aussi et on ajoute this devant le premier argument (de type object).

 

public static class LateBinding
{
   public static object Get(this object obj, string propName, params object[] parameter)
   {
       return obj.GetType().InvokeMember(propName, BindingFlags.GetProperty, null, obj, parameter);
   }

   public static void Set(this object obj, string propName, params object[] parameter)
   {
       obj.GetType().InvokeMember(propName, BindingFlags.SetProperty, null, obj, parameter);
   }

   public static object Invoke(this object obj, string methName, params object[] parameter)
   {
       return obj.GetType().InvokeMember(methName, BindingFlags.InvokeMethod, null, obj, parameter);
   }
}

 

Avec F#, on pourrait aussi étendre le type object, mais on préfèrera probablement utiliser les fonctions définies dans les sujet précédent en changent l'ordre des arguments et sans les grouper dans un tuple pour pouvoir utiliser l'opérateur de pipeline (|>).

 

let get prop param obj =
   obj.GetType().InvokeMember(prop, BindingFlags.GetProperty, null, obj, param)

let set prop param obj =
   obj.GetType().InvokeMember(prop, BindingFlags.SetProperty, null, obj, param) |> ignore

let invoke meth param obj =
   obj.GetType().InvokeMember(meth, BindingFlags.InvokeMethod, null, obj, param)

 

L'expression utilisée précédemment pour mettre la taille du curseur à 100, s'écrit toujours, en VB avec Strict = On :

SetProperty(GetProperty(Application.Preferences, "Display"), "CursorSize", 100)

mais peut s'écrire en C# :

Application.Preferences.Get("Display").Set("CursorSize", 100);

et en F# :

Application.Preferences |> get "Display" [||] |> set "CursorSize" [| 100 |]

 

Pour terminer cette comparaison, une petite commande qui bascule la taille du curseur entre 100 et 5.

C# avec les méthodes d'extension définies ci-dessus

[CommandMethod("Test")]
public void Test()
{
   object disp = Application.Preferences.Get("Display");
   if ((int)disp.Get("CursorSize") == 100)
       disp.Set("CursorSize", 5);
   else
       disp.Set("CursorSize", 100);
}

VB (Strict=On) avec les méthodes définies dans le sujet précédent

<CommandMethod("Test")> _
Public Sub Test()
   Dim disp As Object = GetProperty(Application.Preferences, "Display")
   If CInt(GetProperty(disp, "CursorSize")) = 100 Then
       SetProperty(disp, "CursorSize", 5)
   Else
       SetProperty(disp, "CursorSize", 100)
   End If
End Sub

VB (option Strict=Off)

<CommandMethod("Test")> _
Public Sub Test()
   Dim disp As Object = Application.Preferences.Display
   If disp.CursorSize = 100 Then
       disp.CursorSize = 5
   Else
   disp.CursorSize = 100
   End If
End Sub

F# avec les fonctions définies ci dessus et en privilégiant le style fonctionnel (composition de fonctions, expression lambda et pipelining)

[<CommandMethod("Test")>]
let test() =
   let toggle = get "CursorSize" [||] >> unbox >> function 100 -> 5 | _ -> 100
   let resize disp = disp |> set "CursorSize" [| disp |> toggle |]
   Application.Preferences |> get "Display" [||] |> resize

Gilles Chanteau - gileCAD -
Développements sur mesure pour AutoCAD
ADSK_Expert_Elite_Icon_S_Color_Blk_125.png

Lien vers le commentaire
Partager sur d’autres sites

Créer un compte ou se connecter pour commenter

Vous devez être membre afin de pouvoir déposer un commentaire

Créer un compte

Créez un compte sur notre communauté. C’est facile !

Créer un nouveau compte

Se connecter

Vous avez déjà un compte ? Connectez-vous ici.

Connectez-vous maintenant
×
×
  • Créer...

Information importante

Nous avons placé des cookies sur votre appareil pour aider à améliorer ce site. Vous pouvez choisir d’ajuster vos paramètres de cookie, sinon nous supposerons que vous êtes d’accord pour continuer. Politique de confidentialité