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

C# 3.0

Par Matthieu MEZIL
 

Présentation de C# 3.0 avec la CTP de janvier 2007

            

I. Orcas
II. C# 3.0
II-A. Solution C# 2.0
II-A-1. Les nouveautés de C# 3.0
II-A-1-a. Les "Object initializers"
II-A-1-b. Les "Local variable type inference"
II-A-1-c. Les "Anonymous types"
II-A-1-d. Les "Lambda expressions"
II-A-1-e. Les "Extension methods"
II-C. LINQ
III. Conclusion

Avant de commencer, j'aimerais préciser le but de cet article. Ce n'est en aucun cas de présenter C# 3.0 de façon exhaustive mais uniquement de faire une analyse de ce qu'il est possible de faire et ce qu'il n'est pas possible de faire avec la CTP de janvier 2007.


I. Orcas

Pour ceux qui l'ignorent, Orcas est le nom de code de la prochaine version de Visual Studio (prévue pour fin 2007 et aujourd'hui disponible en CTP). Celle-ci ajoutera entre autres le support du framework 3.0 (avec WPF, WCF, WF et WCS), C# 3.0 et LINQ ainsi que le développement de plugins pour Office 2007.
Pour du code C# 3.0, il est difficile de faire la différence entre la CTP de janvier 2007 d'Orcas et VS 2005. A noter cependant, l'ajout de l'intellisence JavaScript. Dans cet article, je vais présenter les nouveautés de C#3.0 et notamment de LINQ qui propose des améliorations plus qu'intéressantes. Quelques éléments ne sont pas encore présents dans cette version :

  • Les static virtual / abstract / override n'existent toujours pas
  • Une classe statique ne peut pas hériter d'une autre classe static
  • Un enum ne peut pas hériter d'un autre enum
  • Si une Form est abstract, ses dérivés ne sont pas visibles en mode design
  • Les Controls ou UserControls apparaissent dans la toolbox en mode design. Quand il y en a beaucoup dans notre solution, le temps de chargement de la toolbox devient vite très long et surtout, le fait qu'il faille attendre que la toolbox soit entièrement remplie pour pouvoir faire une modification, quelle qu'elle soit, sur une Form (ou UserControl) en mode design est particulièrement pénible. Pourquoi ne pas avoir mis le chargement de la Toolbox dans un Thread à part ? Et surtout, pourquoi cette régression ? En effet, ce n'était pas le cas avec Visual Studio 2003.
Ne soyons pas négatif, Orcas amène un lot de nouveautés non négligeable.


II. C# 3.0

Je vais présenter quelques nouveautés de C# 3.0 et LINQ notamment.

Pour commencer, voici une classe Person :

accompagnée d'un IEnumerable<Person>. En l'occurrence, un tableau.

Pour récupérer la liste des Person de l'IEnumerable dont le nom est égal à "MEZIL". (voir ProjetTest dans les sources)


II-A. Solution C# 2.0

De façon générique en C# 2.0, cela donne :
public class GenericComparer<Type> : IComparer<Type>
{
    private string _propertyName;

    public GenericComparer(string propertyName)
    {
        _propertyName = propertyName;
    }

    #region IComparer Members
    public int Compare(Type x, Type y)
    {
        return new CaseInsensitiveComparer().Compare(typeof(Type).GetProperty(_propertyName).GetValue(x, null),
            typeof(Type).GetProperty(_propertyName).GetValue(y, null));
    }
    #endregion
} 
public static class LinqSimplifieALaMatthieuCSharpV2<TypeIn, TypeOut> where TypeOut : new()
{
    public static IEnumerable<TypeOut> Select(IEnumerable<TypeIn> list, string[] propertiesName, WhereDelegate whereMethod) 
    {
        foreach (TypeIn element in list)
            if (whereMethod == null || whereMethod(element))
            {
                TypeOut result = new TypeOut();
                foreach (string propertyName in propertiesName)
                    typeof(TypeOut).GetProperty(propertyName).SetValue(result, 
						typeof(TypeIn).GetProperty(propertyName).GetValue(element, null), null);
                yield return result;
            }
    }

