AutoCAD blocs trucs et astuces variables AutoCAD Mac AutoCAD WS AutoCAD Architecture AutoCAD MEP AutoCAD WS Revit Covadis AutoCAD Map Actualité Emploi Inventor Solidworks Creo Catia Lisp .NET VBA
ExMateria
Chargement
Discuter informatique sur le Forum Informatique
 
Gros caractères  Caractères normaux
*
Bienvenue sur ExMateria, Invité. Veuillez vous connecter ou vous inscrire. 22 Mai 2013 à 13:19


Connexion avec identifiant, mot de passe et durée de la session


Pages: [1]   Bas de page
  Envoyer ce fil  |  Imprimer  
Auteur Fil de discussion:

Linq et méthodes d'extension, sucre syntaxique ?

 (Lu 1892 fois)
0 Membres et 1 Invité sur ce fil de discussion.
gile
ceinture verte
***

Karma: 16
Hors ligne Hors ligne

Sexe: Homme
Messages: 156



« le: 01 Mars 2012 à 22:14 »

Salut,

Pour faire simple, les fonctionnalités LINQ (Language Integrated Query) intègrent les requêtes SQL au langage. L'utilisation de LINQ concerne donc essentiellement l'accès aux bases de données mais peut aussi être utilisé avec tous types de collections .NET (Array, List, Dictionary, etc.). Ce qui devrait intéresser les programmeurs AutoCAD qui ont souvent des collections à traiter (tables, dictionnaires, jeux de sélection, etc.)
Les requêtes peuvent s'écrire dans un style proche du langage SQL mais aussi grâce aux nombreuses méthodes d'extension de l'interface IEnumerable<T> (implémentée par toutes les collections génériques .NET).
Nombre de ces méthodes sont des fonctions dites "d'ordre supérieur", c'est à dire qu'elle acceptent comme argument une fonction (comme les fonctions LISP mapcar, apply, vl-sort, etc.). La fonction passée en argument, l'est souvent sous forme d'une expression lambda.
Ces fonctionnalités typiques de la programmation fonctionnelle, permettent, à mon avis, d'écrire dans un style plus déclaratif.

Par exemple pour construire une collection des carrés de tous les entiers inférieurs à 10 contenus dans la liste lst, on peut écrire :
Code:
            var sqr = from i in lst
                      where i < 10
                      select i * i;
ou, en utilisant les méthodes d'extension avec des expressions lambda :
Code:
            var sqr = lst
                .Where(i => i < 10)
                .Select(i => i * i);
L'utilisation du mot clé var permet l'inférence de type, le type de sqr est déduit des l'édition en fonction du code. Ici le type de sqr est IEnumerable<int>.

Voyons un peu avec un exemple plus concret, par exemple une commande qui met toutes les lignes contenues dans tous les blocs de la table sur le calque "0".
Dans un style impératif classique on fait une boucle foreach sur tous les blocs de la table et, pour chaque bloc on boucle sur toutes les entités pour trouver les lignes et leur affecter le calque "0".

Code:
        [CommandMethod("CmdImp")]
        public void CmdImp()
        {
            Database db = HostApplicationServices.WorkingDatabase;
            using (Transaction tr = db.TransactionManager.StartTransaction())
            {
                BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
                foreach (ObjectId btrId in bt)
                {
                    BlockTableRecord btr = (BlockTableRecord)tr.GetObject(btrId, OpenMode.ForRead);
                    foreach (ObjectId id in btr)
                    {
                        Line line = tr.GetObject(id, OpenMode.ForRead) as Line;
                        if (line != null)
                        {
                            line.UpgradeOpen();
                            line.Layer = "0";
                        }
                    }
                }
                tr.Commit();
            }
        }

Avec LINQ, il faut convertir la table des blocs et les définitions de blocs en collection d'ObjectId (IEnumerable<ObjectId>) pour pouvoir utiliser les méthodes d'extension. La requête porte sur la table des blocs et collecte toutes les lignes contenues dans tous les blocs. La variable lines est de type IEnumerable<Line>. Une boucle foreach classique met ces lignes sur le calque "0".

