C# 9 – Les ‘Init Only’

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

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.

Frédéric Schmidt

Lead Technical Architect.

Ajouer un commentaire

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Recent Comments

    %d blogueurs aiment cette page :