Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

C# 9.0 est disponible et apporte de nombreuses fonctionnalités et améliorations comme les expressions with
Ou encore les classes Record

Le , par Stéphane le calme

210PARTAGES

27  0 
Propriétés Init-only

Les initialiseurs d'objets sont assez impressionnants. Ils donnent au client d'un type un format très flexible et lisible pour créer un objet, et ils sont particulièrement parfaits pour la création d'objets imbriqués où toute une arborescence d'objets est créée en une seule fois. En voici une simple:

Code C# : Sélectionner tout
1
2
3
4
5
new Person 
{ 
    FirstName = "Scott", 
    LastName = "Hunter" 
}

La seule grande limitation aujourd'hui est que les propriétés doivent être modifiables pour que les initialiseurs d'objet fonctionnent: ils fonctionnent en appelant d'abord le constructeur de l'objet (par défaut, sans paramètre dans ce cas), puis en les affectant aux setters de propriété.

Les propriétés init-only corrigent cela! Elles introduisent un accesseur init qui est une variante de l'accesseur set qui ne peut être appelé que lors de l'initialisation de l'objet:

Code C# : Sélectionner tout
1
2
3
4
5
public class Person 
{ 
    public string FirstName { get; init; } 
    public string LastName { get; init; } 
}

Avec cette déclaration, le code client ci-dessus est toujours légal, mais toute affectation ultérieure aux propriétés FirstName et LastName est une erreur.

Accesseurs init et champs readonly

Étant donné que les accesseurs init ne peuvent être appelés que lors de l'initialisation, ils sont autorisés à muter les champs readonly de la classe englobante, tout comme vous le pouvez dans un constructeur.

Code C# : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person 
{ 
    private readonly string firstName; 
    private readonly string lastName; 
  
    public string FirstName  
    {  
        get => firstName;  
        init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName))); 
    } 
    public string LastName  
    {  
        get => lastName;  
        init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName))); 
    } 
}

Records

Les propriétés init-only sont idéales si vous souhaitez rendre les propriétés individuelles immuables. Si vous voulez que l'objet entier soit immuable et se comporte comme une valeur, alors vous devriez envisager de le déclarer comme un record (enregistrement) :

Code C# : Sélectionner tout
1
2
3
4
5
public data class Person 
{ 
    public string FirstName { get; init; } 
    public string LastName { get; init; } 
}

Le mot-clé data sur la déclaration de classe le marque comme un enregistrement. Cela lui confère plusieurs comportements similaires à des valeurs. De manière générale, les enregistrements sont censés être davantage considérés comme des « valeurs » (des données) et moins comme objets. Ils ne sont pas censés avoir un état encapsulé mutable. Au lieu de cela, vous représentez le changement au fil du temps en créant de nouveaux enregistrements représentant le nouvel état. Ils sont définis non pas par leur identité, mais par leur contenu.

Expressions with

Lorsque vous travaillez avec des données immuables, un modèle courant consiste à créer de nouvelles valeurs à partir de celles existantes pour représenter un nouvel état. Par exemple, si notre personne devait changer son nom de famille, nous le représenterions comme un nouvel objet qui est une copie de l'ancien, exception faite du nom de famille qui est différent. Cette technique est souvent appelée mutation non destructive. Au lieu de représenter la personne au fil du temps, l'enregistrement représente l'état de la personne à un moment donné.

Pour aider avec ce style de programmation, les enregistrements permettent un nouveau type d'expression; l'expression with :

Code C# : Sélectionner tout
var otherPerson = person with { LastName = "Hanselman" };

Les expressions with utilisent la syntaxe d'initialisation d'objet pour indiquer ce qui est différent dans le nouvel objet de l'ancien objet. Vous pouvez spécifier plusieurs propriétés.

Un enregistrement définit implicitement un « constructeur de copie » protected - un constructeur qui prend un objet d'enregistrement existant et le copie champ par champ dans le nouveau:

Code C# : Sélectionner tout
protected Person(Person original) { /* copy all the fields */ } // generated

L'expression with provoque l'appel du constructeur de copie, puis applique l'initialiseur d'objet en haut pour modifier les propriétés en conséquence.

Si vous n'aimez pas le comportement par défaut du constructeur de copie généré, vous pouvez définir le vôtre à la place, et celui-ci sera repris par l'expression with.

Source : Microsoft

Une erreur dans cette actualité ? Signalez-le nous !