Code:
        [CommandMethod("CmdLinq")]
        public void CmdLinq()
        {
            Database db = HostApplicationServices.WorkingDatabase;
            using (Transaction tr = db.TransactionManager.StartTransaction())
            {
                BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
                var lines = bt
                    .Cast<ObjectId>()
                    .SelectMany(btrId => ((BlockTableRecord)tr.GetObject(btrId, OpenMode.ForRead))
                        .Cast<ObjectId>()
                        .Select(id => tr.GetObject(id, OpenMode.ForRead))
                        .OfType<Line>());
                foreach (Line line in lines)
                {
                    line.UpgradeOpen();
                    line.Layer = "0";
                }
                tr.Commit();
            }
        }

On pourrait croire à tort que la construction LINQ et la boucle foreach sont redondantes, il n'en est rien, ce n'est qu'à chaque itération dans foreach que la requête est exécutée. Une fois compilé, le code en langage intermédiaire (MSIL) des deux routines est quasiment le même.

Dans cet exemple on peut penser que l'utilisation de LINQ n'a pas apporté grand chose en terme de lisibilité et de concision, mais nous allons voir que ceci combiné avec quelques méthodes d'extensions peut changer bien des choses...
Journalisée
  
Patrick (admin)
grand gourou
Administrateur
*****

Karma: 11
Hors ligne Hors ligne

Sexe: Homme
Messages: 1760



WWW
« Répondre #1 le: 01 Mars 2012 à 22:44 »

L'ordre a-t-il une importance?

Code:
            var sqr = lst
                .Where(i => i < 10)
                .Select(i => i * i);

est-il équivalent à:

Code:
            var sqr = lst
                .Select(i => i * i)
                .Where(i => i < 10);

car il me semble que c'est le cas en SQL.
Journalisée

gile
ceinture verte
***

Karma: 16
Hors ligne Hors ligne

Sexe: Homme
Messages: 156



« Répondre #2 le: 01 Mars 2012 à 22:51 »

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. Pour définir des méthodes d'extension 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. Les méthodes ainsi définies peuvent être appelées comme des méthodes d'instance sur les objets de la classe étendue.
Par exemple, la classe suivante étend la classe System.String avec la méthode ToTitle(). Si une variable s contient la chaîne : "ceci est un test", s.ToTitle() retourne "Ceci Est Un Test".
Code:
    static class Extension
    {
        public static string ToTitle (this string s)
        {
            System.Globalization.CultureInfo cultureInfo = System.Threading.Thread.CurrentThread.CurrentCulture;
            System.Globalization.TextInfo TextInfo = cultureInfo.TextInfo;
            return TextInfo.ToTitleCase(s);
        }
    }

