2009-11-20 5 views
2

Disons que j'ai une classe simple Cat en C#, avec une propriété Name de type string. Maintenant, j'ai besoin d'une classe de collection pour mes chats, donc je décide d'envelopper un Dictionary<string, Cat> dans une classe de collection personnalisée. En fait, cette classe contient une variable dictionnaire privé et ajoute ou supprime les membres de la collecte, au besoin, ainsi que les chats d'indexation par leur nom:Gestion des clés de collection mutables en C#

class Cats 
{ 
    private Dictionary<string, Cat> m_dic = new Dictionary<string,Cat>(); 

    public void Add(Cat c) 
    { 
     m_dic.Add(c.Name, c); 
    } 

    public void Remove(string name) 
    { 
     m_dic.Remove(name); 
    } 

    public Cat this[string name] 
    { 
     get 
     { 
      return m_dic[name]; 
     } 
    } 
} 

Maintenant, je peux créer une collection et des chats, comme ceci:

Cats cs = new Cats(); 
cs.Add(new Cat("Valentina")); 
cs.Add(new Cat("Sophie")); 
cs.Add(new Cat("Tomboy")); 

Et je peux récupérer un chat de son nom:

Cat c1 = cs["Sophie"]; 

Tout cela est très bon. Le problème est, quand je change le nom d'un chat, comme ceci:

c1.Name = "Sofia"; 

... la clé de collection pour l'objet visé par c1 n'est pas mis à jour, évidemment. Donc, si je tente de récupérer le même élément en utilisant son nouveau nom, je reçois une exception:

Cat c2 = cs["Sofia"]; //KeyNotFoundException is thrown here. 

Ce comportement est correct et évidente par le moteur d'exécution. Ma question est: pouvez-vous suggérer une méthode élégante et fiable pour modifier les clés de collection chaque fois que la propriété de nom d'un élément change?

Mon objectif est de pouvoir récupérer, à tout moment, un objet de son nom, comme vous pouvez l'imaginer. J'ai abordé le problème en ayant le setter de la propriété Name déclencher un événement afin que toute collection contenant cet objet puisse mettre à jour la clé correspondante. Cette méthode est assez lourde et pas très efficace, cependant.

Pouvez-vous penser à quelque chose de mieux? Je vous remercie.

+0

S'il vous plaît voir l'edit à mon poste ci-dessous. –

Répondre

4

Quelle sera la taille de votre collection et à quel point est-il important de pouvoir récupérer un élément via un index?

Si ça va être relativement faible (plusieurs centaines, par opposition à des milliers), vous pourriez être mieux d'utiliser un List<Cat>, et d'y accéder en utilisant les nouvelles méthodes d'extension LINQ, comme:

public Cat this[string name]{ 
    get{ 
     //Will return the first Cat in the list (or null if none is found) 
     return m_List.Where(c => c.Name == name).FirstOrDefault(); 
    } 
} 

Ajout (et la suppression) est également trivial:

public void Add(Cat c){ 
    m_List.Add(c); 
} 

Faites-moi savoir si cela ne fonctionnera pas pour vous. J'espère que cela t'aides!

+0

Salut, merci. Je comprends que List <> est indexé par int seulement, donc en utilisant un tel endroit devrait être assez inefficace, non? Je ne m'attends pas à avoir de très grandes collections, donc cela peut fonctionner. – CesarGon

+0

"indexé par int" devrait être "indexé par position"; p – CesarGon

+1

Ce n'est pas une solution terrible, à mon avis. Comme je l'ai mentionné dans ma réponse, la taille de la collection affectera la vitesse de l'accès aux objets. En outre, la fréquence d'accès peut affecter négativement les performances si la liste est très volumineuse et que vous recherchez un élément plusieurs fois de suite. Je vais essayer de penser à une autre solution comparable à votre exemple original! – Pwninstein

1

vous pouvez également mettre un rappel sur Cat ainsi quand sa propriété Name change votre collection est notifié

+0

J'ai expliqué dans ma question que j'ai essayé de faire en sorte que Cat soulève un événement lorsque le nom est changé, et que la collection réponde à cet événement en mettant à jour la clé. C'est ce que tu veux dire? :-) – CesarGon

4

... en ayant le poseur de la Name propriété ... déclencher un événement

Voulez-vous dire quelque chose comme ça?

c1.Name = "Sofia"; 
NameChangedEventHandler handler = NameChangedEvent; 
if (handler != null) 
{ 
    handler(c1, new NameChangedEventArgs("Sophie", "Sophia")); 
} 

