38 votes

Un moyen efficace de cloner un HashSet<T> ?

Il y a quelques jours, j'ai répondu une question intéressante sur SO à propos de HashSet<T> . Une solution possible consistait à cloner le hashset, et dans ma réponse j'ai suggéré de faire quelque chose comme ceci :

HashSet<int> original = ...
HashSet<int> clone = new HashSet<int>(original);

Bien que cette approche soit assez simple, je pense qu'elle est très inefficace : le constructeur de la nouvelle fonction HashSet<T> doit ajouter séparément chaque élément du hashset original, et vérifier s'il n'est pas déjà présent . Il s'agit clairement d'une perte de temps : puisque le recueil des sources est une ISet<T> il est garanti qu'il ne contient pas de doublons. Il devrait y avoir un moyen de tirer parti de cette connaissance...

Idéalement, HashSet<T> devrait mettre en œuvre ICloneable mais malheureusement, ce n'est pas le cas. J'ai également vérifié auprès de Reflector pour voir si les HashSet<T> le constructeur faisait quelque chose de spécifique si la collection source était un hashset, mais ce n'est pas le cas. Cela pourrait probablement être fait en utilisant la réflexion sur les champs privés, mais ce serait un hack moche...

Alors, quelqu'un a-t-il trouvé une solution intelligente pour cloner un hashset plus efficacement ?

(Notez que cette question est purement théorique, je n'ai pas besoin de faire cela dans un programme réel)

0 votes

Hm, bonne question, juste par curiosité, quelles sont les inefficacités théoriques qui nous préoccupent ? je suis rouillé sur ma notation d'ordre pour les types de données abstraites, mais une vérification de l'existence dans l'ensemble de hachage cible ne serait-elle pas un simple test de collision O(1) ? je suis d'accord que d'un point de vue informationnel, cela pourrait être "mieux", mais pouvons-nous mettre une limite sur cela, et serait-elle significative ?

2 votes

Je soupçonne qu'ils n'ont pas de constructeur HashSet<T>(ISet<T>) parce que n'importe quelle classe pourrait implémenter ISet<T>, peut-être mal ; ce qui signifie que la présence d'ISet<T> n'est pas une garantie qu'il n'y a pas de duplicata.

2 votes

@Steve Ellinger, vous avez probablement raison. Cependant, ils auraient pu fournir un constructeur HashSet<T>(HashSet<T>)...

11voto

jthg Points 1075

Si vous vouliez vraiment le moyen le plus efficace de cloner une HashSet<T> vous feriez ce qui suit (mais peut-être au prix de la maintenabilité)

  1. Utilisez le réflecteur ou le débogueur pour déterminer exactement quels sont les champs de l'application HashSet<T> doivent être copiés. Vous devrez peut-être effectuer cette opération de manière récursive pour chaque champ.
  2. Utilisez Reflection.Emit ou utiliser des arbres d'expression pour générer une méthode qui effectue la copie nécessaire de tous les champs. Il peut être nécessaire d'appeler d'autres méthodes générées qui copient la valeur de chaque champ. Nous utilisons la génération de code à l'exécution parce que c'est le seul moyen d'accéder directement aux champs privés.
  3. Utilisez FormatterServices.GetUninitializedObject(...) pour instancier un objet vierge. Utilisez la méthode générée à l'étape 2 pour copier l'objet original vers le nouvel objet vierge.

0 votes

J'ai oublié de mentionner (l'optimisation évidente) que vous voudriez mettre en cache la méthode générée et la réutiliser pour toutes les opérations de clonage.

0 votes

Oups, j'ai raté la partie où tu disais que c'était un "hack hideux". Ca ne devrait pas être trop laid si vous utilisez des tresses d'expression plutôt que Reflection.Emit. Bien entendu, la dépendance étroite à l'égard des détails de la mise en œuvre de HashSet peut rendre la situation désagréable si MS décide de modifier HashSet.

0 votes

Je n'aime pas l'idée de la réflexion sur les membres privés... mais à moins que Microsoft n'implémente un constructeur de copie approprié, je reconnais que c'est probablement le moyen le plus efficace de le faire.

2voto

Bas Smit Points 205

EDITAR: Après une inspection plus approfondie, cela ne semble pas être une bonne idée, avec moins de 60 éléments dans le hashset d'origine, la méthode ci-dessous semble être plus lente que la simple création d'un nouveau hashset.

CLAUSE DE NON-RESPONSABILITÉ : cela semble fonctionner mais à utiliser à vos propres risques, si vous allez sérialiser les hashsets clonés, vous voudrez probablement copier SerializationInfo m_siInfo.

J'ai également été confronté à ce problème et j'ai tenté de le résoudre. Vous trouverez ci-dessous une méthode d'extension qui utilise FieldInfo.GetValue et SetValue pour copier les champs requis. C'est plus rapide que d'utiliser HashSet(IEnumerable), combien dépend de la quantité d'éléments dans le hashset original. Pour 1 000 éléments, la différence est d'environ un facteur 7. Pour 100 000 éléments, elle est d'un facteur 3.

Il existe d'autres méthodes qui peuvent être encore plus rapides, mais cette méthode m'a permis d'éliminer le goulot d'étranglement pour l'instant. J'ai essayé d'utiliser les arbres d'expression et l'émission, mais j'ai rencontré un obstacle. Si je parviens à les faire fonctionner, je mettrai ce message à jour.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;