Mais revenons à nos moutons, Ci dessous un petite classe où sont définies trois méthodes d'extensions, chacune ayant une surcharge dont on ne se servira pas dans l'exemple (merci à kaefer sur TheSwamp pour l'inspiration).
Ces méthodes sont génériques au même titre que les collections génériques (List<T>, Dictionary<TKey, TValue>, etc) ce qui signifie qu'elles utiliseront un typage fort quand le type sera défini dans le code.

Code:
    public static class Extensions
    {
        /// <summary>
        /// Ouvre un objet AutoCAD (DBObject) en mode lecture (à utiliser dans une transaction).
        /// </summary>
        /// <typeparam name="T">Type de l'objet à retourner.</typeparam>
        /// <param name="id">Identifiant (ObjectId) de l'objet à ouvrir.</param>
        /// <returns>L'objet ou null si la conversion a échoué.</returns>
        public static T GetObject<T>(this ObjectId id) where T : DBObject
        {
            return id.GetObject<T>(OpenMode.ForRead);
        }

        /// <summary>
        /// Ouvre un objet AutoCAD (DBObject) dans le mode donné (à utiliser dans une transaction).
        /// </summary>
        /// <typeparam name="T">Type de l'objet à retourner.</typeparam>
        /// <param name="id">Identifiant (ObjectId) de l'objet à ouvrir.</param>
        /// <param name="mode">Mode d'ouverture (OpenMode).</param>
        /// <returns>L'objet ou null si la conversion a échoué.</returns>
        public static T GetObject<T>(this ObjectId id, OpenMode mode) where T : DBObject
        {
            return id.GetObject(mode) as T;
        }

        /// <summary>
        /// Ouvre une collection d'objets AutoCAD (DBObject) en mode lecture (à utiliser dans une transaction).
        /// </summary>
        /// <typeparam name="T">Type des objets à retourner.</typeparam>
        /// <param name="ids">Collection d'identifaints (ObjectId).</param>
        /// <returns>La collection d'objets correspondants au type.</returns>
        public static IEnumerable<T> GetObjects<T>(this IEnumerable ids) where T : DBObject
        {
            return ids.GetObjects<T>(OpenMode.ForRead);
        }

        /// <summary>
        /// Ouvre une collection d'objets AutoCAD (DBObject) dans le mode donné (à utiliser dans une transaction).
        /// </summary>
        /// <typeparam name="T">Type des objets à retourner.</typeparam>
        /// <param name="ids">Collection d'identifiants (ObjectId).</param>
        /// <param name="mode">Mode d'ouverture (OpenMode).</param>
        /// <returns>La collection d'objets correspondants au type.</returns>
        public static IEnumerable<T> GetObjects<T>(this IEnumerable ids, OpenMode mode) where T : DBObject
        {
            return ids
                .Cast<ObjectId>()
                .Select(id => id.GetObject(mode))
                .OfType<T>();
        }

        /// <summary>
        /// Applique la procédure donnée à chaque élément de la collection.
        /// </summary>
        /// <typeparam name="T">Type d'objet auquel s'applique la procédure.</typeparam>
        /// <param name="collection">Collection d'objets.</param>
        /// <param name="action">Procédure à appliquer à chaque élément de la collection.</param>
        public static void Iterate<T>(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection) action(item);
        }

        /// <summary>
        /// Applique la procédure donnée à chaque élément de la collection.
        /// L'entier passé à la procédure indique l'index de l'élément.
        /// </summary>
        /// <typeparam name="T">Type d'objet auquel s'applique la procédure.</typeparam>
        /// <param name="collection">Collection d'objets.</param>
        /// <param name="action">Procédure à appliquer à chaque élément de la séquence qui peut également accéder à l'index actuel.</param>
        public static void Iterate<T>(this IEnumerable<T> collection, Action<int, T> action)
        {
            int i = 0;
            foreach (T item in collection) action(i++, item);
        }
    }

On peut maintenant écrire une troisième version de notre commande en utilisant les méthodes ci-dessus définies.

Code:
        [CommandMethod("CmdExt")]
        public void CmdExt()
        {
            Database db = HostApplicationServices.WorkingDatabase;
            using (Transaction tr = db.TransactionManager.StartTransaction())
            {
                db.BlockTableId
                    .GetObject<BlockTable>()
                    .GetObjects<BlockTableRecord>()
                    .SelectMany(btr => btr.GetObjects<Line>())
                    .Iterate(line => { line.UpgradeOpen(); line.Layer = "0"; });
                tr.Commit();
            }
        }

Là, on a indéniablement gagné en concision et, à mon sens en lisibilité avec un style plus déclaratif.
Journalisée
gile
ceinture verte
***

Karma: 16
Hors ligne Hors ligne

Sexe: Homme
Messages: 156



« Répondre #3 le: 01 Mars 2012 à 22:57 »

Bien sûr que l'ordre a une importance. On parle de chaînage de méthodes avec l'opérateur point (.) ce qui ressemble un peu au Pipeline dans d'autres langages (comme F#).
Code:
            var sqr = lst
                .Where(i => i < 10)
                .Select(i => i * i);