    public static IEnumerable<TypeOut> OrderBy(IEnumerable<TypeOut> list, string propertyName)
    {
        List<TypeOut> listToSort = new List<TypeOut>();
        foreach (TypeOut element in list)
            listToSort.Add(element);
        listToSort.Sort(new GenericComparer<TypeOut>(propertyName));
        foreach (TypeOut element in listToSort)
            yield return element;
    }

    public delegate bool WhereDelegate(TypeIn element);
}
Et pour utiliser le tout, quelques lignes suffisent :
IEnumerable<PersonView> mezilFamily = LinqSimplifieALaMatthieuCSharpV2<Person, PersonView>.OrderBy(
    LinqSimplifieALaMatthieuCSharpV2<Person, PersonView>.Select(persons, new string[] { "FirstName", "BirthDay" },  
    delegate(Person person){ return person.LastName.ToUpper() == "MEZIL";}), "FirstName");
Intéressant, mais nécessite l'écriture de la classe PersonView :


II-A-1. Les nouveautés de C# 3.0


II-A-1-a. Les "Object initializers"

Il est fréquent d'écrire des parties de code sous cette forme :
Person p = new Person() ;
p.LastName = "MEZIL" ;
p.FirstName = "Matthieu" ;
p.BirthDay = new DateTime(1981, 11, 18) ;
Dans ce cas précis, il vaudrait mieux écrire un deuxième constructeur mais, dans un cas où il y aurait beaucoup de propriétés et non obligatoires, il faudra affecter propriété par propriété.

En C# 3.0, il est possible d'écrire
Person p = new Person()  {LastName = "MEZIL", FirstName = "Matthieu", BirthDay = new DateTime(1981, 11, 18) };
Vu qu'on a utilisé le constructeur sans paramètre, il est même possible de se passer des parenthèses :
Person p = new Person {LastName = "MEZIL", FirstName = "Matthieu", BirthDay = new DateTime(1981, 11, 18) };
En revanche, il n'est toujours pas possible d'écrire ceci :
Person p = new Person;
Pour info,
Person p = new Person();
pourra être remplacé par
Person p = new Person{};
mais cela n'apporte rien bien entendu.

Il est important de noter que l'IL reste identique à celui généré avec C# 2.0.

Les "Object initializers" permettent également de gagner des lignes de code sur les List. En effet, en C# 2.0, il est possible d'initialiser un tableau à la déclaration :
string[] strings = new string[] { "a", "b", "c", "d" };
mais cela n'était valable que pour les tableaux. Ce n'était par exemple pas valable pour les List. Il fallait donc passer par un Add ou AddRange. Maintenant, cette écriture est également possible pour les Lists :
List<string> strings = new List<string> { "a", "b", "c", "d" };
Pour les tableaux, la double déclaration du type peut être supprimée rendant l'écriture de la ligne suivante parfaitement valable :
string[] strings = { "a", "b", "c", "d" };

II-A-1-b. Les "Local variable type inference"

Pour instancier un objet en 2.0, il faut préciser deux fois le type :
Person p = new Person();
Dans ce cas, ça va car Person ne comporte que 6 caractères mais si le nom de ma classe comportait une trentaine de caractère, cela peut vite prendre beaucoup de place et rallonger les lignes de code de manière significative.

C# 3.0 introduit un nouveau mot clé (" var "). Il est maintenant possible d'écrire :
var p = new Person();
L'IL compilé est exactement le même.

Dans le cas suivant :
IEnumerable<Person> persons = MyClass.GetPersons();
le mot clé var va nous permettre de ne pas s'occuper du type retourné par la méthode :
var persons = MyClass.GetPersons();
Le type de persons sera alors le type retourné par MyClass.GetPersons().