Avatar de mermich
Membre expérimenté https://www.developpez.com
Le 21/05/2020 à 21:15
Ca me fait penser pas mal q certaines fonctionnalites des koltin, je dis n'importe quoi ou c'est bien le cas ?
1  0 
Avatar de codec_abc
Membre confirmé https://www.developpez.com
Le 21/05/2020 à 23:58
Je connais pas Kotlin mais l'inspiration vient surement de F# qui à ce genre de features depuis longtemps. C'est la stratégie de Microsoft depuis longtemps. F# sert en quelque sorte de Labo et les features sont ensuite intégré dans C#.
1  0 
Avatar de StringBuilder
Expert éminent https://www.developpez.com
Le 22/05/2020 à 14:25
Bonjour,

Y'a un truc que j'ai pas bien compris avec le "record" (mot clé data).

Du coup, c'est quoi la différence entre un "struct" et un "class data", mise à part que le "class data" est readonly ?

Pourquoi ne pas avoir créé un "struct readonly" à la place du coup ?

Transformer une déclaration de type référence en type valeur, alors qu'une déclaration de type valeur existe déjà, je suis perplexe !
1  0 
Avatar de zero_divide
Nouveau Candidat au Club https://www.developpez.com
Le 22/05/2020 à 19:08
Moi qui structure mon code selon le "Domain-driven design (DDD)", c’est évidemment pour représenter un Value Object.
1  0 
Avatar de StringBuilder
Expert éminent https://www.developpez.com
Le 22/05/2020 à 21:13
Citation Envoyé par zero_divide  Voir le message
Moi qui structure mon code selon le "Domain-driven design (DDD)", c’est évidemment pour représenter un Value Object.

Justement, j'ai un peu du mal à comprendre…

A la base, une instance d'objet, c'est une référence.
Donc un objet valeur c'est à la base plus ou moins antonyme.

C#, depuis la version 1.0 a coupé la poire en deux, en proposant les structures, qui n'ont rien à voir avec les structures qu'on peut trouver dans d'autres langages : ce sont des objets à part entière (ils héritent d'ailleurs du type objetc) et supportent méthodes et constructeurs... La seule grosse différence avec une classe, c'est d'être justement un type valeur.

Code csharp : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System; 
  
namespace StructOrObject 
{ 
    class Program 
    { 
        static void Main(string[] args) 
        { 
            MaStructure s1 = new MaStructure("Alfred", "E. Neuman"); 
            Console.WriteLine(s1.FullName); 
            Console.WriteLine(s1.NbNotEmptyChars()); 
  
            MaStructure s2 = new MaStructure() { FullName = "Alfred E. Neuman" }; 
            Console.WriteLine(s2.FullName); 
            Console.WriteLine(s2.NbNotEmptyChars()); 
  
            Console.WriteLine(s1.Equals(s2)); 
        } 
    } 
  
    struct MaStructure 
    { 
        public string FullName { get; set; } 
  
        public MaStructure(string firstName, string lastName) 
        { 
            FullName = $"{firstName} {lastName}"; 
        } 
  
        public int NbNotEmptyChars() 
        { 
            if (string.IsNullOrWhiteSpace(FullName)) 
            { 
                return 0; 
            } 
            else 
            { 
                return FullName.Replace(" ", "").Length; 
            } 
        } 
    } 
}

Du coup, un "Value Object", existe déjà dans .NET et s'appelle struct...

Le seul truc qui manque, c'est qu'on ne peut pas initialiser une proriété readonly directement avec un new MaStructure() { FullName = "Toto" } mais je ne vois pas ce qui empêchait de mettre "init" sur les structures aussi...
Dans quel cas un "class data" est-il différent d'un "struct" donc toutes les propriétés sont readonly ?

Bref, je pige vraiment pas.
1  0 
Avatar de Programmator
Membre régulier https://www.developpez.com
Le 23/05/2020 à 15:54
Citation Envoyé par StringBuilder Voir le message

Le seul truc qui manque, c'est qu'on ne peut pas initialiser une proriété readonly directement avec un new MaStructure() { FullName = "Toto" } mais je ne vois pas ce qui empêchait de mettre "init" sur les structures aussi...
Dans quel cas un "class data" est-il différent d'un "struct" donc toutes les propriétés sont readonly ?

Bref, je pige vraiment pas.
Les classes et les structures ont aussi 2 différences majeures :
  • Les instances de classes sont stockées dans le tas, qui est une zone mémoire large gérée par le ramasse-miettes. Alors que les instances de structures sont stockées sur la pile et détruites automatiquement lorsqu'on sort de la portée.
  • Les classes sont dérivables, alors que les structures ne le sont pas.


Les classes data restent des classes et continuent donc de profiter de ces 2 caractéristiques propres aux classes.
De plus, en lisant l'article original de Microsoft, je crois comprendre que les classes data ont besoin de l'héritage dans certains scénarios (cf. chapitre Value-based equality and inheritance)
1  0 
Avatar de StringBuilder
Expert éminent https://www.developpez.com
Le 23/05/2020 à 16:04
En effet, bien vu pour l'héritage. J'avais pourtant l'exemple sous les yeux, je n'y avais pas pensé.