Est-ce que vous entendez par avoir le setter déclencher un événement?Si oui, alors je suggérerais de déplacer ceci au setter de la propriété Name de la classe Cat. Il n'y a aucune raison d'exiger que les setters augmentent l'événement comme ceci. Cela devrait être fait implicitement lorsque le nom du Cat change via la propriété publique. Pour moi, ce est une solution élégante; cela ne correspond pas à la façon dont fonctionnent les collections. Je ne sais pas si c'est un problème en soi, mais cela associe étroitement la collection Cats à la classe Cat. Gardez à l'esprit que vous voudrez probablement implémenter beaucoup des mêmes interfaces que la classe Dictionary générique. Sinon, la collection Cats ressemblera à Dictionary à certains égards, mais pas complètement.

EDIT: Ceci est en réponse à votre commentaire. J'espère que je peux transmettre plus clairement mes pensées. Mon intention est d'améliorer votre conception.

Je suis d'accord que, en général, les événements fournissent un niveau de couplage plus lâche. Cependant, dans ce cas, la collection Cats est toujours étroitement associée à la classe Cat car la collection est en train de s'enregistrer avec un type spécifique d'événement exposé par un type spécifique de classe.

Alors, comment cela peut-il être amélioré?

Une méthode simple pour améliorer cela est que la classe Cat implémente un événement qui est défini dans une interface. .NET fournit une telle interface dans ce but précis - l'interface INotifyPropertyChanged dans l'espace de noms System.ComponentModel. En mettant en œuvre cette interface dans la classe Cat, cela permettrait à l'enveloppe de collecte Cats à définir comme ceci:

class Cats 
{ 
    private Dictionary<string, INotifyPropertyChanged> m_dic = 
     new Dictionary<string, INotifyPropertyChanged>(); 
    public void Add(INotifyPropertyChanged obj) 
    { 
     m_dic.Add(obj.Name, obj); 
    } 
    public void Remove(string name) 
    { 
     m_dic.Remove(name); 
    } 
    public INotifyPropertyChanged this[string name] 
    { 
     get { return m_dic[name]; } 
    } 
} 

Voir l'amélioration? La collection est plus flexible maintenant. Il peut contenir n'importe quel type qui implémente l'interface INotifyPropertyChanged. En d'autres termes, il n'est plus lié à la classe Cat.

Cependant, il est toujours nécessaire que toute valeur stockée dans le dictionnaire implémente une propriété Name (voir la méthode Add()). Il reste donc du travail à faire. En fin de compte, vous souhaitez que la collection contienne des objets qui fournissent une propriété string à utiliser comme valeur de clé. La solution est de définir cela comme une interface.

public interface INotificationKey : INotifyPropertyChanged 
{ 
    string Key { get; set; } 
} 

Notez que l'interface INotificationKey hérite de l'interface INotifyPropertyChanged, ce qui permet à l'enveloppe de recouvrement à définir comme suit:

class NotificationDictionary 
{ 
    private Dictionary<string, INotificationKey> m_dic = 
     new Dictionary<string, INotificationKey>(); 
    public void Add(INotificationKey obj) 
    { 
     m_dic.Add(obj.Key, obj); 
    } 
    public void Remove(string key) 
    { 
     m_dic.Remove(key); 
    } 
    public INotificationKey this[string key] 
    { 
     get { return m_dic[key]; } 
    } 
} 

Cette solution est sensiblement plus flexible. Mais il reste court parce qu'il ne fonctionne pas pleinement comme un Dictionary devrait. Par exemple, comme défini, la classe NotificationDictionary ne peut pas être utilisée dans une itération foreach car elle n'implémente pas l'interface IEnumerable<>.

Pour se qualifier comme une solution vraiment élégante, la collection devrait se comporter comme Dictionary. Cela exigera un peu plus d'efforts à l'avant, mais à l'arrière, vous aurez une solution suffisamment souple pour s'adapter à diverses situations.

+0

Ce que j'ai fait (et ce que j'ai essayé d'expliquer dans ma question) était précisément ce que vous suggérez: élever l'évènement du bloc set dans la propriété publique Name de Cat. :-) Le couplage est correct, car les événements, après tout, ne font pas beaucoup de couplage entre les classes. Les collections sont libres de s'inscrire aux événements ou non. C'est bon (faible couplage) et mauvais aussi (car il n'y a aucun moyen de forcer une collection à garder ses clés à jour). Cette raison est pourquoi je n'aime pas trop l'approche; il est relativement facile pour quelqu'un d'oublier de s'abonner à l'événement NameChanged et, par conséquent, de mettre à jour les clés de collection. – CesarGon

+0

Merci Matt. Votre modification est la plus appréciée. Cependant, le but de la collection Cats est d'avoir un conteneur fortement typé pour Cat. Non seulement cela ne me dérange pas que Cats soit étroitement lié à Cat; C'est tellement par conception. Où je veux qu'ils soient faiblement couplés en ce qui concerne les mises à jour clés, c'est-à-dire qu'une instance de Cat ne "sait" pas quelles collections contiennent des références, donc une approche basée sur les événements (qui est intrinsèquement lâche) sonne bien moi, en principe. Encore, merci pour l'idée de INotifyPropertyChanged, je ne savais pas à ce sujet. :-) – CesarGon