Certains trouvent qu'on perd en lisibilité, je ne suis pas d'accord, d'autant que l'intellisence continue de mettre le ToolTip "Person p" ou "IEnumerable<Person>" (suivant l'exemple).

Personnellement, je trouve ça bien pratique car ça raccourcit les lignes de codes.

J'aimerais même aller encore plus loin dans l'allégement du code. En effet, j'aimerais pouvoir transformer l'écriture du code suivant :
var persons = new Person[] { new Person { LastName = "MEZIL", FirstName = "Matthieu" }};
en ceci :
var persons = new Person[] { new { LastName = "MEZIL", FirstName = "Matthieu" }};
En effet, vu que j'affecte des éléments de mon tableau de Person, le compilateur devrait pouvoir déterminer que je vais créer par défaut des Person. Cela impliquerait pouvoir écrire un "var" inversé :
Person p = new ;
Même si cela devenait possible, le mot clé "var" garderait son intérêt pour une affectation autre qu'une instanciation.

L'utilisation du mot clé var n'est possible que lors de l'affectation immédiate.

Nous allons voir maintenant que le vrai intérêt des "local variable type inference" se trouve dans les "anonymous types".


II-A-1-c. Les "Anonymous types"

Les anonymous type sont des types automatiquement générés à la compilation. Là, effectivement, ça devient moins lisible.
var generatedObject = new { LastName = "MEZIL", FirstName = "Matthieu", BirthDay = new DateTime(1981, 11, 18) };
Le type de generatedObject a un nom bien particulier (commençant par "<>" pour qu'il ne soit pas possible de créer un type portant le même nom). Dans mon exemple, ce type est une classe qui a trois propriétés : LastName, FirstName et BirthDay, il est donc possible d'écrire generatedObject.LastName. L'intellisence ne peut bien entendu pas donner le nom du type de generatedObject mais il proposera en revanche les différentes propriétés après avoir tapé "generatedObject.".

Dans l'exemple suivant :
var generatedObject = new { LastName = "MEZIL", FirstName = "Matthieu", BirthDay = new DateTime(1981, 11, 18) };
var generatedObject2 = new { LastName = "MEZIL", FirstName = "Matthieu", BirthDay = new DateTime(1981, 11, 18) };
generatedObject et generatedObject2 sont du même type.

En revanche, si l'ordre des paramètres est différent, le type ne sera plus le même. Cela évoluera peut-être d'ici la version finale.

Vous ne pouvez pas renvoyer un type anonyme dans une méthode à moins de passer par object pour cela.
private object Test()
{
  return new { LastName = "MEZIL", FirstName = "Matthieu", BirthDay = new DateTime(1981, 11, 18) };
}
Il sera alors possible d'utiliser la réflexion pour récupérer les valeurs des propriétés de l'objet renvoyé par Test. Cependant, je ne recommande pas l'utilisation de types anonymes dans ce cas là. En effet, comme nous venons de le voir, la classe (et donc ses propriétés) est créée à la volée. Cela implique que si je change LastName en LastName2 par exemple, je ne vais pas avoir la fonctionnalité de Visual Studio Rename par exemple. (Rassurez-vous, il y a quand même une erreur à la compilation). Par contre, passer par réflexion pour récupérer la valeur d'une propriété implique de passer le nom de la propriété en tant que chaine de caractère. Il n'y aura donc pas d'erreur de compilation, l'erreur se produira à l'exécution. Autant j'aime beaucoup la réflexion, autant, je ne suis pas sûr que l'économie d'une classe comportant uniquement des propriétés soit intéressante au vu du temps perdu par ailleurs pour accéder à ces propriétés. Cela va alourdir le code donc je le déconseille.

En revanche, dans une utilisation locale à une méthode, cela peut s'avérer très pratique.
Leur utilisation est particulèrement utile avec Linq.

Par contre, j'aurais apprécié pouvoir écrire cela :
var result = new { FirstName = "Matthieu"};
var myList = List<result.GetType()>();
Malheureusement, ce n'est, à ma connaissance, pour l'instant pas encore possible.

J'aurais également aimé pouvoir écrire ceci :
var myList = new List<> { myObject1, myObject2, myObject3 } ;
Le compilateur créerait automatiquement une liste dont le type serait le plus petit dénominateur commun entre tous les objets définis lors de l'affectation immédiate. Par exemple, dans le code suivant :
var test = new List<> { new ComboBox(), new TextBox(), new RichTextBox() };
test serait une List<Control>.
Et dans celui là :
var test = new List<> { new TextBox(), new RichTextBox() };
test serait une List<TextBoxBase>.

Un petit détail important, un type anonyme est bien une classe. Ce n'est pas une structure.

Bien entendu, il n'est pas possible de modifier la structure d'un type anonyme durant l'exécution en debug.


II-A-1-d. Les "Lambda expressions"

Pour passer un delegate à une method, il faut écrire le code suivant :
public delegate bool WhereDelegate(Person element);
public static IEnumerable<TypeOut> Select(IEnumerable<TypeIn> list, string[] propertiesName, WhereDelegate whereMethod){…}
LinqSimplifieALaMatthieuCSharpV2<Person, PersonView>.Select(persons, new string[] { "FirstName", "BirthDay" }, 
delegate(Person person){ return person.LastName.ToUpper() == "MEZIL";})
Avec C# 3.0, si le delegate ne fait qu'un return il est possible d'utiliser les Lambda expressions :
LinqSimplifieALaMatthieuCSharpV2<Person, PersonView>.Select(persons, new string[] { "FirstName", "BirthDay" }, 
(person) => person.LastName.ToUpper() == "MEZIL" )
Et comme on ne passe qu'un seul paramètre à notre délégué, on peut même écrire :
LinqSimplifieALaMatthieuCSharpV2<Person, PersonView>.Select(persons, new string[] { "FirstName", "BirthDay" }, 
person => person.LastName.ToUpper() == "MEZIL" )
Personnellement, j'aime beaucoup. Une fois de plus, cela simplifie et réduit le code.

J'ai l'impression qu'il n'est possible d'effectuer qu'une seule instruction (de type return dans un delegate classique) après les =>. Si c'est effectivement le cas, c'est dommage !


II-A-1-e. Les "Extension methods"

Pour créer une méthode qui s'appliquera à une Classe C sans modifier le code de C ni la dériver, il faut généralement passer par une méthode static :
public static <type de retour> MyMethod(C monObject, <autres paramètres>){…}
Là encore, C# 3.0 va permettre de raccourcir les lignes de code. En effet, il suffit de rajouter le mot clé "this" devant C :
public static <type de retour> MyMethod(this C monObject, <autres paramètres>){…}
Pour pouvoir faire :
C monObject = new C();
monObejct.MyMethod(<autres paramètres>);
Là aussi, l'IL est le même. Par conséquent, on ne pourra pas accéder aux méthodes private ni même protected de C dans MyMethod.

C'est également intéressant quand on veut ajouter une méthode sur plusieurs classes avec un code centralisé. Par exemple :
public static void  MyMethod(this IInterface1 i1) {…}
Cette méthode est valable pour toutes mes classes implémentant l'interface IInterface1.

Les "extension methods" ne sont valables que dans une méthode static, elle-même contenue dans une classe static non générique. Pour utiliser les generics avec les "extension methods", il faut les mettre sur les méthodes statics et non sur la classe.

Petit détail important (quoi qu'évident), si vous mettez votre méthode internal, seules les classes du namespace de votre classe static auront accès à cette méthode.

Le "Goto definition" amène dans la classe (C dans notre premier exemple) et non pas dans notre classe static. En revanche, l'intellisence intègre parfaitement cette nouvelle méthode. Il n'est cependant pas parfait : il répète le premier paramètre préfixé de this. Dans mon exemple MyMethod sur Class1, après "c1.MyMethod(", l'intellisence ne devrait nous proposer aucun paramètre mais, dans la CTP de janvier 2007, il met "this Class1 c1". C'est dommage et perturbant car cela donne l'impression qu'il faut passer un paramètre.

Une nouvelle icône a été créée pour les "extension methods" dans le designer afin de les différencier des méthodes "classiques".

Il est dommage de ne pas pouvoir, par le même procédé, rajouter une propriété. De toute façon, une propriété est convertie en méthode dans l'IL ("get_<nom de ma propriété>" et "set_<nom de ma propriété>"), donc on aimerait que :
public static int get_MyProperty (this Class1 c1)
donne accès à une propriété ReadOnly MyProperty sur la classe Class1.

Les puristes (même chez Microsoft, j'en connais :) "hurlent au scandale" car ils jugent que les "extension methods" ne respectent pas les concepts de l'objet. Je ne suis pas d'accord. Il faut garder à l'esprit que l'IL ne contourne pas les principes de l'objet et que les "extension methods" permettent surtout de simplifier la vie du développeur en lui faisant gagner du temps en développement.

De plus, je vais même aller plus loin. La centralisation du code, pour rajouter des méthodes sur une interface mais en le codant une seule fois grâce aux "extension methods", est à mes yeux presque aussi intéressante que l'apport des generics par la version 2.0.

Un cas concret pour lequel les "extension methods" sont super utiles : Pour faire un Copy sur une DataTable typée, le résultat est un DataTable qu'il va falloir caster en notre type de DataTable. Grâce aux "extension methods", il est désormais possible de récupérer directement une table typée :
public static class ExtensionMethods
{
  public static DataTableType CopyKeepingType<DataTableType>(this DataTableType table) 
    where DataTableType : DataTable, new()
  {
    DataTableType newTable = new DataTableType();
    foreach (DataRow row in table.Rows)
    {
        DataRow newRow = newTable.NewRow();
        foreach (DataColumn column in table.Columns)
            newRow[column.ColumnName] = row[column];
        newTable.Rows.Add(newRow);
    }
    return newTable;
  }
}
Pour l'utiliser, il suffit de faire :
DataSet1.DataTable1DataTable copyTable = table.CopyKeepingType<DataSet1.DataTable1DataTable>();
C'est pas mal mais il est regrettable de devoir mettre le <DataSet1.DataTable1DataTable>. En fait, il n'est pas obligatoire. En effet, vu que le premier paramètre de notre méthode static est de type DataTableType, le compilateur n'a pas besoin qu'on le lui rappelle. Aussi, il est possible d'écrire directement ceci :
DataSet1.DataTable1DataTable copyTable = table.CopyKeepingType();
Et là, ça commence à être vraiment sympa !

Par contre, l'intellisence a un gros défaut : il ne regarde pas le where dans le cas d'une "extension method" générique. Par conséquent, il va proposer l'"extension method" CopyKeepingType sur un type qui n'est pas une DataTable.
Là aussi, il est fort probable que cela soit corrigé dans la version finale.

Un cas particulièrement intéressant des extension methods c'est qu'il va désormais être possible d'avoir un pseudo héritage multiple en jouant avec les extension methods sur les interface.
interface I1
{
}

interface I2
{
}

interface I3 : I1
{
}

class C1 : I1, I2
{
}

class C2 : I3
{
}

static class ExtensionMethods
{
    public static void WriteLine(this I1 i1)
    {
        Console.WriteLine("I1");
    }
    public static void WriteLine(this I2 i2)
    {
        Console.WriteLine("I2");
    }
    public static void WriteLine(this I3 i3)
    {
        Console.WriteLine("I3");
    }
}
L'héritage multiple implique le problème des deux classes filles avec la même méthode.
Ici, c'est un peu le même problème.
new C1().WriteLine()
génèrera une exception à la compilation.
new C2().WriteLine()
écrira "I3" dans la console, malgré le fait que C1 implémente aussi I1.

Petite précision, tout comme les délégués anonymes, il n'est pas possible de modifier le code d'une "extension method" à l'exécution en mode debug. Pour utiliser la méthode de I1, il faudra passer par un cast :
(new C2() as I1).WriteLine()
écrira "I1" dans la console.


II-C. LINQ

Avec LINQ, il est possible de requêter les IEnumerable.

Le but de LINQ est aussi d'unifier le requêtage, que ce soit sur les IEnumerable, les bases de données (DLINQ) ou le XML (XLINQ).

Avec LINQ, mon code se résume à une simple requête.
var mezilFamily =
    from p in persons
    where p.LastName.ToUpper() == "MEZIL"
    orderby p.FirstName
    select new { FirstName = p.FirstName, BirthDay = p.BirthDay };
Dans ce cas, je n'ai, en théorie, plus besoin de PersonView (en dehors du fait que j'ai surchargé le ToString() de PersonView).

Dans ce cas, mezilFamily est un IEnumerable d'une classe que je ne connais pas. Comment en faire une IList pour pouvoir l'affecter à un DataSource ? Grâce à la méthode ToList. Cette méthode est une "extension method" apporté par LINQ.

Bien entendu, il n'est pas obligatoire d'utiliser de type anonyme dans une requête LINQ. Il est donc possible de garder la classe PersonView et l'utiliser afin de bénéficier de ma surcharge de ToString :
IEnumerable<PersonView> mezilFamily =
    from p in persons
    where p.LastName.ToUpper() == "MEZIL"
    orderby p.FirstName
    select new PersonView { FirstName = p.FirstName, BirthDay = p.BirthDay };
Si vous faîtes une faute de frappe, le message d'erreur n'est franchement pas explicite. Par exemple, si je tape "p.Lastname" au lieu de "p.LastName", j'obtiens les deux erreurs suivantes :

  • The best overloaded method match for 'System.Linq.Enumerable.Where<Common.Person>(System.Collections.Generic.IEnumerable<Common.Person>, System.Linq.Func<Common.Person,bool>)' has some invalid arguments
  • Argument '2': cannot convert from 'lambda expression' to 'System.Linq.Func<Common.Person,bool>
Avant de comprendre que cette erreur est due à une faute de frappe, il est possible de passer un petit moment. En revanche, il y a peu de chance de faire une faute de frappe en utilisant l'intellisence.
De plus, autant pour les "extension methods", le message est très clair disant qu'on a besoin de System.Core ; autant, l'oublie de "using System.Linq" (jamais proposé), engendre une erreur à la compilation ('System.Array' does not contain a definition for 'Where') qui n'est pas franchement explicite non plus.

LINQ intègre de nouvelles classes, interface dans LINQ. Par exemple, vous trouverez l'interface IQueryable<T>, pour laquelle je vous conseille l'exemple de Mitsu.


III. Conclusion

Si certains appréhendent le C# 3.0, je suis au contraire plutôt enthousiaste. Pour les problèmes que j'ai relevés, rappelons que C# 3.0 est toujours en version Beta et que la version finale ne sortira qu'à la fin de l'année.

Le C# 3.0 permet une réduction significative du code que ce soit en nombre de lignes de code ou en nombre de caractères par ligne. Dans mon exemple, j'ai gagné 14 lignes de codes entre la version C# 2.0 et la version C# 3.0 et j'ai gagné 16 lignes entre la version C# 3.0 et la version LINQ, soit 30 lignes de code entre la version C#2.0 et la version LINQ pour un projet qui faisait à l'origine 45 lignes de codes écrites (j'ai donc réduit mon code de 2/3 !) et j'aurais pu faire encore mieux si je n'avais pas redéfini la méthode ToString de PersonView. Rappelons cependant que le but de ce projet était justement de présenter l'intérêt de LINQ. Ce rapport est donc largement supérieur à ce que vous aurez dans la "vraie vie".

En conclusion, les mauvaises langues tiendront le discours suivant : qu'y a-t-il de vraiment nouveau ? Les types anonymes dont l'utilisation n'est intéressante que dans des cas très précis et peu fréquents et LINQ. Pour tout le reste, ce ne sont que des simplifications plus ou moins intéressantes du code.

Cette vision des choses est bien sûr totalement fausse. Microsoft a utilisé massivement les "extension methods" pour rajouter une quantité impressionnante de nouvelles méthodes (47 sur les IEnumerable<> ! (avec le using de System.Linq)). Parmi ces méthodes, on trouve notamment Min, Max, Average, Union, Distinct, etc.

Microsoft aurait pu, à mon sens, allé encore plus loin comme je l'ai montré tout au long de cet article qui je l'espère (bien que ce soit un peu prétentieux de ma part), permettra d'aller plus loin dans la version finale.

En revanche, je regrette les quelques éléments évoqués dans l'introduction qui ne sont pas encore présents dans cette version.

Vivement la version finale ! ;-)

Pour plus de détails sur C# 3.0 et LINQ, je vous conseille l'article de Mitsu et l'article de Thomas Lebrun alias Morpheus. Je vous conseille également les webcasts de Mitsu.



            

Valid XHTML 1.1!Valid CSS!

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.