Par contre, pour la partie gestion de la mémoire, j'ai envie de dire :
- on s'en moque un peu (après tout, c'est la sauce interne du Framework, ça peut tout à fait changer d'une version à l'autre, voir même d'une implémentation à l'autre)
- mais surtout, je ne vois pas l'intérêt de stocker des value objects dans une zone mémoire gérée par le GC. En effet, vu que ce sont des objets valeur, il ne peut y avoir qu'une seule référence (c'est con de manipuler une référence d'un type value... c'est un peu dommage d'utiliser un objet valeur pour n'en manipuler que la référence. Et très peu de chance/raison de recréer le même objet exactement, (et vérifier qu'il est identique à un détruit précédemment prendra de toute façon autant de temps que de le récupérer) sinon, pourquoi l'avoir détruit ?
1  0 
Avatar de François DORIN
Expert éminent sénior https://www.developpez.com
Le 24/05/2020 à 11:04
Citation Envoyé par StringBuilder Voir le message
Y'a un truc que j'ai pas bien compris avec le "record" (mot clé data).

Du coup, c'est quoi la différence entre un "struct" et un "class data", mise à part que le "class data" est readonly ?
A priori, un class data reste une classe. Donc passage par référence. Le passage en tant que paramètre dans une méthode ou la copie dans une variable serait donc bien rapide que dans le cas d'une struct, notamment lorsqu'il y a beaucoup d'attributs.

L'objectif c'est de donner un comportement de type "valeur" à une classe. L'égalité, par exemple, se base non plus sur les références, mais bien sur la valeur des attributs sous-jacents.

Citation Envoyé par Programmator

Les instances de classes sont stockées dans le tas, qui est une zone mémoire large gérée par le ramasse-miettes. Alors que les instances de structures sont stockées sur la pile et détruites automatiquement lorsqu'on sort de la portée.
Partiellement inexacte. Les types valeur peuvent être allouées sur le tas et non sur la pile dans certaines condicitions. Par exemple, en cas de boxing :
Code : Sélectionner tout
1
2
object monEntier = 3; // alloué sur le tas !
1  0 
Avatar de mermich
Membre expérimenté https://www.developpez.com
Le 25/05/2020 à 16:22
Dans le debat classe vs struct, il ya aussi les generiques :

Code : Sélectionner tout
public void Prout<T>(T t) where T : class
ce qui engrange des doublon si l'on doit a la fois gerer des classes et des structures...
Au final avec le temps je pousse mes collegues a ne pas utiliser les struct pour toutes ces raisons. Mais il reste ce soucis de mutabilite que l'on ne desire pas forcement.

Je me demande si ces data class fonctionnent correctement lors d'un post mvc, par exemple
1  0 
Avatar de Programmator
Membre régulier https://www.developpez.com
Le 26/05/2020 à 18:53
Citation Envoyé par François DORIN Voir le message
A priori, un class data reste une classe. Donc passage par référence. Le passage en tant que paramètre dans une méthode ou la copie dans une variable serait donc bien rapide que dans le cas d'une struct, notamment lorsqu'il y a beaucoup d'attributs.

L'objectif c'est de donner un comportement de type "valeur" à une classe. L'égalité, par exemple, se base non plus sur les références, mais bien sur la valeur des attributs sous-jacents.

Partiellement inexacte. Les types valeur peuvent être allouées sur le tas et non sur la pile dans certaines condicitions. Par exemple, en cas de boxing :
Code : Sélectionner tout
1
2
object monEntier = 3; // alloué sur le tas !
Je partage tout à fait ce que tu dis François concernant le comportement de type valeur. C'est d'ailleurs déjà ce qui existe avec le type string, qui se comporte comme un type valeur bien que ce soit un type référence.

Concernant l'allocation mémoire, même dans le boxing, la règle des types valeurs stockés sur la pile reste vraie. C'est un mécanisme que j'ai bien étudié pour faire mon cours C# et voici ce que j'ai compris :
3 est un type valeur qui est donc stocké sur la pile.
monEntier étant de type object, il doit obligatoirement référencer un emplacement du tas. S'il pointait sur un emplacement de la pile, cela créerait un défaut de sécurité potentiel. Par conséquent, le runtime réserve un bloc de mémoire sur le tas, y copie la valeur de l'entier 3, puis référence cette copie dans l’objet monEntier. Ceci est illustré par le schéma suivant :



La règle types valeurs stockés sur la pile et types références stockés sur le tas est ainsi toujours respectée.
1  0