72 votes

Changer la valeur d'un élément dans une liste de structures

J'ai une liste de structs et je veux changer un élément. Par exemple :

MyList.Add(new MyStruct("john");
MyList.Add(new MyStruct("peter");

Maintenant je veux changer un élément :

MyList[1].Name = "bob"

Cependant, chaque fois que j'essaie de le faire, j'obtiens l'erreur suivante :

Impossible de modifier la valeur de retour de 'System.Collections.Generic.List.this[int]' car ce n'est pas une variable

Si j'utilise une liste de classes, le problème ne se produit pas.

Je suppose que la réponse a à voir avec le fait que les structs sont un type de valeur.

Donc, si j'ai une liste de structs, devrais-je les traiter comme lecture seule? Si j'ai besoin de changer des éléments dans une liste, alors je devrais utiliser des classes et non des structs?

47voto

Gishu Points 59012

Ne tout à fait. Concevoir un type en tant que classe ou struct ne doit pas être motivé par votre besoin de le stocker dans des collections :) Vous devriez plutôt vous concentrer sur les 'sémantiques' nécessaires

Le problème que vous rencontrez est dû aux sémantiques des types de valeur. Chaque variable/référence de type de valeur est une nouvelle instance. Lorsque vous faites

Struct obItem = MyList[1];

ce qui se passe, c'est qu'une nouvelle instance de la struct est créée et que tous les membres sont copiés un par un. Ainsi, vous avez un clone de MyList[1] c'est-à-dire 2 instances. Maintenant, si vous modifiez obItem, cela n'affecte pas l'original.

obItem.Name = "Gishu";  // MyList[1].Name reste "peter"

Maintenant, accrochez-vous pendant 2 minutes ici (Cela prend un certain temps à avaler.. cela a été le cas pour moi :) Si vous avez vraiment besoin que des structs soient stockées dans une collection et modifiées comme vous l'avez indiqué dans votre question, vous devrez faire en sorte que votre struct expose une interface (Cependant cela entraînera le boxing). Vous pourrez ensuite modifier la struct réelle via une référence d'interface, qui se réfère à l'objet boxé.

L'extrait de code suivant illustre ce que je viens de dire ci-dessus

public interface IMyStructModifier
{
    String Name { set; }
}
public struct MyStruct : IMyStructModifier ...

List obList = new List();
obList.Add(new MyStruct("ABC"));
obList.Add(new MyStruct("DEF"));

MyStruct temp = (MyStruct)obList[1];
temp.Name = "Gishu";
foreach (MyStruct s in obList) // => "ABC", "DEF"
{
    Console.WriteLine(s.Name);
}

IMyStructModifier temp2 = obList[1] as IMyStructModifier;
temp2.Name = "Now Gishu";
foreach (MyStruct s in obList) // => "ABC", "Now Gishu"
{
    Console.WriteLine(s.Name);
}

J'espère que cela vous aide. Bonne question.
Mise à jour: @Hath - tu m'as fait vérifier si j'avais négligé quelque chose d'aussi simple. (Ce serait incohérent si les propriétés setter ne le faisaient pas et les méthodes le faisaient - l'univers .Net est toujours équilibré :)
La méthode setter ne fonctionne pas
obList2[1] renvoie une copie dont l'état serait modifié. La struct d'origine dans la liste reste non modifiée. Donc l'ensemble via une interface semble être la seule façon de le faire.

List obList2 = new List();
obList2.Add(new MyStruct("ABC"));
obList2.Add(new MyStruct("DEF"));
obList2[1].SetName("WTH");
foreach (MyStruct s in obList2) // => "ABC", "DEF"
{
    Console.WriteLine(s.Name);
}

0 votes

Encore aucune bonne. La liste devrait être déclarée comme le type d'interface, auquel cas tous les éléments qui y figurent seraient mis en boîte. Pour chaque type de valeur, il existe un équivalent mis en boîte qui possède des sémantiques de type de classe. Si vous voulez utiliser une classe, utilisez une classe, mais soyez alors conscient des autres éventuelles difficultés.

0 votes

@Supercat - Comme je l'ai mentionné ci-dessus... cela causera du Boxing. Je ne recommande pas de modifier via une référence d'interface - je dis juste que ça fonctionnera si vous devez avoir une collection de structs + que vous voulez les modifier sur place. C'est une astuce.. fondamentalement, vous créez des wrappers de type ref pour des types de valeurs.

1 votes

Plutôt que d'utiliser List ou de faire en sorte que la structure implémente une interface setter (dont la sémantique est horrible, comme noté ci-dessus), une alternative consiste à définir `class SimpleHolder { public T Value; }` et ensuite à utiliser un `List>`. Si `Value` est un champ plutôt qu'une structure, alors une déclaration comme `obList2[1].Value.Name="George";` fonctionnera très bien.

41voto

Andrew Points 1294
MaListe[1] = new MaStructure("bob");

Les structs en C# doivent presque toujours être conçus pour être immuables (c'est-à-dire, ne pas avoir de moyen de modifier leur état interne une fois qu'ils ont été créés).

Dans votre cas, ce que vous voulez faire est de remplacer l'ensemble du struct dans un indice de tableau spécifié, pas essayer de modifier simplement une seule propriété ou un champ.

2 votes

Voici pas la réponse complète, la réponse de Gishu est beaucoup plus complète.

3 votes

Ce que Jolson a dit - Ce n'est pas tant que les structs sont "immuables." est correct. -1 cos Il est vraiment faux de dire que les structs sont immuables.

