Astuces DotNet (Sébastien Courtois)

05/10/2010

[XNA 4] Tutoriel 3D 1 : Hello World 3D

Filed under: .NET, 3D, C# 4, Débutant, Intermediaire, XBOX 360, XNA — Étiquettes : , , , , , , , , — sebastiencourtois @ 16:31

Après une série de tutoriels sur la programmation 2D en XNA, nous allons passer à une partie plus intéressante et surtout plus complexe … la 3D. Qui n’a pas rêvé de recréer soi-même un Crysis ou encore un Gran Tourismo.

new-crysis-dx10-screenshot-20061110001326035 gran_turismo_5
Copie d’écran de Crysis Copie d’écran de Gran Tourismo 5

Il faut bien se rendre compte que ces jeux, magnifique graphiquement, ont été réalisés par des équipes composés de dizaines de programmeurs, artistes 2D/3D  qui ont travaillé pendant des mois pour arriver à un tel réalisme. Même si être ambitieux/optimiste  aide dans ce métier, il faut se rendre à l’évidence que vous ne pourrez pas seul et avant de nombreuses années arriver à un tel résultat.

Toutefois, si cela peut vous consoler, nous allons utiliser des outils/concepts proches de ce que les développeurs de ces jeux utilisent et nous seront confrontés (à notre niveau) aux mêmes types de problèmes inhérent à la création de jeu vidéo.

  • Un peu de vocabulaire/concept

Le monde de la 3d est un monde qui parait assez obscur et mathématiques avec son propre vocabulaire et ses propres concepts. Toutefois, ceux-ci peuvent être rattachés à des éléments du quotidien (on vit dans un univers en 3d, non :)).

Un Vertex (pluriel : vertices) : Un vertex est un point dans un univers 3d. C’est l’élément de base dans la construction de formes 3D primitives (lignes, triangle). Il est généralement décrit par sa position dans l’univers 3d ainsi que sa couleur, normals et d’autres informations nécessaires au rendu 3d que nous verrons plus tard.

Un ligne : Primitive 3D composé de deux vertices

Un triangle : Primitive 3d composé de 3 vertices. C’est la primitive la plus évolué qu’une carte graphique peut dessiner. Les modèles 3d que vous pouvez voir dans les jeux vidéo ne sont généralement qu’une accumulation de triangles mis cote à cote.

eames_wireframe Remarque : Les cartes graphiques peuvent aussi dessiner des primitives à 4 vertices mais cette fonctionnalité n’est pas disponible dans XNA 4.

Graphics Device : Ce terme représente l’accès à la carte graphique. Toute les opérations graphiques passeront par des appels au graphics Device (notamment la partie de rendu 3d).

  • Premier projet 3D en XNA

Dans ce tutoriel, nous allons créer un triangle.

Pour commencer, il faut créer un nouveau projet XNA (Windows Game). XNA nous crée déjà le graphics Device nécessaire à la communication avec la carte graphique ainsi que les outils pour la gestion du contenu (ContentManager).

Pour une scène 3d, il est nécessaire d’avoir au minimum 2 choses :

    • Un modèle 3D

Dans notre exemple, notre modèle 3D va être relativement simple car il s’agira d’un triangle. Pour cela, il nous suffit de créer un tableau de 3 vertices. Chaque vertex aura une position et une couleur. Dans notre classe Game, nous allons créer un tableau pour contenir les vertices ainsi qu’une méthode créant les données du triangle.

VertexPositionColor[] vertices;
//Création du triangle
private void CreateTriangle()
{
   this.vertices = new VertexPositionColor[]
   {
       new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),
       new VertexPositionColor( new Vector3(0,1,0), Color.Green),
       new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
   };
}

Pour contenir les données d’un vertex, XNA fournit une série de classe de base permettant de stocker correctement ces données. Dans notre cas, nous souhaitons définir seulement la position et la couleur des points. Nous utiliserons donc la structure VertexPositionColor prenant en paramètre une position dans un univers 3D (sous la forme d’une structure Vector3) et une couleur. Nous stockons le tout dans un tableau défini au préalable dans notre classe Game.

Remarque : D’autres types de structures de stockage de vertices existent dans XNA (VertexPositionColor / VertexPositionColorTexture / VertexPositionNormalTexture / VertexPositionTexture). Vous pouvez créer votre propre classe en implémentant l’interface IVertexType. Nous verrons dans un  prochain tutoriel la création de notre propre structure de stockage.

    • Une visualisation (ou caméra)

Une fois le modèle 3D défini, il est nécessaire d’indiquer comment on souhaite le voir. Pour cela, il est nécessaire de fournir la position de l’observateur, l’endroit qu’il regarde et d’autres informations comme l’angle de vision. Ces informations seront ensuite stockées sous forme de matrices mathématiques qui serviront à la carte graphique pour le rendu 3D.

Matrix View;
Matrix Projection;
Matrix World;

Vector3 CameraPosition = new Vector3(0.0f, 0.0f, 5.0f);
Vector3 CameraTarget = Vector3.Zero;
Vector3 CameraUp = Vector3.Up;

//Création de la caméra
private void CreateCamera()
{
    View = Matrix.CreateLookAt(CameraPosition, CameraTarget, CameraUp);
    Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, this.GraphicsDevice.Viewport.Width / this.GraphicsDevice.Viewport.Height, 0.01f, 1000.0f);
    World = Matrix.Identity;
}

La première matrice est la matrice View (dite de “Visualisation”). Elle sert à positionner la caméra et la façon dont celle-ci regarde la scène. On utilise une méthode d’aide fourni par XNA (CreateLookAt) qui prend en premier paramètre la position de l’utilisateur dans l’univers 3D (sous la forme d’un Vector3). Le paramètre suivant est l’endroit que regarde l’utilisateur. Le troisième paramètre indique “l’orientation” de la caméra. Dans notre exemple, nous souhaitons que le dessus de la camera soit orienté vers le haut (coordonnées {0,1,0} ce qui équivaut au Vector3.Up).

La deuxième matrice est la matrice de projection. Celle-ci indique les caractéristique de la caméra (lentille, distance focale). Une méthode d’aide XNA permet de créer cette matrice facilement (CreatePerspectiveFieldOfView). Le premier paramètre est l’angle d’ouverture (ou angle de vision) de la caméra. Par exemple, un humain a un angle de vision allant de 1° (angle d’attention) jusqu’à 180 ° (angle de perception).Cela veut dire qu’il peut, sans bouger la tête voir tout ce qui l’entoure à 180 °. Arbitrairement, nous allons décider d’utiliser un angle de vision de 45° ou PI/4 (car les angles sont décrits en radians en XNA). Le paramètre suivant est le ratio d’aspect. Cela indique le format de la caméra (16/9 comme au cinéma, 4/3). Cela correspond tout simplement à la largeur de la zone visible divisé par la hauteur. Les deux derniers paramètres correspondent à la distance minimale et maximale d’affichage. Ainsi tous les objets trop proche ou trop loin de la caméra ne seront pas affichés.

La troisième matrice nous permettra de placer les objets dans l’univers 3d. Pour l’instant, nous lui assignons la valeur de base (matrice identité).

    • Méthode d’initialisation

Nous avons maintenant tous les éléments nécessaire pour réaliser un scène 3d. Nous allons maintenant appelé les deux méthodes décrites plus haut dans la méthode Initialize().

protected override void Initialize()
{
   this.CreateTriangle();
   this.CreateCamera();
   base.Initialize();
}
  • Affichage d’un triangle

Afin de réaliser l’affichage, nous allons utiliser un shader. Un shader est un programme créé spécifiquement pour être exécuté sur la carte graphique. En effet, la carte graphique permet la réalisation de calcul mathématique très rapidement. La création de shader sera l’objet de plusieurs tutoriels mais, dans ce premier article sur la 3d, nous allons utilisé celui fournit par XNA : BasicEffect.