Ici on commence par ne conserver que les nombres inférieurs à 10 puis on met ces nombres au carré.
Code:
            var sqr = lst
                .Select(i => i * i)
                .Where(i => i < 10);
Là on met d'abord les nombres au carré et on ne conserve que ceux qui sont inférieurs à 10.
Journalisée
rom1_am
ceinture jaune
*

Karma: 10
Hors ligne Hors ligne

Sexe: Homme
Messages: 26



« Répondre #4 le: 02 Mars 2012 à 07:22 »

Merci Gilles. C'est bien expliqué.
Journalisée
Patrick (admin)
grand gourou
Administrateur
*****

Karma: 11
Hors ligne Hors ligne

Sexe: Homme
Messages: 1760



WWW
« Répondre #5 le: 02 Mars 2012 à 08:58 »

Pour définir des méthodes d'extension 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.

Ici on étend la classe System.string mais en ne mentionnant que "string", qu'arrive-t-il si on a deux classes ou plus contenant "string", laquelle sera étendue?
Journalisée

gile
ceinture verte
***

Karma: 16
Hors ligne Hors ligne

Sexe: Homme
Messages: 156



« Répondre #6 le: 02 Mars 2012 à 10:19 »

Normalement, quand il y a ambiguïté sur un nom de classe, l'intellisense prévient tout de suite et propose les différentes "adresses complètes" avec les espaces de nom.
Mais dans ce cas, le problème ne se posera pas, string (en minuscules) est un alias pour la classe System.String.
Journalisée
Patrick (admin)
grand gourou
Administrateur
*****

Karma: 11
Hors ligne Hors ligne

Sexe: Homme
Messages: 1760



WWW
« Répondre #7 le: 02 Mars 2012 à 10:28 »

Ok donc pour éviter une ambiguïté, aurait-on pu écrire:
Code:
public static string ToTitle (this System.string s)

au lieu de:
Code:
public static string ToTitle (this string s)
Journalisée

gile
ceinture verte
***

Karma: 16
Hors ligne Hors ligne

Sexe: Homme
Messages: 156



« Répondre #8 le: 02 Mars 2012 à 11:46 »

Petit complément concernant les "fonctions d'ordre supérieur".
Dans les langages dits "fonctionnels" (LISP, F#, OCaml, Haskell, etc.), les fonctions sont des objets de "première classe", c'est à dire que les fonctions sont traitées comme n'importe quel autre objet. Il est donc naturel qu'une fonction puisse prendre comme argument une autre fonction ou retourner une fonction.
C# (comme VB) n'est pas un langage fonctionnel et, pour pouvoir passer une méthode comme argument d'une autre méthode, recours à l'utilisa tion de délégués.
Un délégué (delegate en anglais) est un objet de la classe Delegate (ou d'une classe qui en hérite) qui permet d'appeler une méthode qui n'est pas forcément toujours la même mais qui doit respecter la signature du délégué, c'est à dire que la méthode doit avoir le même nombre d'arguments du même type et le même type de valeur de retour.
Les gestionnaires d'évènements, par exemple, sont des délégués de type EventHandler (ou d'une classe qui en dérive) qui doivent être des méthodes (procédures dirait on en VB) qui ne retournent rien (void) et qui acceptent deux arguments, le premier de type Object, le second de type EventArgs (ou d'une classe qui en hérite).

La signature de la fonction SelectMany utilisée dans l'exemple est :
Code:
public static IEnumerable<TResult> SelectMany<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TResult>> selector)
Le type de retour, IEnumerable<TResult>, est une collection d'objets de type TResult (Line dans l'exemple ci-dessus) et les arguments sont :
- IEnumerable<TSource> : la collection d'objets de type TSource (ObjectId dans l'exemple) à laquelle s'applique la fonction ;
- Func<TSource, IEnumerable<TResult>> : la fonction passée en argument. Func (comme Action) est un délégué générique, sa signature indique que la fonction doit prendre un argument de type TSource et retourner une collection d'objet de type IEnumerable<TResult>.
C'est SelectMany qui concatène les collections de TResult retournées par Func pour chaque TSource contenu dans la collection à laquelle s'applique SelectMany.