3 votes

Pour être juste envers Andrew - je n'interprète pas qu'il dit que les structs sont "immuables", il dit qu'ils devraient être utilisés comme si ils étaient immuables; et bien sûr, vous pouvez les rendre immuables si tous les champs sont en lecture seule.

15voto

Jason Olson Points 2752

Ce n'est pas tant que les struct sont "immuables".

Le vrai problème sous-jacent est que les structs sont un type de valeur, et non un type de référence. Donc, lorsque vous extrayez une "référence" de la struct de la liste, cela crée une nouvelle copie de toute la struct. Donc, les modifications que vous apportez concernent la copie, et non la version originale dans la liste.

Comme le mentionne Andrew, vous devez remplacer l'ensemble de la struct. À ce stade, je pense cependant que vous devez vous demander pourquoi vous utilisez une struct en premier lieu (au lieu d'une classe). Assurez-vous de ne pas le faire pour des préoccupations d'optimisation prématurée.

0 votes

Hm. Pourquoi puis-je modifier un champ d'une structure allouée sur la pile, même lorsque la structure fait partie d'un tableau MyStruct[]? Pourquoi cela fonctionne-t-il?

7voto

supercat Points 25534

Il n'y a rien de mal avec les structs qui ont des champs exposés, ou qui permettent la mutation via les setters de propriété. Les structs qui se mutent eux-mêmes en réponse aux méthodes ou aux accesseurs de propriété, en revanche, sont dangereux car le système permettra que des méthodes ou accesseurs de propriété soient appelés sur des instances de struct temporaires; si les méthodes ou accesseurs apportent des modifications à la struct, ces modifications finiront par être ignorées.

Malheureusement, comme vous le notez, les collections intégrées dans .net sont vraiment faibles pour exposer les objets de type valeur qu'elles contiennent. Votre meilleur choix est généralement de faire quelque chose comme :

  MyStruct temp = myList\[1\];
  temp.Name = "Albert";
  myList\[1\] = temp;

Un peu ennuyeux, et pas du tout sûr avec plusieurs threads. Tout de même une amélioration par rapport à une Liste d'un type de classe, où faire la même chose pourrait nécessiter :

  myList\[1\].Name = "Albert";

mais cela pourrait aussi nécessiter :

  myList\[1\] = myList\[1\].Withname("Albert");

ou peut-être

  myClass temp = (myClass)myList\[1\].Clone();
  temp.Name = "Albert";
  myList\[1\] = temp;

ou peut-être une autre variante. On ne pourrait vraiment pas le savoir à moins d'examiner myClass ainsi que les autres codes qui mettent des choses dans la liste. Il est tout à fait possible que l'on ne puisse pas savoir si la première forme est sûre sans examiner le code dans les assemblages auxquels on n'a pas accès. En revanche, si 'Name' est un champ exposé de MyStruct, la méthode que j'ai donnée pour le mettre à jour fonctionnera, peu importe ce que MyStruct contient d'autre, ou peu importe ce que d'autres éléments ont pu faire avec myList avant que le code ne s'exécute ou ce à quoi ils peuvent s'attendre à faire avec après.

0 votes

Vous dites : "...si Name est un champ exposé de MyStruct, la méthode que j'ai donnée pour le mettre à jour fonctionnera..." Pas exactement. Étant donné que vous avez soulevé le problème de la sécurité des threads pour le cas de la référence class, il est juste de juger le code ValueType sur la même base, et les positions d'index dans la liste peuvent changer pendant votre opération de telle sorte que myList[1] ne corresponde plus à l'instance de struct que vous avez récupérée. Pour corriger cela, vous auriez besoin d'un type de verrouillage ou de synchronisation qui soit conscient de l'instance de collection dans son ensemble. De plus, la version class souffre toujours de ces mêmes problèmes également.

0 votes

@GlennSlayden: Une collection qui expose des éléments en tant que références pour une édition sur place pourrait facilement permettre aux éléments d'être édités de manière thread-safe si toutes les ajouts et suppressions qui seront jamais effectuées, sont effectuées avant que des références ne soient exposées. Si nécessaire, il serait possible de construire une collection permettant d'ajouter des éléments à la fin sans affecter aucune référence, mais cela nécessiterait que toute expansion soit effectuée uniquement en ajoutant de nouveaux objets ou tableaux - certainement possible, mais pas la façon dont la plupart des collections sont implémentées.

0 votes

Je n'ai pas dit que ce n'était pas possible et je connais certainement de nombreuses façons de le réparer, je voulais simplement signaler la condition de course dans votre exemple tel qu'il est. Peut-être que c'est parce que ma micro-expertise curieuse dans ce domaine obscur fait ressortir les courses comme des météorites sur le glacier de l'Antarctique. En ce qui concerne "une collection qui expose des éléments comme des références pour une édition sur place:" Oui, exactement; je n'aurais pas pu mieux dire moi-même. J'ai déjà vu ça, cela fonctionne très bien. Oh, et j'ai presque oublié, la concurrence sans verrouillage aussi.

2voto

PassingBy Points 21

À quelqu'un de plus haut dans le fil de discussion qui a écrit ".Net universe is still balanced...":

Alors pourquoi le comportement n'est-il pas cohérent lors de l'utilisation d'un tableau ordinaire de structures? Je comprends que la raison se trouve dans l'implémentation de la liste générique mais ...

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