BasicEffect effect;

protected override void LoadContent()
{
  spriteBatch = new SpriteBatch(GraphicsDevice);
  this.effect = new BasicEffect(this.GraphicsDevice);
}

BasicEffect se crée en fournissant uniquement le graphicsDevice et il sera utilisé lors de la partie affichage (méthode Draw).

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World;
    effect.VertexColorEnabled = true;

    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
    }

    base.Draw(gameTime);
}

Première étape dans la méthode Draw. Nettoyer l’écran avec une couleur de fond de la même façon que pour la 2D. On fournit ensuite les trois matrices de projections au shader BasicEffect. On active aussi le booléen VertexColorEnabled pour indiquer que l’on souhaite que les couleurs définis sur les vertices soient prises en compte. On utilise ensuite la propriété CurrentTechnique pour utiliser les différentes passes du shaders (le fonctionnement exacte des shaders sera expliqué dans un autre tutoriel et sa compréhension n’est pas indispensable pour celui-ci).

La méthode la plus importante de ce morceau de code est le DrawUserPrimitivers qui va prendre en paramètre le type de primitive. Il s’agit de la façon dont nous allons associer les vertices.

Initialising_Article_triangles

Dans notre exemple, nous utiliserons TriangleList. Cette primitive prend 3 vertex pour en faire un triangle puis les 3 vertex suivants pour un autre triangle et ainsi de suite.

Le deuxième paramètre de DrawUserPrimitives est le tableau de vertices proprement dit. Le paramètre suivant est l’index de départ du tableau de vertices. Dans notre cas, on souhaite utiliser tous les vertices donc on indique que le premier vertex est en position 0. Le dernier paramètre indiquent le nombre de primitives (ici triangle) que l’on souhaite dessiner. On prend le nombre total de vertices de notre tableau (3) et on divise par le nombre de vertex nécessaire pour faire un triangle (3). On indique que l’on souhaite dessiner un seul triangle.

Si vous lancer le projet, vous devriez obtenir l’écran suivant :

xna_tuto3d1_1

On remarque que le fond est bleu ce qui correspond à la couleur d’effacement au début de notre méthode Draw (Clear(Color.CornFlowerBlue)). Le triangle est visible mais on remarque que les couleurs, correctes aux sommets, fusionnent entre elles en s’écartant des sommets. Il s’agit du comportement de base de DirectX. Si l’on souhaite faire un triangle uni, il faut que les sommets aient la même couleur.

  • Un peu de mouvement

Nous allons introduire une rotation sur ce triangle afin de prouver que nous sommes bien dans un univers 3D. Pour cela, on va rajouter une variable de rotation que nous allons incrémenter dans la méthode Update.

float RotateZ = 0.0f;
protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
        this.Exit();

    RotateZ += MathHelper.ToRadians(2.0f);

    base.Update(gameTime);
}

Nous incrémentons donc la variable de 2° par appel à la méthode Update (60 x par secondes par défaut sur XNA). Il est à noter que la valeur est toujours à stocker en Radians. MathHelper fournit des méthodes d’aides pour traduire les angles de degré en radians et inversement.

Une fois la variable modifié, il suffit de modifier la matrice World qui est chargé de placer le triangle dans l’espace.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    effect.View = View;
    effect.Projection = Projection;
    effect.World = World * Matrix.CreateRotationY(RotateZ);
    effect.VertexColorEnabled = true;

    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
    {
        pass.Apply();
        this.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
    }

    base.Draw(gameTime);
}

Il suffit de multiplier la matrice World avec une matrice de rotation créé à partir de l’angle RotateZ pour réaliser cette effet. Si vous relancer l’application le triangle tourne…

  • Mon triangle disparait puis réapparait sans arrêt.

Lorsque vous lancez l’application le triangle commence à tourner mais il disparait un moment au bout d’un demi-tour puis réapparait. Cela est dû à une optimisation de la carte graphique nommé Backface Culling. En effet, les cartes graphiques n’affiche pas les faces (triangles) ne leur faisant pas face. Vous trouverez une explication plus complète sur le fonctionnement  ici. Il est possible de désactiver cette optimisation en modifiant la propriété RasterisationState du graphicsDevice. Cela se réalise généralement dans la méthode Initialize().

this.GraphicsDevice.RasterizerState = new RasterizerState()
{
   CullMode = CullMode.None,
   FillMode = FillMode.Solid
};

Remarque : Vous pouvez aussi régler par la propriété FillMode si vous souhaitez une vision des objets “solides” ou en fil de fer.

Vous pouvez remarquer que CullMode à 3 valeurs possibles : CullClockwiseFace ,CullCounterClockwiseFace, None. Par défaut, le CullMode est à CullClockwise ce qui veut dire que les vertex doivent être définis dans l’ordre des aiguilles d’une montre pour que la face soit visible.

TriangleAll
Représentation d’un triangle dont les vertices sont dans le sens des aiguilles d’une montre (tiré d’un tutoriel anglais XNA : http://www.toymaker.info/Games/XNA/html/xna_simple_triangle.html).

Lorsque le triangle a fait son premier demi-tour, les vertex passent dans le sens inverses. N’étant plus dans le sens des aiguilles d’une montre, le triangle n’est plus affiché.

Une autre solution, qui permet de conserver le Backface Culling, serait de créer un deuxième triangle avec les vertices dans l’ordre inverse. Ainsi, si on modifie le tableau de vertices comme suit  :

//Création du triangle
private void CreateTriangle()
{
    this.vertices = new VertexPositionColor[]
    {
        //Triangle 1
        new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),
        new VertexPositionColor( new Vector3(0,1,0), Color.Green),
        new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
        //Triangle 2
        new VertexPositionColor( new Vector3(1,-1,0), Color.Blue),
        new VertexPositionColor( new Vector3(0,1,0), Color.Green),
        new VertexPositionColor( new Vector3(-1,-1,0), Color.Red),

    };
}

On obtient un triangle (en fait deux triangles qui s’interchange) qui tourne correctement.

  • Conclusion

Après ce tutoriel un peu long et complexe, je pense que vous aurez compris que la route est longue avant de pouvoir faire une jeu aussi techniquement abouti que les jeux présentés plus haut. Vous savez maintenant faire un triangle ce qui est la base de toutes les applications 3D. Dans les prochains tutoriels, nous verrons comment afficher un cube, le texturer puis se déplacer dans un univers 3d. Nous verrons aussi quelques astuces pour optimiser l’affichage.

N’hésitez pas à donner vos commentaires et poser vos questions sur ce tutoriel.

Lien vers la solution

Publicités

14/09/2010

[XNA 4] Tutoriel 1 : Création et structure d’un projet XNA

Filed under: .NET, C# 4, Débutant, Jeux vidéos, XBOX 360, XNA — Étiquettes : , , , , , , , — sebastiencourtois @ 13:57

En l’honneur de la sortie de XNA 4, j’ai décidé de réaliser une série de posts sur le sujet. J’essaierais d’être le plus général possible afin que l’ensemble des codes fonctionnent sur les plateformes cibles de XNA 4 : PC, XBOX 360, Zune et le nouveau Windows Phone. Lorsque cela ne sera pas le cas, cela sera indiqué dans le titre ou dans les premières lignes des posts.

Ces tutoriels seront à destination des débutants en programmation 3D.Toutefois, il est nécessaire d’avoir des bonnes notions de C#.

  • Commençons par le début : XNA ?

D’après les forums/FAQ, XNA voudrait dire : “XNA’s Not Acronymed”. Vu comme ça on n’est pas plus avancé (un peu comme le GNU is Not Unix chez les adorateurs de pingouins) :).

