28 votes

Qu'est-ce qu'une approche (im)mutabilité "presque complète" pour C# ?

Puisque l'immutabilité n'est pas complètement intégrée dans C# comme elle l'est dans F#, ou complètement dans le framework (BCL) malgré un certain support dans le CLR, quelle est une solution assez complète pour l'(im)mutabilité pour C# ?

Ma préférence va à une solution composée de modèles/principes généraux compatibles avec

  • une seule bibliothèque open-source avec peu de dépendances
  • un petit nombre de bibliothèques open-source complémentaires/compatibles
  • quelque chose de commercial

que

  • couvre les types d'activités de Lippert immutabilité
  • offre des performances décentes (je sais que c'est vague)
  • supporte la sérialisation
  • prend en charge le clonage/la copie (profonde, superficielle, partielle ?)
  • semble naturel dans des scénarios tels que DDD, builder patterns, configuration et threading
  • fournit des collections immuables

J'aimerais également inclure des modèles que vous, en tant que communauté, pourriez proposer et qui ne s'inscrivent pas exactement dans un cadre tel que l'expression de l'intention de mutabilité par le biais d'interfaces (où les deux clients qui ne devrait pas changer quelque chose et peut vouloir modifier quelque chose ne peut se faire que par le biais d'interfaces, et non de la classe d'appui (oui, je sais que ce n'est pas une véritable immutabilité, mais c'est suffisant) :

public interface IX
{
    int Y{ get; }
    ReadOnlyCollection<string> Z { get; }
    IMutableX Clone();
}

public interface IMutableX: IX
{
    new int Y{ get; set; }
    new ICollection<string> Z{ get; } // or IList<string>
}

// generally no one should get ahold of an X directly
internal class X: IMutableX
{
    public int Y{ get; set; }

    ICollection<string> IMutableX.Z { get { return z; } }

    public ReadOnlyCollection<string> Z
    {
        get { return new ReadOnlyCollection<string>(z); }
    }

    public IMutableX Clone()
    {
        var c = MemberwiseClone();
        c.z = new List<string>(z);
        return c;
    }

    private IList<string> z = new List<string>();       
}

// ...

public void ContriveExample(IX x)
{
    if (x.Y != 3 || x.Z.Count < 10) return;
    var c= x.Clone();
    c.Y++;
    c.Z.Clear();
    c.Z.Add("Bye, off to another thread");
    // ...
}

5voto

Chris Marisic Points 11495

La meilleure solution ne serait-elle pas d'utiliser F# là où l'on souhaite une véritable immutabilité ?

3voto

James Dunne Points 1602

Utiliser cette Modèle T4 que j'ai mis en place pour résoudre ce problème. Il devrait généralement répondre à vos besoins pour tous les types d'objets immuables que vous avez besoin de créer.

Il n'est pas nécessaire d'utiliser des génériques ou des interfaces. Dans mon cas, je ne veux pas que mes classes immuables soient convertibles les unes avec les autres. Pourquoi le feriez-vous ? Quels sont les traits communs qu'elles devraient partager pour être convertibles les unes avec les autres ? L'application d'un modèle de code devrait être le travail d'un générateur de code (ou mieux encore, d'un système de types suffisamment agréable pour vous permettre de définir des modèles de code généraux, ce que C# n'a malheureusement pas).

Voici un exemple de sortie du modèle pour illustrer le concept de base en jeu (sans parler des types utilisés pour les propriétés) :

public sealed partial class CommitPartial
{
    public CommitID ID { get; private set; }
    public TreeID TreeID { get; private set; }
    public string Committer { get; private set; }
    public DateTimeOffset DateCommitted { get; private set; }
    public string Message { get; private set; }

    public CommitPartial(Builder b)
    {
        this.ID = b.ID;
        this.TreeID = b.TreeID;
        this.Committer = b.Committer;
        this.DateCommitted = b.DateCommitted;
        this.Message = b.Message;
    }

    public sealed class Builder
    {
        public CommitID ID { get; set; }
        public TreeID TreeID { get; set; }
        public string Committer { get; set; }
        public DateTimeOffset DateCommitted { get; set; }
        public string Message { get; set; }

        public Builder() { }

        public Builder(CommitPartial imm)
        {
            this.ID = imm.ID;
            this.TreeID = imm.TreeID;
            this.Committer = imm.Committer;
            this.DateCommitted = imm.DateCommitted;
            this.Message = imm.Message;
        }

        public Builder(
            CommitID pID
           ,TreeID pTreeID
           ,string pCommitter
           ,DateTimeOffset pDateCommitted
           ,string pMessage
        )
        {
            this.ID = pID;
            this.TreeID = pTreeID;
            this.Committer = pCommitter;
            this.DateCommitted = pDateCommitted;
            this.Message = pMessage;
        }
    }

    public static implicit operator CommitPartial(Builder b)
    {
        return new CommitPartial(b);
    }
}

Le modèle de base consiste à avoir une classe immuable avec une classe mutable imbriquée. Builder qui est utilisée pour construire des instances de la classe immuable de manière mutable. La seule façon de définir les propriétés de la classe immuable est de construire une classe ImmutableType.Builder et la définir de manière mutable normale, puis la convertir en son contenu ImmutableType avec un opérateur de conversion implicite.

Vous pouvez étendre le modèle T4 pour ajouter un ctor public par défaut à l'élément ImmutableType elle-même, ce qui permet d'éviter une double allocation si l'on peut définir toutes les propriétés à l'avance.

Voici un exemple d'utilisation :

CommitPartial cp = new CommitPartial.Builder() { Message = "Hello", OtherFields = value, ... };

ou...

CommitPartial.Builder cpb = new CommitPartial.Builder();
cpb.Message = "Hello";
...
// using the implicit conversion operator:
CommitPartial cp = cpb;
// alternatively, using an explicit cast to invoke the conversion operator:
CommitPartial cp = (CommitPartial)cpb;

Notez que l'opérateur de conversion implicite de CommitPartial.Builder a CommitPartial est utilisé dans l'affectation. C'est la partie qui "gèle" le mutable CommitPartial.Builder en construisant un nouveau CommitPartial de l'instance avec la sémantique normale de la copie.

2voto

Erik Dietrich Points 3646

Personnellement, je n'ai pas vraiment connaissance de solutions tierces ou antérieures à ce problème, donc je m'excuse si j'aborde de vieux sujets. Mais si je devais mettre en œuvre une sorte de norme d'immutabilité pour un projet sur lequel je travaille, je commencerais par quelque chose comme ça :

public interface ISnaphot<T>
{
    T TakeSnapshot();
}

public class Immutable<T> where T : ISnaphot<T>
{
    private readonly T _item;
    public T Copy { get { return _item.TakeSnapshot(); } }

    public Immutable(T item)
    {
        _item = item.TakeSnapshot();
    }
}

Cette interface serait mise en œuvre de la manière suivante

public class Customer : ISnaphot<Customer>
{
    public string Name { get; set; }
    private List<string> _creditCardNumbers = new List<string>();
    public List<string> CreditCardNumbers { get { return _creditCardNumbers; } set { _creditCardNumbers = value; } }

    public Customer TakeSnapshot()
    {
        return new Customer() { Name = this.Name, CreditCardNumbers = new List<string>(this.CreditCardNumbers) };
    }
}

Et le code client serait quelque chose comme :

    public void Example()
    {
        var myCustomer = new Customer() { Name = "Erik";}
        var myImmutableCustomer = new Immutable<Customer>(myCustomer);
        myCustomer.Name = null;
        myCustomer.CreditCardNumbers = null;

        //These guys do not throw exceptions
        Console.WriteLine(myImmutableCustomer.Copy.Name.Length);
        Console.WriteLine("Credit card count: " + myImmutableCustomer.Copy.CreditCardNumbers.Count);
    }

Le défaut le plus flagrant est que la qualité de l'implémentation dépend de la qualité du client de ISnapshot La mise en œuvre par l TakeSnapshot mais au moins, cela normaliserait les choses et vous sauriez où chercher si vous avez des problèmes liés à une mutabilité douteuse. Il incomberait également aux implémenteurs potentiels de reconnaître s'ils peuvent ou non fournir une immutabilité instantanée et de ne pas implémenter l'interface, si ce n'est pas le cas (c'est-à-dire que la classe renvoie une référence à un champ qui ne supporte aucun type de clonage/copie et ne peut donc pas faire l'objet d'un cliché instantané).

Comme je l'ai dit, il s'agit d'un début - comme je commencerais probablement - et certainement pas d'une solution optimale ou d'une idée finie et peaufinée. À partir de là, je verrais comment mon utilisation évolue et je modifierais cette approche en conséquence. Mais, au moins ici, je saurais que je peux définir comment rendre quelque chose immuable et écrire des tests unitaires pour m'assurer que c'est le cas.

Je me rends compte que cela n'est pas très éloigné de la simple mise en œuvre d'une copie d'objet, mais cela standardise la copie par rapport à l'immutabilité. Dans une base de code, vous pourriez voir des implémenteurs de ICloneable Il s'agit de définir des constructeurs de copie et des méthodes de copie explicites, peut-être même dans la même classe. Définir quelque chose comme cela vous indique que l'intention est spécifiquement liée à l'immutabilité - je veux un instantané plutôt qu'un objet dupliqué parce qu'il se trouve que je veux n plus de cet objet. Les Immtuable<T> Si, par la suite, vous souhaitez optimiser d'une manière ou d'une autre, par exemple mettre en cache l'instantané jusqu'à ce qu'il soit sale, vous n'avez pas besoin de le faire dans tous les implémenteurs de la logique de copie.

1voto

supercat Points 25534

Si l'objectif est d'avoir des objets qui se comportent comme des objets mutables non partagés, mais qui peuvent être partagés lorsque cela améliore l'efficacité, je suggérerais d'avoir un type de "données fondamentales" privé et mutable. Bien que toute personne détenant une référence à des objets de ce type soit en mesure de les modifier, aucune référence de ce type ne pourra jamais s'échapper de l'assemblée. Toutes les manipulations extérieures des données doivent être effectuées par l'intermédiaire d'objets enveloppants, chacun d'entre eux contenant deux références :

  1. UnsharedVersion--Détient la seule référence existante à son objet de données interne, et est libre de la modifier
  2. SharedImmutableVersion--Détient une référence à l'objet de données, pour lequel aucune référence n'existe sauf dans d'autres champs SharedImmutableVersion ; de tels objets peuvent être d'un type mutable, mais seront en pratique immuables parce qu'aucune référence ne sera jamais mise à la disposition du code qui les ferait muter.

L'un des champs ou les deux peuvent être remplis ; lorsque les deux sont remplis, ils doivent renvoyer à des instances dont les données sont identiques.

Si l'on tente de muter un objet via le wrapper et que le champ UnsharedVersion est nul, un clone de l'objet dans SharedImmutableVersion doit être stocké dans UnsharedVersion. Ensuite, SharedImmutableCVersion doit être effacé et l'objet dans UnsharedVersion doit être muté comme souhaité.

Si l'on tente de cloner un objet et que SharedImmutableVersion est vide, un clone de l'objet dans UnsharedVersion doit être stocké dans SharedImmutableVersion. Ensuite, un nouveau wrapper doit être construit avec son champ UnsharedVersion vide et son champ SharedImmutableVersion rempli avec le SharedImmutableVersion de l'original.

Si plusieurs clones d'un objet sont créés, directement ou indirectement, et que l'objet n'a pas été modifié entre la construction de ces clones, tous les clones feront référence à la même instance de l'objet. L'un de ces clones peut cependant être muté sans que cela n'affecte les autres. Une telle mutation génère une nouvelle instance et la stocke dans UnsharedVersion.

1voto

Kit Points 4632

J'ai voté pour les deux Chris's y James's parce que, combinées, elles suggèrent la meilleure combinaison de cadres ( réutiliser l'espace de noms des collections F# mais en C#) et un moyen de créer facilement des types immuables par le biais du templating. Ces éléments peuvent être complétés par la notion d'"interface" que j'ai décrite dans le document ma question .

Nous disposons donc de nos collections par l'intermédiaire d'une bibliothèque telle que Collections immuables Microsoft et notre modèle via le code-gen et les interfaces ; vous pouvez coder le clonage (ou au moins la méthode partielle pour cela : clonage via la sérialisation, copie champ par champ, MemberwiseClone() et ainsi de suite) et, si vous le souhaitez, créez un fichier générique ICloneable<T> pour prendre en charge cette tâche.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X