public static class HashSetExtensions
{
    public static HashSet<T> Clone<T>(this HashSet<T> original)
    {
        var clone = (HashSet<T>)FormatterServices.GetUninitializedObject(typeof(HashSet<T>));
        Copy(Fields<T>.comparer, original, clone);

        if (original.Count == 0)
        {
            Fields<T>.freeList.SetValue(clone, -1);
        }
        else
        {
            Fields<T>.count.SetValue(clone, original.Count);
            Clone(Fields<T>.buckets, original, clone);
            Clone(Fields<T>.slots, original, clone);
            Copy(Fields<T>.freeList, original, clone);
            Copy(Fields<T>.lastIndex, original, clone);
            Copy(Fields<T>.version, original, clone);
        }

        return clone;
    }

    static void Copy<T>(FieldInfo field, HashSet<T> source, HashSet<T> target)
    {
        field.SetValue(target, field.GetValue(source));
    }

    static void Clone<T>(FieldInfo field, HashSet<T> source, HashSet<T> target)
    {
        field.SetValue(target, ((Array)field.GetValue(source)).Clone());
    }

    static class Fields<T>
    {
        public static readonly FieldInfo freeList = GetFieldInfo("m_freeList");
        public static readonly FieldInfo buckets = GetFieldInfo("m_buckets");
        public static readonly FieldInfo slots = GetFieldInfo("m_slots");
        public static readonly FieldInfo count = GetFieldInfo("m_count");
        public static readonly FieldInfo lastIndex = GetFieldInfo("m_lastIndex");
        public static readonly FieldInfo version = GetFieldInfo("m_version");
        public static readonly FieldInfo comparer = GetFieldInfo("m_comparer");

        static FieldInfo GetFieldInfo(string name)
        {
            return typeof(HashSet<T>).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
        }
    }
}

0voto

supercat Points 25534

Un modèle facile qui debe ne le fera pas travail pour de nombreuses collections :

Class cloneableDictionary(Of T, U)
    Inherits Dictionary(Of T, U)
    Function clone() As Dictionary(Of T, U)
        Return CType(Me.MemberwiseClone, cloneableDict(Of T, U))
    End Function
End Class

Malheureusement, je ne sais pas si Microsoft a fait quelque chose pour empêcher l'appel de MemberwiseClone dans des endroits où il ne devrait pas être appelé (par exemple en déclarant quelque chose d'autre qu'une méthode - comme peut-être une classe - avec le nom MemberwiseClone), donc je ne sais pas comment on peut dire si une telle approche a des chances de fonctionner.

Je pense qu'il y a une bonne raison pour qu'une collection standard ne supporte pas une méthode de clonage publique mais seulement une méthode protégée : il est possible qu'une classe qui dérive d'une collection puisse se casser sévèrement si elle est clonée, et si la méthode de clonage de la classe de base est publique, il n'y a aucun moyen d'empêcher un objet d'une classe dérivée d'être donné au code qui s'attend à le cloner.

Ceci étant dit, il aurait été bien que .net inclue cloneableDictionary et d'autres classes de ce type comme types standard ( bien qu'évidemment pas mis en œuvre essentiellement comme ci-dessus).

1 votes

Cela ne marchera pas... Il fait une copie superficielle, ce qui est ce que je veux (plus ou moins), mais c'est... trop Peu importe : la plupart des collections utilisent en interne des tableaux pour stocker les éléments et/ou les seaux, et MemberwiseClone créera une copie de la collection. avec la même instance de tableau . Les clones ne seront donc pas des copies indépendantes : si je modifie l'une des collections, l'autre sera affectée aussi, et sera corrompue, ce qui est pire !

0 votes

Notez les modifications ci-dessus. Cela vaut probablement la peine de le garder comme réponse, pour dissuader toute autre personne qui pourrait proposer la même "solution". BTW, c'est dommage que Microsoft n'ait pas fait de "BaseClone" une méthode protégée dont l'implémentation par défaut serait un clone par membre, et défini un moyen standard pour la désactiver (par exemple, en la masquant avec quelque chose d'autre appelé BaseClone qui n'est pas une méthode).

0 votes

Thomas Levesque : Vraiment une erreur embarrassante, surtout que j'essayais juste de trouver le bon modèle pour les objets clonables. Dès que j'ai vu votre premier message, j'ai su immédiatement que j'avais fait une erreur. Il semble que beaucoup de gens préfèrent la notion de constructeur de copie, mais en général un constructeur de copie est un mauvais substitut pour une méthode de clonage, puisque le type de l'objet créé par un constructeur de copie sera le type du constructeur, plutôt que le type de l'objet copié. Je vais peut-être publier ma proposition de modèle de clonage sur un blog et créer un lien vers celui-ci.

0voto

Farinha Points 5518

Serait-il possible d'utiliser la méthode serialize -> de-serialize ?

http://stackoverflow.com/questions/78536/cloning-objects-in-c

-1voto

Ari Gesher Points 406

Un clone en O(n) est aussi bon qu'il peut l'être, théoriquement, pour cloner deux ensembles qui ne partagent pas la même structure de données sous-jacente.

Vérifier si un élément se trouve ou non dans un HashSet doit être une opération en temps constant (c'est-à-dire O(1)).

Vous pourriez donc créer un wrapper qui ne ferait qu'envelopper un HashSet existant et conserverait tous les nouveaux ajouts, mais cela semble plutôt pervers.

Quand vous dites "efficace", vous voulez dire "plus efficace que la méthode O(n) existante". Je pense qu'il est impossible d'être plus efficace que O(n) sans jouer à des jeux sémantiques sérieux sur la signification du mot "clone".

2 votes

Non, quand je dis "efficace", je ne veux pas dire une meilleure complexité. Vous avez raison de dire que ce sera une opération O(n) de toute façon, mais il ne s'agit pas seulement de complexité. Considérez ceci : List<T>.Add a une complexité O(1), comme HashSet<T>.Add mais c'est beaucoup plus plus rapide car il n'a pas besoin de vérifier si l'élément est déjà présent. Donc, quand je dis "efficace", je veux dire plus rapide, pas moins complexe.

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