XNA est une plateforme (Framework) de développement de jeu vidéo 2D/3D à destination du PC, de la XBOX 360 et d’appareils portables (Zune/Windows Phone pour l’instant). Cette plateforme est en fait une surcouche d’une API de développement de jeu vidéo utilisés dans la plupart des jeux vidéo commerciaux actuels : DirectX.

XNA a été créé pour :

  • Fournir un environnement de développement simplifié pour réaliser des applications graphique 2D/3D rapidement sans trop se préoccuper des couches plus bases (notamment Win32/DirectX qui sont assez indigeste).

  • Fournir un environnement entièrement managé pour la création de jeu vidéo (DirectX étant uniquement axé C++). Un projet de DirectX managé a été lancé par Microsoft, il y a quelque années, mais il fut arrêté au profit de XNA. Un projet communautaire a été relancé sur le même principe : SlimDX. L’idée est de fournir un wrapper directement sur les méthodes DirectX afin de permettre l’utilisation de fonctionnalités non disponibles dans XNA.

  • Permettre de déployer l’application sur plusieurs plateformes différentes (PC,XBOX 360,mobiles) sans avoir à (trop) changer le code.

Pour atteindre ces objectifs, XNA a limité les fonctionnalités disponibles. En effet, XNA ne permet d’utiliser que DirectX 9 afin d’être compatible avec l’ensemble des plateformes cibles (Il faut savoir que DIrectX9 est sorti en Août 2005  et nous sommes actuellement à DirectX 11sorti en Octobre 2009). De plus, XNA étant une plateforme managée, le garbage collector et les mécanismes internes de .NET seront activés et pourront ralentir l’application (comparé à une application DX C++). Toutefois, le niveau des applications XNA restent correcte pour la création de jeu (voir les démos présentées sur le concours Imagine Cup par des étudiants : http://www.xna-connection.com/post/Imagine-Cup-2010-Les-finalistes-section-Game-Design).

  • Fini les discours commerciaux, passons à la pratique :

Afin de développer sur XNA 4, il est nécessaire d’installer :

Une fois installé, lancez Visual Studio, puis aller dans Fichier>Nouveau Projet. Dans l’écran “Nouveau projet”, allez sur Visual Studio > XNA Game Studio 4 puis choisissez “Windows Game (4.0)”. N’oubliez pas de choisir un nom de projet (“Tutorial2D” dans ce post) puis cliquez sur OK.

xnatuto1

 

 

 

 

 

 

 

 

 

 

 

Une fois créé, vous devriez avoir l’arborescence suivante :

xnatuto2

Le projet Tutorial2D est le projet principal contenant le point d’entrée ainsi que toute les références de votre application. Il est composé de 4 fichiers :

  • Game.ico : Icone du jeu (visible dans le coin en haut à gauche de la fenêtre)
  • Game1.cs : Code du jeu (voir suite du tutorial)
  • GameThumbnail.png : Vignette représentant le jeu (visible dans le menu XBOX 360)
  • Program.cs : Point d’entrée du programme.

Le projet Tutorial2DContent est un projet dit “de contenu”. Il accueillera uniquement les ressources (textures, modèles 3D, sons…) qui seront utilisées dans le jeu.

  • Analysons le point d’entrée du jeu : Program.cs
using System;

namespace Tutorial2D
{
#if WINDOWS || XBOX
    static class Program
    {
        static void Main(string[] args)
        {
            using (Game1 game = new Game1())
            {
                game.Run();
            }
        }
    }
#endif
}

Il s’agit d’un point d’entrée classique. Le .NET lance l’application en appelant Program.Main. On crée une instance de Game1 et on appelle “le point d’entrée” du jeu. Le #if Windows || XBOX sont des commandes de préprocesseurs pour indiquer que le code qui encadré est uniquement pour PC/XBOX. Pour une application mobile, le code est différent et fera l’objet d’un prochain post.

  • Déroulement d’un programme XNA

Une fois la méthode Game1.Run() appelé dans Program.cs, XNA va rentrer dans un processus défini comme suit :

xnatuto3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • Initialize() : Zone d’initialisation du jeu. C’est ici que se trouve le chargement et la configuration des modules non graphiques.
  • LoadContent() : Zone de chargement des données du jeu (textures, effets(shaders), modèles 3D…).
  • On rentre ensuite dans une boucle infini (la boucle de jeu). A intervalle régulier (60 fois par secondes au maximum par défaut), les deux méthodes suivantes vont être appelées dans cet ordre :
    • Update() : Mise à jour des données du jeu. Gestion du clavier/souris/gamepad, déplacement des joueurs, moteur physique …
    • Draw() : Affichage de l’ensemble des données graphiques. C’est ici que l’on indique à la carte graphique ce que l’on souhaite afficher à l’écran.
  • UnloadContent() : Lorsque l’on sort de cette boucle infini (En appelant la méthode Exit()), la méthode UnloadContent() est appelée afin de libérer la mémoire des objets dont on n’a plus besoin.

 

  • Game1.cs

L’ensemble des méthodes décrites ci-dessus se retrouvent dans la classe Microsoft.Xna.Framework.Game dont dérive Game1. Il est donc nécessaire de les surcharger afin de pouvoir créer son propre code.

public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
            

        base.Initialize();
    }

    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
    }

    protected override void UnloadContent()
    {

    }

    protected override void Update(GameTime gameTime)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            this.Exit();


        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);


        base.Draw(gameTime);
    }
}

Il est à noter que des appels aux classes parents sont réalisés à la fin de Initialize(), Update() et Draw(). Ces appels sont nécessaires au bon fonctionnement de l’application et doivent toujours se trouver à la fin des méthodes concernés. Par exemple, si vous commentez base.Initialize(), XNA ne fera pas l’appel à LoadContent() mais lancera la boucle de jeu directement (Update <=> Draw).

