Expression « with » : une mutation non destructive qui crée un objet avec des propriétés modifiées

Une expression with produit une copie de son opérande avec les propriétés et champs spécifiés modifiés. Vous utilisez la syntaxe de l’initialiseur d’objet pour spécifier les membres à modifier ainsi que leurs nouvelles valeurs :

using System;

public class WithExpressionBasicExample
{
    public record NamedPoint(string Name, int X, int Y);

    public static void Main()
    {
        var p1 = new NamedPoint("A", 0, 0);
        Console.WriteLine($"{nameof(p1)}: {p1}");  // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }
        
        var p2 = p1 with { Name = "B", X = 5 };
        Console.WriteLine($"{nameof(p2)}: {p2}");  // output: p2: NamedPoint { Name = B, X = 5, Y = 0 }
        
        var p3 = p1 with 
            { 
                Name = "C", 
                Y = 4 
            };
        Console.WriteLine($"{nameof(p3)}: {p3}");  // output: p3: NamedPoint { Name = C, X = 0, Y = 4 }

        Console.WriteLine($"{nameof(p1)}: {p1}");  // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }

        var apples = new { Item = "Apples", Price = 1.19m };
        Console.WriteLine($"Original: {apples}");  // output: Original: { Item = Apples, Price = 1.19 }
        var saleApples = apples with { Price = 0.79m };
        Console.WriteLine($"Sale: {saleApples}");  // output: Sale: { Item = Apples, Price = 0.79 }
    }
}

L’opérande de gauche d’une expression with peut être d’un type d’enregistrement. À compter de C# 10, un opérande de partie gauche d’une expression with peut également être d’un type de structure ou d’un type anonyme.

Le résultat d’une expression with a le même type d’exécution que l’opérande de l’expression, comme l’illustre l’exemple suivant :

using System;

public class InheritanceExample
{
    public record Point(int X, int Y);
    public record NamedPoint(string Name, int X, int Y) : Point(X, Y);

    public static void Main()
    {
        Point p1 = new NamedPoint("A", 0, 0);
        Point p2 = p1 with { X = 5, Y = 3 };
        Console.WriteLine(p2 is NamedPoint);  // output: True
        Console.WriteLine(p2);  // output: NamedPoint { X = 5, Y = 3, Name = A }
    }
}

Dans le cas d’un membre de type référence, seule la référence à une instance de membre est copiée lorsqu’un opérande est copié. La copie et l’opérande d’origine ont accès à la même instance de type référence. L’exemple suivant illustre ce comportement :

using System;
using System.Collections.Generic;

public class ExampleWithReferenceType
{
    public record TaggedNumber(int Number, List<string> Tags)
    {
        public string PrintTags() => string.Join(", ", Tags);
    }

    public static void Main()
    {
        var original = new TaggedNumber(1, new List<string> { "A", "B" });

        var copy = original with { Number = 2 };
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B

        original.Tags.Add("C");
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B, C
    }
}

Sémantique de copie personnalisée

Tout type de classe d’enregistrement a le constructeur de copie. Un constructeur de copie est un constructeur avec un seul paramètre du type d’enregistrement conteneur. Il copie l’état de son argument dans une nouvelle instance d’enregistrement. Lors de l’évaluation d’une expression with, le constructeur de copie est appelé pour instancier une nouvelle instance d’enregistrement basée sur un enregistrement d’origine. Après cela, la nouvelle instance est mise à jour en fonction des modifications spécifiées. Par défaut, le constructeur de copie est implicite, c’est-à-dire généré par le compilateur. Si vous voulez personnaliser la sémantique de copie d’enregistrement, déclarez explicitement un constructeur de copie avec le comportement souhaité. L’exemple suivant met à jour l’exemple précédent avec un constructeur de copie explicite. Le nouveau comportement de copie consiste à copier des éléments de liste au lieu d’une référence de liste lorsqu’un enregistrement est copié :

using System;
using System.Collections.Generic;

public class UserDefinedCopyConstructorExample
{
    public record TaggedNumber(int Number, List<string> Tags)
    {
        protected TaggedNumber(TaggedNumber original)
        {
            Number = original.Number;
            Tags = new List<string>(original.Tags);
        }

        public string PrintTags() => string.Join(", ", Tags);
    }

    public static void Main()
    {
        var original = new TaggedNumber(1, new List<string> { "A", "B" });

        var copy = original with { Number = 2 };
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B

        original.Tags.Add("C");
        Console.WriteLine($"Tags of {nameof(copy)}: {copy.PrintTags()}");
        // output: Tags of copy: A, B
    }
}

Vous ne pouvez pas personnaliser la sémantique de copie pour les types de structure.

spécification du langage C#

Pour plus d’informations, consultez les sections suivantes de la note de proposition de fonctionnalité d’enregistrements :

Voir aussi