Voici une présentation des éventuelles nouveautés du langage qui devraient apparaître dans la version 9, voir 10 en fonction des décisions de Microsoft sur sa roadmap et l’état d’avancement des fonctionnalités du langage que l’on retrouve ici
Où ?
Le langage C# 9 se retrouvera dans la version 5.0 du framework .NET (qui à l’heure où j’écris ce billet est en verison v5.0.100-preview.5.
J’ai volontairement garder les termes d’origine pour éviter de faire des traductions hasardeuses, l’original permettra facilement de savoir de quoi l’on parle par rapport aux présentation de l’équipe C#.
Avis
Pour ma part, j’utilise tout les jours C# personnellement et professionnellement, je trouve bien que des évolutions sur le langage (cela rassure aussi sur sa pérennité) se fassent. Mais, et oui il y a toujours un mais, je trouve que l’on simplifie vraiment trop certains concepts ce qui a un impact sur l’approche de la programmation orientée objet et ses fondamentaux. C# et ses concepteurs ont plus une approche DRY et cela me dérange. J’aimerai plutôt voir des concepts plus fondamentaux mise en place ou intégré au langage.
Car, et pour en avoir fait les frais, la facilité peut engendrer de la complexité dans certaines problématiques. Il est préférable de choisir la simplicité à la facilité KISS pour ma part.
“Mais je dis cela, je dis rien…”
Les propriétés “Init only”
Contexte
L’initialisation des objets à l’heure est du facilité et simplicité d’écriture très impressionnante surtout quand on doit initialiser une grappe d’objets. Mais, oui il y a un mais, on aimerait dans certains pouvoir rendre non mutable ces propriétés. Voici déjà un exemple de l’initialisation comme nous la connaissons à l’heure actuelle en C#.
Lors cette initialisation, le constructeur par défaut est appelé et ensuite les setter’s sont appelés pour faire l’initialisation avec la valeur.
Exemple initialisation “mutable”
using System.Collections.Generic; namespace SampleInitOnly { class Program { static void Main(string[] args) { Person person = new Person { FirstName = "John", LastName = "Doe", Address = new Address { City = "Strasburg", Country = "France", Number = 10, Addresses = new List<string> { "Address1", "Address2" } } }; } } public class Person { public string FirstName { get; set; } public string LastName { get; set; } public Address Address { get; set; } } public class Address { public int Number { get; set; } public List<string> Addresses { get; set; } public string City { get; set; } public string Country { get; set; } } }
Voici le code IL, nous pouvons voir qu’il y a bien l’appel au constructeur par défaut et ensuite l’appel aux setter’s.
// [9 13 - 20 15] IL_0001: newobj instance void SampleInitOnly.Person::.ctor() IL_0006: dup IL_0007: ldstr "John" IL_000c: callvirt instance void SampleInitOnly.Person::set_FirstName(string) IL_0011: nop IL_0012: dup IL_0013: ldstr "Doe" IL_0018: callvirt instance void SampleInitOnly.Person::set_LastName(string) IL_001d: nop IL_001e: dup IL_001f: newobj instance void SampleInitOnly.Address::.ctor() IL_0024: dup IL_0025: ldstr "Strasburg" IL_002a: callvirt instance void SampleInitOnly.Address::set_City(string) IL_002f: nop IL_0030: dup IL_0031: ldstr "France" IL_0036: callvirt instance void SampleInitOnly.Address::set_Country(string) IL_003b: nop IL_003c: dup IL_003d: ldc.i4.s 10 // 0x0a IL_003f: callvirt instance void SampleInitOnly.Address::set_Number(int32) IL_0044: nop IL_0045: dup IL_0046: newobj instance void class [System.Collections]System.Collections.Generic.List`1<string>::.ctor() IL_004b: dup IL_004c: ldstr "Address1" IL_0051: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<string>::Add(!0/*string*/) IL_0056: nop IL_0057: dup IL_0058: ldstr "Address2" IL_005d: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<string>::Add(!0/*string*/) IL_0062: nop IL_0063: callvirt instance void SampleInitOnly.Address::set_Addresses(class [System.Collections]System.Collections.Generic.List`1<string>) IL_0068: nop IL_0069: callvirt instance void SampleInitOnly.Person::set_Address(class SampleInitOnly.Address) IL_006e: nop IL_006f: stloc.0 // person
Objectif
Avoir la possibilité lors de l’initialisation, uniquement, de l’objet de pouvoir fixer des valeurs.
public class Person { public string FirstName { get; init; } public string LastName { get; init; } public Address Address { get; init; } }
Dans l’exemple présenté ci-dessus, on pourra donc lors de l’initialisation fixer des valeurs, toutes modifications ultérieures de celles-ci engendrera une erreur. Donc pour effectuer une initialisation on écrira donc
Person person = new Person() { FirstName = "John", LastName = "Doe" };
En détails
L’accesseur init ne sera que pour les propriétés et les indexeurs. Il ne servira quand lieu et place du set. On ne pourra affecter une valeur que lors des moments suivants (qui sont considérés comme étant la phase de construction de l’objet)
- Lors de l’initialisation de l’objet ;
- Lors de l’initialisation avec l’aide de l’expression with ;
- Lors de l’utilisation d’un attribut et en nommant le paramètre ;
- Lors de la construction de l’objet ou d’un type dérivé par l’utilisation de base ou this ;
- Lors d’un autre init d’un accesseur d’un propriétés, se trouvant dans l’objet ou d’un type dérivé par l’utilisation de base ou this ;
- L’utilisation de init ne peut se faire que sur des propriétés d’instance ;
- Pas de propriétés avec init et un set ;
- Dès qu’une propriétés est marquée avec init les dérivées doivent en hériter aussi, même principe dans le cas d’interfaces.
Si nous reprenons notre exemple, l’affectation comme suit, provoquera une erreur :
Person person = new Person() { FirstName = "John", LastName = "Doe" }; person.LastName = "Lennon"; // Une erreur car à ce moment là la propriété n'est plus 'Settable'
Au vue des règles énoncés juste avant, on pourra avoir l’exemple suivant :
namespace SampleInitOnly { class Program { static void Main(string[] args) { Employee person = new Employee() { LastName = "Doe" }; } } public class Person { public string FirstName { get; init; } public string LastName { get; init; } } public class Employee : Person { public Employee() { FirstName = "John"; LastName = "Doe"; } public int RegistrationNumber { get; set; } } }
Utilisation avec les champs readonly
Puisque l’accesseur init se déroule pendant la phase d’initialisation, il peut sans problème modifier une propriété qui se trouve être non mutable avec le mot clé readonly, comme dans l’exemple suivant
public class Person { private readonly string firstName; private readonly string lastName; public string FirstName { get => firstName; init => firstName = (value ?? throw new ArgumentException(nameof(FirstName))); } public string LastName { get => lastName; init => lastName = (value ?? throw new ArgumentException(nameof(LastName))); } }
Utilisation avec les interfaces
Il est possible aussi d’utiliser l’init dans les interfaces. Mais en utilisant un pattern précis, comme dans l’exemple suivant
interface IPerson { string Name { get; init; } } class Init { void M<T>() where T : IPerson, new() { var local = new T() { Name = "Jared" // Uniquement à ce moment. }; } }
Remarques
Comme l’accesseur init est invoqué pendant la phase de construction, il peut effectuer les mêmes actions qu’un set classique mais en plus :
- Appeler un autre accesseur init au travers de base ou this ;
- Assigner des propriétés en readonly pour le même type au travers de this ;
Contraintes d’utilisation
L'affectation de champs readonly par init ne peux que se faire qu'au même niveau c'est-à-dire pour le même type, impossible pour modifier un readonly d'un type parent. Cela permet que l'objet parent garde toute son intégrités car il peut être issue d'un autre auteur.
Lorsqu'un accesseur init est marqué par virtual toutes les classes dérivés devront avoir un override en init.
Inversement il n'est pas possible de marquer avec init une propriété parente marquée avec set.
Ajouer un commentaire