Pour passer les fonctions en argument dans les exemples ci-dessus, on a utilisé des expressions lambda. Si ceci peut être familier pour les LISPeurs (avec la fonction lambda, justement) ou pour les utilisateurs d'autres langages fonctionnels, ça peut être un peu perturbant pour d'autres.
L'utilisation d'expressions lambda (ou de fonctions anonymes comme nous le verrons) n'est pas une obligation, mais présente l'intérêt, outre la concision du code, de définir la fonction à l'endroit où elle est exécutée, ce qui peut rendre le code plus lisible (surtout quand les fonctions sont très courtes).
On peut très bien passer en argument une méthode définie ailleurs dans le code pour autant que cette méthode respecte la signature (c'est ce que l'on fait habituellement avec les gestionnaires d'évènement).

Par exemple, le traitement des lignes, argument de la méthode Iterate() dans le dernier exemple aurait pu être défini dans une méthode séparée qui aurait été appelée directement dans Iterate().

Code:
        public void CmdExt()
        {
            ...
                    .Iterate(ToLayer0);
             ...
        }

        private void ToLayer0(Line line)
        {
            line.UpgradeOpen();
            line.Layer = "0";
        }

On aurait aussi pu, utiliser une méthode anonyme, mais, à mon avis, ça ne présent que peu d'intérêt par rapport à une expression lambda :

Code:
.Iterate(delegate(Line line) { line.UpgradeOpen(); line.Layer = "0"; });
Journalisée
gile
ceinture verte
***

Karma: 16
Hors ligne Hors ligne

Sexe: Homme
Messages: 156



« Répondre #9 le: 02 Mars 2012 à 12:01 »

Ok donc pour éviter une ambiguïté, aurait-on pu écrire:
Code:
public static string ToTitle (this System.string s)

au lieu de:
Code:
public static string ToTitle (this string s)

Attention à la casse en C#, System.string provoque une erreur à l'édition, il faut écrire System.String.
Mais comme dit ci dessus, string (sans majuscule) est un alias pour System.String, comme int pour System.Int32, short pour System.Int16, etc. Il n'y a donc ici aucune ambiguïté possible, du moins pour le compilateur.
Journalisée
Patrick (admin)
grand gourou
Administrateur
*****

Karma: 11
Hors ligne Hors ligne

Sexe: Homme
Messages: 1760



WWW
« Répondre #10 le: 05 Mars 2012 à 08:34 »

Petite précision que je crois utile, le mot sucre dans le titre du sujet ne fait pas référence à ce que vous mettez dans le café, mais à ceci...  grand sourire
Après, c'est au goût de chacun, moi je ne suis pas trop sucré/salé, mais plutôt sucreries, c'est bien connu...  clin d'oeil
Journalisée

Libellés: extension  linq  collections  [messages similaires]  [sites CAO]  [AutoCAD Q&R]   
Pages: [1]   Haut de page
  Envoyer ce fil  |  Imprimer  
 
Aller à:  

* Permissions
Vous ne pouvez pas poster de nouveaux sujets. Vous ne pouvez pas poster de nouvelles réponses. Vous ne pouvez pas poster des pièces jointes. Vous ne pouvez pas modifier vos messages.
BBCode Activé Emoticônes Activé [img] Activé HTML Activé

Propulsé par MySQL Propulsé par PHP Powered by SMF 1.1.7 | SMF © 2006-2007, Simple Machines LLC

Copyright (c) 2008 ExMateria .NET AutoCAD developpers center  Blog  Wiki  
XHTML 1.0 Transitionnel valide ! CSS valide ! Dilber MC Theme by HarzeM