Deux propriétés sont utilisées dans cette classe :

 GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
  • Gestionnaire de carte graphique. Cette propriété permet la configuration de la carte graphiques ainsi que l’accès à celle-ci.

     

  • Cette propriété permet l’affichage de texte et de texture à l’écran.
  • Au sein de la méthode Update(), on remarque l’utilisation du GamePad.

      if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

    Ainsi, si l’on clique sur le bouton “back” (petit bouton blanc à gauche du bouton central), on sort de la boucle de jeu. La gestion des entrées utilisateurs est une des tâches à effectuer dans la méthode Update().

    Nous étudierons l’utilisation de l’ensemble de ces outils en détail dans les prochains posts.

    • Lancement de son premier jeu XNA

    Lorsque l’on lance le projet, on obtient un écran avec un fond bleu.

    xnatuto4

    Cela provient du code de la méthode Draw(). Lors de la première ligne, on demande à la carte graphique (GraphicsDevice) de vider la fenêtre et de mettre une couleur de fond CornFlowerBlue. (si vous souhaitez savoir pourquoi cette couleur en particulier est présente par défaut lorsque l’on crée un projet XNA, je vous conseille la lecture de ce post).

    • C’est déja fini ? non, ce n’est que le début.

    Maintenant que vous avez les bases sur le fonctionnement de XNA (désolé pas de code :(), les prochains posts seront dédiés à la création d’un jeu 2d multiplateforme (PC/XBOX360,Windows Phone). J’ai dans l’idée de faire un petit Space Invader mais si vous avez d’autres idées, n’hésitez pas à poster en commentaires.

    La suite de la séries XNA : Tutoriel 2 : Gestion des images

    Pour ceux qui veulent déjà faire de la 3D, des tutoriaux arriveront très bientôt. En attendant vous pouvez toujours aller sur les liens XNA suivants :

    • Français :

    http://msmvps.com/blogs/valentin/

    http://www.xna-connection.com/

    http://xna-france.com/

    http://www.xnainfo.com/

    http://msdn.microsoft.com/fr-fr/directx/default.aspx

    • Anglais :

    http://creators.xna.com/fr-FR/

    http://blogs.msdn.com/b/shawnhar/

    http://xna-uk.net/

    09/06/2010

    [.NET 3.5] Attention lors de l’utilisation des lambdas/closures

    Filed under: .NET, C#, C# 4, Hors Catégorie, Intermediaire — Étiquettes : , , , , — sebastiencourtois @ 13:05

    Afin de commencer cette article, commençons par une petite devinette.

    Quel sont les résultats de ces deux morceaux de code :

    var actions = new List<Action>();
    
    for (int i = 0; i < 10; i++)
        actions.Add(() => Console.WriteLine(i));
    
    foreach (var action in actions)
        action();
    var actions = new List<Action>();
    
    for (int i = 0; i < 10; i++)
    {
        int j = i;
        actions.Add(() => Console.WriteLine(j));
    }
    
    foreach (var action in actions)
        action();

    La première réponse que vous avez du vous faire est que ces deux codes ont le même résultat : afficher les nombres de 0 à 9. En effet, on met des méthodes délégués dans une liste de délégués puis on parcourt cette liste afin d’exécuter le code de ces délégués. La seule différence entre les deux codes est l’utilisation d ‘une variable local j.

    Et pourtant la réponse est :

    • Exemple 1 : 10 10 10 10 10 10 10 10 10 10
    • Exemple 2 : 0 1 2 3 4 5 6 7 8 9

    Autre exemple dans le même genre :

    var actions = new List<Action>();
    string[] urls = 
    { 
       "http://www.url.com", 
       "http://www.someurl.com", 
       "http://www.someotherurl.com", 
       "http://www.yetanotherurl.com" 
    };
    
    for (int i = 0; i < urls.Length; i++)
    {
        actions.Add(() => Console.WriteLine(urls[i]));
    }
    var actions = new List<Action>();
    string[] urls = 
    { 
       "http://www.url.com", 
       "http://www.someurl.com", 
       "http://www.someotherurl.com", 
       "http://www.yetanotherurl.com" 
    };
    
    for (int i = 0; i < urls.Length; i++)
    {
        int j = i;
        actions.Add(() => Console.WriteLine(urls[j]));
    }
    Résultat : IndexOutOfRangeException (Index was outside the

    bounds of the array
    Résultat : les liens du tableaux sont affichés
    • Les lambdas peuvent être nuire gravement à la santé … mentale des développeurs 

    Etrange non ? Décompilons le tout première exemple afin devoir ce qui est effectivement exécuté.

    Note : Pour une raison inconnue, je n’ai pas réussi à obtenir ceci depuis Reflector. J’ai donc récupérer le code de l’article original.

    [CompilerGenerated]
    private sealed class <>c__DisplayClass2
    {
        // Fields
        public int i;
    
        // Methods
        public void <Main>b__0()
        {
            Console.WriteLine(this.i);
        }
    }
    [...]
    var actions = new List<Action>();
    
    <>c__DisplayClass2 localFrame = new <>c__DisplayClass2();
    for (localFrame.i = 0; localFrame.i < 10; localFrame.i++)
    {
    	actions.Add(localFrame.<Main>b__0);
    }
    
    foreach (var action in actions)
    {
    	action();
    }

    Comme on peut le voir, le fait de créer une lambda en utilisant une variable extérieure à celle ci à entrainer la génération d’une classe (<>c__DisplayClass2") qui est utilisé pour stocker la valeur de cette variable (dans la varaible i) et une méthode (<Main>b__0) qui contient le code de la lambda.  Si on regarde le code du premier for, on s’aperçoit que cette boucle incrémente la variable i de la classe DisplayClass2. Donc, à la fin de la première boucle, la classe DisplayClass2 contient une variable i = 10 et la liste actions contient une référence vers la méthode de cette classe. Ainsi lors de la boucle d’appel actions, on se retrouve a appeler le code Console.WriteLine(i) <=> Console.WriteLine(10) !!!!

    Si on résonne de la même façon sur l’exemple 2, on obtient un appel Console.WriteLine(urls[4]) ==> Exception.

    • Pourquoi l’utilisation d’une variable locale résout le problème

    Si on regarde le code ci dessous, on s’aperçoit que le problème vient du fait que la classe DisplayClass2 est créé en dehors de la boucle for et que celle ci utilise la variable i comme compteur. Le fait d’utiliser une variable temporaire à l’intérieur de la boucle for, oblige le compilateur à créer plusieurs instance de DisplayClass (une par itération). On obtient ainsi quelque chose comme le code suivant.

    for (int idx = 0; idx < 10; idx++)

    {

        <>c__DisplayClass2 localFrame = new <>c__DisplayClass2();

        localFrame.i = idx;

        actions.Add(localFrame.<Main>b__0);

    }

    Remarque : On pourrait penser que chaque instance est dispose lors de la sortie du scope. Cela n’’est pas vrai car la liste actiosn conserve une référence sur l’objet (en l’occurence sur une de ses méthodes) donc le garbage collector ne prend pas cet objet.

    • Conclusion

    Les lambdas sont aujourd’hui utilisé dans de nombreuses technologies .NET (LINQ,PFX…) et sont une bonne alternative aux délégués classiques. Toutefois, on peut voir que cela peut engendré une certaine confusion voire des bugs lorsqu’on les utilise.

    Je vous mets le lien vers l’article dont je me suis fortement inspiré pour cet article : Lambdas – Know your closures

    09/05/2010

    [Nouveautés C# .NET 4] Code Contracts

    Filed under: .NET, C# 4, Débutant, DevLabs — Étiquettes : , , , , , , , — sebastiencourtois @ 16:12

    Code Contracts est une fonctionnalité ajouté à .NET 4 cette année mais qui a, pendant des années, été un projet DevLabs sous le nom de Spec# ou Code Contracts. L’idée est de permettre au développeur de fournir des informations sur son code au travers du code lui même.

    • Un exemple concret vaut mieux que de long discours

    Prenons un exemple d’une classe utilitaire.

    public class Division
    {
        public Division(decimal _num,decimal _denom)
        {
            this.Denominator = _denom;
            this.Numerator = _num;
        }
    
        public decimal Numerator { get; set; }
        public decimal Denominator { get; set; }
    
        public void Invert()
        {
            decimal tmpNum = this.Numerator;
            this.Numerator = this.Denominator;
            this.Denominator = tmpNum;
        }
    
        public decimal Compute()
        {
            return this.Numerator / this.Denominator;
        }
    
        public static decimal StaticCompute(decimal numerator, decimal denominator)
        {
            return numerator / denominator;
        }
    }
    
    public class Absolu
    {
        public static decimal StaticCompute(decimal value)
        {
            return Math.Abs(value);
        }
    }

    La première classe de division permet de créer des fractions et d’éxécuter le résultat. Un méthode statique permet de faire la même opération sans créer une instance de la classe. Une deuxième classe qui permet, au travers d’une méthode statique, de récupérer la valeur absolu d’un nombre.

    Si vous vous rappelez vos cours de mathématiques, vous savez surement qu’une valeur interdite dans le calcul d’une division est la valeur 0 pour le dénominateur. Si on regarde la méthode division, on voit que la valeur Dénominator peut être changé par le constructeur, par la propriété elle même (public get;set;), par la méthode Invert (si le numérateur = 0, alors l’invert donne un dénominateur = 0. Une solution avec .NET 3.5 serait de mettre un condition dans le set du numérateur afin de lancer une exception si une valeur = 0 tente d’être assigné. Toutefois cette façon de faire peut entrainer des codes dans les set assez long car il peut y avoir d’autres processus dans ce set ( NotifyPropertyChanged en WPF par exemple).

    • Contract Invariant / PréConditions (Requires)

    Afin de sortir le code de validation des propriétés, Code Contract propose de créer une méthode dite “Invariant” où l’on mettra toute les conditions à vérifier lorsqu’une méthode ou un constructeur est exécuté. Pour la classe Divsion, on pourrait créer la méthode “Invariant” suivante :

    [ContractInvariantMethod()]
    protected void DivisionInvariant()
    {
       Contract.Invariant(this.Denominator != 0, "Le dénominateur doit toujours être différent de 0.");
    }

    Ainsi si on tente de créer une instance Division d = new Division(2,0), on obtient l’exception suivantes

    cc1jpg

    Il est possible de faire cela sur des méthodes pour vérifier les paramètres par exemple.

    public static decimal StaticCompute(decimal numerator, decimal denominator)
    {
        Contract.Requires(denominator != 0, "Le dénominator doit être différent de 0.");
        return numerator / denominator;
    }

    Remarque : Le Contract.Requires fonctionne aussi pour les méthodes non statique.

    • PostConditions (Ensures)

    Les méthodes ci-dessus permettent de vérifier les entrées. Il serait aussi intéressant de définir les sorties. Toujours grâce à vos rappels de mathématiques, vous savez que la méthode absolu renverra toujours une valeur supérieure ou égale à 0. Cela est une spécification d’une sortie de méthode. Ainsi la classe absolu ci-dessus, devient :

    public class Absolu
    {
        public static decimal StaticCompute(decimal value)
        {
            Contract.Ensures(Contract.Result<decimal>() >= 0);
            return Math.Abs(value);
        }
    }

    La méthode Ensures() indique que la condition qui suit est satisfait lors de la sortie de la méthode. La méthode Result() récupère le résultat afin de l’analyser. Il est aussi possible de vérifier l’état d’un paramètre au début de la méthode gràce à la méthode Contract.OldValue().

    Si on remplace return Math.Abs(value) par return –1;, on obtient l’exception suivante :

    cc2

    • Installation et Configuration

    Afin d’utiliser les code contracts, il est nécessaire d’installer les outils Code Contracts : http://msdn.microsoft.com/fr-fr/devlabs/dd491992(en-us).aspx

    Une fois installé, il est nécessaire d’aller dans les propriétés du projet pour activer Code Contracts :

    cc3

    L’écran ci-dessus dépend de votre version de Visual Studio.

      • VS Express 2010 : Aucune possibilité de d’activer Code Contracts
      • VS Pro 2010 : Runtime Checking  (Standard Edition)
      • VS Team System 2008 ou VS Premium / Ultimate 2010 : Runtime + Static Checkin.  (Premium Edition)

    Juusqu’à maintenant, nous avons vu le Runtime Checking. Les vérifications se font lors de l’éxécution et génère des exceptions. Le Static Checking permet de voir ces problèmes dès la compilation lors d’une analyse statique de code.

    • Fonctionnement de Code Contracts

    Les codes de vérifications Code Contracts ne sont pas utilisé uniquement à l’exécution mais aussi à la compilation. Lors de la compilation ,le compilateur analyse les codes et génère du IL pour l’insérer aux endroits nécessaire. Ainsi la méthode Compute de Division est représenté comme suit :

    cc4 En Haut : Code généré sans code contract / En Bas : Code généré  avec Code Contracts

    Il est donc possible de mettre ses codes Contracts.Requires/Contracts.Ensures dans n’importe quel ordre dans les méthodes car tout est reclassé lors de la compilation.

    26/04/2010

    [Nouveautés .NET 4] Task Parallel Library : Gestion des architectures multicores/multiprocesseurs

    Filed under: .NET, C# 4, Débutant, Hors Catégorie, Optimisation — Étiquettes : , , , , , , , , — sebastiencourtois @ 09:55

    De nos jours, on ne trouve plus dans le marché de la vente d’ordinateurs que des architectures avec des processeurs double,quad,octo…. cores (et parfois même multi processeurs). Malgré cette débauche de puissance de calculs, il est triste à noter que nos applications ne sont, pour la plupart, pas tellement plus rapide qu’avant. La cause :

    • Les développeurs ne savent généralement pas coder des applications gérant plusieurs threads

    PFX1 Ca fait mal d’avoir 4 coeurs et d’en utiliser qu’un seul… à moitié !!!

    C’est un fait. Lorsque l’on apprend la programmation, on nous enseigne généralement beaucoup de choses mais rarement comment faire des applications gérant correctement plusieurs threads. L’une des raison est simple : Les API de développement sont généralement complexes et demande des efforts d’architecture/programmation/débug afin de rendre le tout utilisable. WaitHandle,Process,Mutex,lock,Interlocked … autant de mot clé et concepts barbare que le développeur doit maitriser afin de pouvoir décupler la puissance de ses applications. “On a déja du mal à faire une application correspondant aux spécifications dans le temps imparti, on va pas en plus passer des heures à gérer les multi coeurs”.

    Partant de ce constat, Microsoft a créé Parallel FX afin d’aider le développeur à gérer facilement les threads au sein de ses applications.

    • Organisation de TPL

    TPL se compose de 3 namespaces NET 4 :

    1. System.Threading.Tasks : Ce namespace contient la base de Task Parallel Library (TPL), il contient la classe principale de la technologie : la classe Tasks (sorte de “super Thread”)
    2. System.Collections.Concurrent : Ce namespace contient les collections pouvant être utilisé dans des contextes multithreads (thread safe).
    3. System.Linq : LINQ a été amélioré pour intégrer des aides au multithreading. Connu sous le nom de PLINQ (Parallel LINQ), il permet de paralléliser certaines étapes des requêtes LINQ afin de les rendre plus rapide.
    • Le namespace System.Threading.Task

    Ce namespace contient une classe Task. Cette classe représente une opération asynchrone. Ainsi on défini notre tache et, lorsqu’on la démarre, celle-ci est assigné par le TaskScheduler à un des coeurs disponibles de la machine. Ce système marche sous la forme d’une  file (first in / first out) de priorité (il y a des VIP :)). Ce Taskscheduler est aussi capable de distribuer un grand nombre de tâches aux différentes cœurs et de balancer la charge si un cœur est plus occupé qu’un autre (un peu à l’instar des load balancing dans les serveurs web).

    La création et l’utilisation des Task est simple :

    Task t1 = Task.Factory.StartNew(() =>
        {
            for (int i = 0; i < 200; i++)
            {
                if(i % 2 == 0)
                    Console.WriteLine(i+" ");
            }
        });
    
    Task t2 =new Task(() =>
    {
        for (int i = 0; i < 200; i++)
        {
            if (i % 2 == 1)
                Console.WriteLine(i + " ");
        }
    });
    t2.Start();

    Il y a,au moins, deux méthodes pour créer une Task. La première en utilisant une fabrique d’objet fournit par la classe Task. La seconde est de créer soi même une classe Task et de lui fournir la méthode à éxécuter puis de le lancer en appelant la méthode Start (un peu de la même façon qu’un thread. Il est à noter que l’on ne peut fournir de méthode retournant de valeurs. De plus, la méthode doit être sans paramètre ou alors avec un seul paramètres de type Object (Action, Action<object>).

    L’autre nouveautés très sympathique est la possibilité de paralléliser facilement ses boucles. Si on part des exemples suivants :

    foreach (string filename in Directory.GetFiles(MyDirectory, "*"))
        Console.WriteLine(filename);
    
    for (int i = 0; i < 200; i++)
    {
        if (i % 2 == 1)
            Console.WriteLine(i + " ");
    }

    Il est possible d’exécuter les itérations en parallèle en remplacement le for et le foreach par Parallel.For et Parallel.Foreach.

    Parallel.ForEach(Directory.GetFiles(MyDirectory, "*"), (filename) =>
    {
        Console.WriteLine(filename);
    });
    
    Parallel.For(0, 200, (i) =>
    {
        if (i % 2 == 1)
            Console.WriteLine(i + " ");
    });

    Et c’est tout !!! Le taskscheduler s’occupe de la répartition des opérations sur le processeur. On peut ainsi passer d’un code compliqué de copie de fichier multithreadée :

    public void SpeedCopyFolder(DirectoryInfo source, DirectoryInfo target, bool overrideExisting)
    {
        using (ManualResetEvent mre = new ManualResetEvent(false))
        {
            int threadCount = 0;
            foreach (FileInfo fi in source.GetFiles())
            {
                Interlocked.Increment(ref threadCount);
                // Create a thread for each file
                FileInfo file = new FileInfo(fi.FullName); // Created for the delegate scope
                ThreadPool.QueueUserWorkItem(delegate
                {
                    if (File.Exists(Path.Combine(target.ToString(), file.Name)) && !overrideExisting)
                        return;
                    fi.CopyTo(Path.Combine(target.ToString(), file.Name), overrideExisting);
                    if (Interlocked.Decrement(ref threadCount) == 0) mre.Set();
                });
            }
            if (Interlocked.Decrement(ref threadCount) == 0) mre.Set();
            mre.WaitOne();
        }
    }

    à un code beaucoup plus simple :

    public static void CopyFiles(string fromFolder, string toFolder)
    {
        Parallel.ForEach<string>(Directory.EnumerateFiles(fromFolder, "*"), f =>
        {
            File.Copy(f, toFolder + @"\" + Path.GetFileName(f), true);
        });
    }

    • Le namespace System.Collections.Concurrent

    Ce namespace contient des collections utilisables dans les cas de multithreading. Ces collections utilisent une interface créé spécialement pour l’occasion : IProducerConsumerCollection<T> qui permet la manipulation de ces collections.

    Les collections ainsi ajoutés :

    Les collections classiques fonctionnent toujours avec les tasks (sur les exemples que j’ai pu essayer.). Je pense qu’il doit y avoir des différences de performances/d’utilisations possibles qui doivent indiquer quel type utiliser. Je publierais surement un post quand j’en saurais un peu plus sur le sujet.

    • Parallel LINQ

    PLINQ est une technologie qui permet de paralléliser vos requêtes LINQ.

    Prenons l’exemple suivant : On souhaite récupérer les fichiers dont la première lettre est un ‘a’ puis classer ce résultat selon la dernière lettre du nom du fichier. On aurait là requête LINQ suivante  :

    Directory.GetFiles(MyDirectory, "*").Where((name) => name.First() == 'a').OrderBy((name) => name.Last());

    Si l’on étudie un peu la façon dont la requête s’éxécute, LINQ va d’abord parcourir la liste des fichiers en recherche de ceux commençant par a puis il va les classer par ordre. Cela est logique pour un traitement mono-core. Toutefois, on pourrait imaginer faire le classement en parralèle du Where. Ainsi lorsque le Where trouve un élément commençant par un ‘a’, il est envoyé au Orderby (se trouvant dans un autre thread) qui réalise un classement en temps réel. Vous devez vous dire que cela doit être complexe à coder. Pourtant voici là façon de faire.

    Directory.GetFiles(MyDirectory, "*").AsParallel().Where((name) => name.First() == 'a').OrderBy((name) => name.Last());

    Il suffit de rajouter un AsParallel() afin que toutes les expressions suivantes s’exécutent “simultanément”. “C’est pas wonderful ?!?!”

    • C’est bien beau tout ça mais … et les performances ?

    Sur un exemple qui est très parlant pour PLINQ (BabyNames disponible sur le training Kit VS 2010), j’obtiens les résultats suivants (sur une machine 4 cœurs avec une sélection du nombre de cœurs utilisés pour l’application).

      Nombre de cœurs LINQ (temps en secondes) PLINQ (Temps en secondes) Amélioration
      1 37,71 38,57 x 0.98
      2 37,46 20,68 x 1.81
      3 37,14 12,82 x 2.90
      4 37,36 10,60 x 3.53

    Remarque : La raison pour laquelle on n’arrive pas à 4x plus de performance avant un quad core est tout simplement du au fait que l’OS plus d’autres applications tourne en arrière plan avec des pics imprévisibles. De plus, les mécanismes de synchronisation entre thread prend aussi un peu de la puissance de calculs.

    Pour terminer, un petit graph qui fait plaisir :

    pfx2

    • Conclusion

    Cet technologie va surement réconcilier de nombreux développeurs avec la gestion des applications multithreadés. Toutefois, il est à noter que, dans certains cas, le parallélisme peut nuire au performance de l’application (car cela implique toujours en tâches de fond des techniques de synchronisation). A utiliser avec parcimonie et toujours en testant les performances avec et sans TPL.

    Remarque : TPL est disponible en C++ aussi.

    Note : Ce post a été fortement inspiré du post de Gal Ratner : http://galratner.com/blogs/net/archive/2010/04/24/a-quick-lap-around-net-4-0-s-parallel-features.aspx

    21/04/2010

    [Nouveautés .NET 4] Le Tuple

    Filed under: .NET, C# 4, Débutant — Étiquettes : , , , , — sebastiencourtois @ 10:05

    La classe Tuple est une nouvelle classe de base du Framework .NET 4. Cette classe permet de stocker des données ayant “une relation logique” au sein d’une application.

    On peut stocker 1 à 7 valeurs au sein d’un tuple grâce aux classes suivantes :

    public class Tuple<T1>
    
    public class Tuple<T1, T2>
    public class Tuple<T1, T2, T3>
    public class Tuple<T1, T2, T3, T4>
    public class Tuple<T1, T2, T3, T4, T5>
    public class Tuple<T1, T2, T3, T4, T5, T6>
    public class Tuple<T1, T2, T3, T4, T5, T6, T7>

    Afin d’utiliser un tuple, il suffit de créer une instance en fournissant les types que l’on souhaite stocker en paramètres générique de la classe. On obtient ainsi une classe typés ce qui permet d’éviter les problèmes de performances liés aux boxing/unboxing.

    Tuple<string, int> tsi = new Tuple<string, int>(string.Empty, 0);
    Tuple<string, int,bool> tsib = new Tuple<string, int,bool>(string.Empty, 0,true);

    Un tuple s’instancie comme une classe normale. On fait appel au constructeur en fournissant les données que l’on souhaite stocker conjointement. Il est intéressant de noter que, gràce à la généricité, on peut imbriquer les tuples et ainsi stocker théoriquement une infinité de données dans un tuple (voir exemple suivant).

    Tuple<string, int,bool,int,string,bool,int,Tuple<int,string>> tsib3 =
    new Tuple<string, int,bool,int,string,bool,int,Tuple<int,string>> (string.Empty, 0,true,2,string.Empty,true,2,new Tuple<int,string>(2,"Toto"));

    L’accès au données se réalise par une propriété ItemX (avec X étant le numéro d’ordre de la donnée) en lecture seule.

    public class Tuple<T1, T2> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple
    {
        public Tuple(T1 item1, T2 item2)
        public T1 Item1 { get; }
        public T2 Item2 { get; }
    }

    • Pourquoi Tuple expose ses propriétés en lecture seule ?

    La raison principale est l’immutabilité. On dit qu’une classe est “immutable” si on ne peut modifier ses données qu’au travers de son constructeur. Un des intérêts est que, dans le cas de programmation parralèlle, une classe immutable ne peut être l’objet de verrou ou de race situation car ses données sont figés dans le RAM après la création de la classe. C’est donc uniquement pour des soucis de performances que ces propriétés sont en read only.

    Il est toutefois possible de se créer une classe Tuple modifiable en créant la même classe et en rajoutant l’accesseur set sur les propriétés

    public class MyTuple<T1, T2>
    {
        public MyTuple(T1 t1, T2 t2) { }
    
        public T1 Item1 { get; set; }
        public T2 Item2 { get; set; }
    }
    • Cas d’utilisation des tuples : Les valeurs retours de méthodes

    Examinons un exemple pour comprendre une des utlisations des tuples. Je souhaite créer une méthode prenant des données sur une rectangle en paramètres et me fournissant les données sur les coordonnées de deux points de ce rectangle.

    En .NET 3.5, je pourrais faire une première méthode avec le mot clé out :

    static void CreateRectangleData(string name, int x, int y, int sizex, int sizey,out string finalname,out int p1x,out int p1y,out int p2x,out int p2y)
    {
        finalname = name;
        p1x = x;
        p1y = y;
        p2x = x + sizex;
        p2y = y + sizey;
    }

    L’utilisation de cette méthode peut être fastidieuse car il faut créer l’ensemble des variables du coté de l’appelant. De plus, il serait plus propre de créer une classe intermédiaire pour stocker/structurer ces données.

    public class MyRectangle
    {
       public string name;
       public int Point1X;
       public int Point1Y;
       public int Point2X;
       public int Point2Y;
    }

    static MyRectangle CreateRectangleData(string name,int x,int y,int sizex,int sizey)
    {
         return new MyRectangle()
         {
           name = name,
           Point1X = x,
           Point1Y = y,
           Point2X = x+sizex,
           Point2Y = y+sizey
        };
    }

    Meilleure solution (selon moi) pour récupérer ces données. Toutefois, lorsque l’on a trop de classe intermédiaire de ce genre (ne servant souvent qu’à récupérer plusieurs données d’une seul méthode) le code devient vite illisible. C’est là où les tuples peuvent intervenir.

    static Tuple<string,int,int,int,int> CreateRectangleData(string name, int x, int y, int sizex, int sizey)
    {
       return new Tuple<string, int, int, int, int>(name,x,y,x + sizex,y + sizey);
    }

    Le code devient donc plus propre car on peut se débarrasser des classes intermédiaires en les remplaçant par des tuples. Il faut toutefois bien être conscient de l’ordre des données afin de ne pas se tromper en les traiter après.

    • Avant j’utilisais KeyValuePair<K,T> et cela marchait très bien. Pourquoi utiliser Tuple<T1,T2> à la place ?

    Il faut, tout d’abord, savoir que Tuple est une généralisation de la classe KeyValuePair. On peut décrire KeyValuePair comme étant un Tuple à 2 valeurs alors que Tuple peut prendre N valeurs.

    Une différence notable est que Tuple est une classe (type référence) alors que KeyValuePair est une structure (type valeur).

    public class Tuple<T1, T2> : /**/
    {
        public Tuple(T1 item1, T2 item2)
        public T1 Item1 { get; }
        public T2 Item2 { get; }
    }

    public struct KeyValuePair<TKey, TValue>
    {
        public KeyValuePair(TKey key, TValue value);
        public TKey Key { get; }
        public TValue Value { get; }
    }

    Définition de la classe Tuple Définition de la structure KeyValue Pair

    Le choix entre KeyValuePair<K,T> et Tuple<T1,T2> doit aussi être fait selon la logique de votre code. Il est plus logique de stocker des données d’un dictionnaire selon un système de clé/valeur en utilisant KeyValuePair  et d’utiliser Tuple<T1,T2> pour stocker les données d’une méthode ayant deux valeurs retours.

    15/04/2010

    [Nouveautés C# .NET 4] Paramètres nommés et optionnels

    Filed under: .NET, Débutant — Étiquettes : , , , , — sebastiencourtois @ 12:43

    Suite de notre tour sur les nouveautés de C# 4. Après une présentation de la Co-Contravariance, nous allons nous intéresser à une fonctionnalité qui faisait cruellement défaut à .NET : les paramètres nommés / optionnels.

    • Paramètres optionnels

    Qui n’a pas écrit ce type de code dans un programme :

    public void MaFonction(int a)
    {
      this.MaFonction(a, "toto");
    }
    public void MaFonction(int a, string b)
    {
       this.MaFonction(a, b, null);
    }
    public void MaFonction(int a, string b, StringBuilder c)
    {
       //Fait le traitement
    }

    une suite (parfois sans fin) de méthodes s’appelant les unes les autres avec des ajouts de paramètres pour arriver à “la fonction qui fait tout”. Cela a pour désavantage d’augmenter sensiblement (et inutilement) la taille du code source et cela peut être la source d’erreurs dans l’attribution des paramètres.

    Pour parer à ces inconvénients, Microsoft a introduit, dans .NET 4, une nouvelle fonctionnalité : les paramètres optionnels. Il s’agit de définir des valeurs par défaut aux paramètres que l’on souhaite définir comme optionnel.

    Le code précédent peut donc s’écrire en .NET 4 :

    public void MaFonctionNET4(int a, string b = "Toto", StringBuilder c = null)
    {
        //Fait le traitement
    }

    Au niveau de l’Intellisense, on se retrouve avec les affichages suivants : 

    params1params2

    Intellisense pour la version classique

    Intellisense pour la version .NET 4

    Remarque : Les paramètres optionnels doivent toujours apparaitre après les paramètres obligatoires. Ainsi le code suivant génère une erreur de compilation.

    params3 
    • Paramètres nommés

    Lorsque l’on a des méthodes avec beaucoup de paramètres, il arrive que l’on ne se rappelle plus de l’ordre de chacun des paramètres. Récupérer l’information par l’IntelliSense ou en allant voir dans le code source peut prendre parfois un peu de votre temps au combien précieux.

    Pour pallier ce problème, .NET 4 permet d’utiliser les paramètres nommés.

    Prenons une méthode classique :

    public void MaFonctionNET4WithName(string param1, int param2 = 0, double param3 = -42.424242)
    {
    
    }

    .NET 4 permet de définir les paramètres en fonction de leur nom et non plus en fonction de leur ordre. On peut ainsi écrire :

    t.MaFonctionNET4WithName(param1: "Test",param2: 5);

    Il est à noter que les paramètres nommés et les paramètres optionnels peuvent cohabiter.

    Remarque : Lorsque l’on utilise les paramètres nommés pour l’appel d’une méthode, il faut le faire sur l’ensemble des paramètres il faut indiquer les paramètres nommés après les paramètres classiques (qui, eux, doivent être dans l’ordre)  . Dans le cas contraire,on récupère l’exception suivante. Dans le cas contraire,on récupère l’exception suivantes.

    params4 

    En revanche, l’exemple suivant est valide :

      t.MaFonctionNET4WithName("Toto", param3: 34.45, param2: 8); // Fonctionne car les paramètres non nommés sont en premiers
    

    • Conclusion

    J’attendais cette fonctionnalité depuis un moment (surtout parce que je l’avais déjà utilisé dans d’autres langages). Il faut avouer que c’est une fonctionnalité simple,rapide à mettre en place et qui apporte une meilleur lisibilité et stabilité au code produit.

    Merci à François Guillot pour avoir détecté l’erreur concernant l’ordre des paramètres.

    14/04/2010

    [Nouveautés C# .NET 4] Introduction à la Covariance / Contravariance

    Filed under: .NET, Débutant, Intermediaire — Étiquettes : , , , , , — sebastiencourtois @ 14:44

    Pour la sortie de .NET 4, je vais faire quelques articles sur les nouveautés du langages (en attendant de pouvoir jouer avec SL4). Aujourd’hui, nous allons voir la notion de Covariance / Contravariance.

    EDIT (08/05/2010) : La Covariance/Contravariance n’est pas disponible pour Silverlight 4

    • Définitions

    La covariance et la contravariance dixit Wikipedia :

    Within the type system of a programming language, a typing rule or a type conversion operator is:

    • covariant if it preserves the ordering, ≤, of types, which orders types from more specific to more generic;
    • contravariant if it reverses this ordering, which orders types from more generic to more specific;
    • invariant if neither of these apply.

    Bon, je suis d’accord qu’avec cette définition, on est pas plus avancé. On voit que cela parle de conversion de type et de généricité mais ça reste très abstrait.

    Nous allons donc voir des exemples puis revenir sur ces définitions.

    • Exemples sur la covariance 

    Nous prendrons une architecture simple pour ces exemples :

    public abstract class Person { }
    public class Employe : Person { }
    public class Customer : Person { }
    public class Manager : Employe { }

    Nous allons maintenant jouer avec des tableaux de ces objets :

    Person[] MaSociete = new Manager[10];  // .NET 3.5 / 4 : OK 
    Employe[] MaSociete2 = new Manager[10]; // .NET 3.5 / 4 : OK 
    

    Rien de transcendant là dessus. On a un tableau de Manager. Un manager étant une personne (ça dépend dans quel société …:)), il n’y a pas de problème à mettre les données d’un tableau de manager dans un tableau de personnes. De même pour mettre nos données manager dans le tableau d’employés (Manager étant un employé).

    Faison la même chose avec des listes

    Manager[] MaSociete3 = new Manager[10];
    IEnumerable<Manager> im = MaSociete3.Where(p => p.ToString() == "Manager"); // .NET 3.5 / 4 : OK 
    IEnumerable<Manager> im2 = new List<Manager>(); // .NET 3.5 / 4 : OK 

    Le requête LINQ renvoie un IEnumerable<Manager> que je peux donc mettre dans im. De même pour une liste de manager car List<T> implémente IEnumerable<T>.

    IEnumerable<Person> ip = im;  // .NET 3.5 : Erreur de compilation  / .NET 4 : OK
    

    Sur cette ligne, on obtient une erreur de compilation en .NET 3.5 (Cannot Convert IEnumerable<Manager> to IEnumerable<Person>). Pourtant cela devrait marcher comme un tableau (un manager est une personne donc un tableau de personne peut contenir des données venant d’un tableau de manager). Cela n’est pas possible car, en .NET 3.5, IEnumerable<T> n’est pas covariant (à l’inverse de Array qui lui est covariant). C’est pour corriger ce manque que Microsoft a autorisé la covariance des interfaces et des délégués en .NET 4.

    En .NET 4, je peux ainsi écrire :

    IEnumerable<Person> ipl = new List<Manager>();  // .NET 3.5 : Erreur de compilation  / .NET 4 : OK
    Func<string> funcString = () => { return "Bonjour le monde"; };
    Func<object> funcObject = funcString;         // .NET 3.5 : Erreur de compilation  / .NET 4 : OK
    

    Pour résumer, on peut dire que la covariance permet de caster un type générique A<T> dans un type générique A<K> si T hérite directement ou indirectement de K et si le type T est un paramètre de sortie (nous reviendrons sur cette notion plus loin).

    Remarque :  La covariance (ainsi que la contravariance) fonctionne uniquement avec des interfaces et des délégués. Les arguments génériques doivent être des types références. Il n’est pas possible de faire les lignes suivantes :

    List<Person> lo = new List<Manager>();         // .NET 3.5 / 4 : Erreur de compilation 
    IEnumerable<ValueType> lo = new List<int>();    // .NET 3.5 / 4 : Erreur de compilation 
    

    • Exemples sur la contravariance 

    Dans certains cas, on aimerait que l’inverse soit possible.

      Action<object> actObject = (object o) => { };
      Action<string> actString = actObject;          // .NET 3.5 : Erreur de compilation  / .NET 4 : OK

    Dans cet exemple, je souhaite pouvoir caster mon délégués avec une valeur de retour de type string ou un de ses parents. En effet,lorsque je récupère une donnée, je peux la caster dans une variable d’un type dont elle hérite. C’est le principe de la contravariance introduite dans .NET 4.

    La contravariance est la possiblité de caster un type générique A<T> dans un type générique A<K> si T est parent direct ou indirect de K et si T est un paramètre d’entrée (nous reviendrons sur cette notion plus loin).

    • Les interfaces / Délégués déja covariant et contravariant

    Voici la liste des interfaces/délégués ayant été mis à jour au sein du framework .net 4 pour tenir compte de cette nouvelle possibilité.

     

    • Exemple pratique : La création d’une interface covariant et contravariant

    Prenons l’exemple d’une classe réalisant de la transformations de données.

    public interface IDataTransformer<T, K>
    {
        K Transform(T data);
    }
    
    public class DataTransformer<T, K> : IDataTransformer<T, K>
    {
        public K Transform(T data)
        {
            return default(K);
        }
    }

    Nous avons donc une interface IDataTransformer comprenant deux paramètres génériques T et K et une méthode qui va transformer une variable de type T en type K. Nous allons créer un type implémentant cette interface puis l’utiliser.

    IDataTransformer<Manager, string> dataTransformer = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : OK
    IDataTransformer<Person, string> dataTransformer2 = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : Erreur de compilation 
    IDataTransformer<Manager, object> dataTransformer3 = new DataTransformer<Manager, string>(); // .NET 3.5 / 4 : Erreur de compilation 
    

    Si le premier cas fonctionne logiquement, les deux cas suivants sont  moins logique. Dans le cas de dataTransfomer2, si on crée un DataTransformer prenant un Manager en entrée, on devrait aussi pouvoir prendre une personne (rappelons le, les managers sont des personnes).Pourtant le compilateur n’est pas de cette avis car il n’arrive pas à convertir DataTransformer<Manager,string> en IDataTransformer<Person,string>. De même pour dataTransformer3. Si on récupère en sortie une chaine de caractère, on pourrait très bien récupérer un object car string dérive de object.

    Voila les limites de .NET 3.5 et voyons comment faire pour corriger cela en  .NET 4. Pour cela nous allons créer une nouvelle interface et une nouvelle classe Variant.

    public interface IDataTransformerVariant<in T,out K>
    {
        K Transform(T data);
    }
    
      
    public class DataTransformerVariant<T, K> : IDataTransformerVariant<T, K>
    {
        public K Transform(T data)
        {
            return default(K);
        }
    }

    La différence se situe uniquement dans la déclaration de l’interface IDataTransformerVariant. On a rajouté un mot clé in devant le type d’entrée (T) et un out devant le type de sortie (K). En procédant ainsi, on active la covariance et la contravariance et on oblige le paramètre générique T a toujours être en paramètres de méthodes et le paramètre générique K a toujours être en valeur retour d’une méthode. Ne pas suivre cette règle entraine l’erreur de compilation suivante :

    CoVarImg1

    • Conclusion

    Pour conclure, je reprendrais les phrases importantes de cet article :

    la covariance permet de caster un type générique A<T> dans un type générique A<K> si T hérite directement ou indirectement de K et si le type T est un paramètre de sortie.

    La contravariance est la possiblité de caster un type générique A<T> dans un type générique A<K> si T est parent direct ou indirect de K et si T est un paramètre d’entrée.

    Le fait que un paramètre générique soit d’entrée ou de sortie dépend de son placement dans les méthodes interfaces ou des délégués.

    A titre d’information, ce concept est disponible depuis .NET 2.0 en IL (comme l’à montré Simon Ferquel dans son blog) mais aucun mot clé n’étant fourni en .NET, on ne pouvait pas l’utliser en C#.

    Merci à Alexis Pera (MSP) pour la relecture et correction de cet article.

    Propulsé par WordPress.com.

    %d blogueurs aiment cette page :