2010-08-17 10 views
1

J'avais besoin d'une méthode qui pourrait prendre une collection de chaînes et remplacer toutes les occurrences d'une chaîne spécifique par une autre.L'instruction LINQ ne fonctionne plus après avoir été enveloppée dans une méthode d'extension

Par exemple, si j'ai un List<string> qui ressemble à ceci:

List<string> strings = new List<string> { "a", "b", "delete", "c", "d", "delete" }; 

et je veux remplacer "supprimer" par "", j'utiliser cette déclaration LINQ:

strings = (from s in strings select (s=="delete" ? s=String.Empty : s)).ToList(); 

et ça marche très bien. Mais alors j'ai pensé que je devrais en faire une méthode d'extension, puisque je l'utiliserais probablement plus tard. Dans ce cas, je veux juste écrire:

strings.ReplaceStringInListWithAnother("delete", String.Empty); 

Alors que mon code compile, et l'instruction LINQ fonctionne à l'intérieur de la méthode d'extension, quand je retourne la collection revient à son contenu d'origine:

Donc il semblerait que je viens de modifier une copie de la liste ... mais quand j'ai regardé le code pour Pop, il modifie la collection de manière similaire, mais les changements restent, donc mon hypothèse était que les déclarations de paramètres de ma méthode sont corrects. Est-ce que quelqu'un peut expliquer ce que je fais mal ici?

+1

En règle générale, il n'est plus nécessaire d'utiliser 'string.Empty'. C'est un vestige de C# 1.0. Votre code sera plus lisible si vous utilisez juste "" "'. – Timwi

Répondre

8

L'instruction LINQ que vous avez écrite ne modifie pas la collection, mais en crée une nouvelle.

La méthode d'extension que vous avez écrite crée cette nouvelle collection, puis l'ignore. L'affectation est redondante: vous affectez un paramètre local, qui sort immédiatement de la portée. Lorsque vous appelez la méthode, vous supprimez également son résultat au lieu de l'attribuer à nouveau.

Par conséquent, vous devriez écrire la méthode comme ceci:

public static List<string> ReplaceStringInListWithAnother(
    this List<string> my_list, string to_replace, string replace_with) 
{ 
    return (from s in my_list select 
     (s == to_replace ? replace_with : s)).ToList(); 
} 

et l'appel comme celui-ci:

strings = strings.ReplaceStringInListWithAnother("delete", ""); 

Par ailleurs, vous pouvez rendre la fonction plus utile en faisant Générique:

public static List<T> ReplaceInList<T>(this List<T> my_list, 
    T to_replace, T replace_with) where T : IEquatable<T> 
{ 
    return (from s in my_list select 
     (s.Equals(to_replace) ? replace_with : s)).ToList(); 
} 

De cette façon, vous pouvez l'utiliser pour d'autres choses, pas seulement string s. En outre, vous pouvez également déclarer utiliser IEnumerable<T> au lieu de List<T>:

public static IEnumerable<T> ReplaceItems<T>(this IEnumerable<T> my_list, 
    T to_replace, T replace_with) where T : IEquatable<T> 
{ 
    return from s in my_list select (s.Equals(to_replace) ? replace_with : s); 
} 

De cette façon, vous pouvez l'utiliser pour une collection d'articles assimilables, non seulement List<T>. Notez que List<T> implémente IEnumerable<T>, de sorte que vous pouvez toujours passer une liste dans cette fonction. Si vous voulez une liste, appelez simplement .ToList() après l'appel à celui-ci.

Mise à jour: Si vous voulez vraiment remplacer les éléments dans une liste au lieu de créer une nouvelle, vous pouvez toujours le faire avec une méthode d'extension, et il peut encore être générique, mais vous ne pouvez pas utiliser LINQ et vous ne pouvez pas utiliser IEnumerable<T>:

public static void ReplaceInList<T>(this List<T> my_list, 
    T to_replace, T replace_with) where T : IEquatable<T> 
{ 
    for (int i = 0; i < my_list.Count; i++) 
     if (my_list[i].Equals(to_replace)) 
      my_list[i] = replace_with; 
} 

Cela ne reviendra pas la nouvelle liste, mais modifier la place de l'ancien, il a donc un type vide de retour comme original.

+0

En guise de commentaire, cependant, je recommande de supprimer le '.ToList()' à l'intérieur de la méthode; renvoie 'IEnumerable ' au lieu de 'Liste '; et appeler '.ToList()' après l'appel à 'ReplaceStringInListWithAnother'. De cette façon, vous pouvez utiliser la même méthode dans les situations où vous ne voulez pas de 'Liste '. – Timwi

+0

@Timwi: Et, pour étendre votre conseil, je dirais que l'OP pourrait tout aussi bien accepter n'importe quel 'IEnumerable ' à ce stade plutôt que strictement une 'Liste '. –

+0

Oui. ---------- – Timwi

1

Vous n'avez pas affiché le code pour "Pop", il est donc difficile de savoir ce que vous voulez dire. Vous parlez de "quand je retourne la collection" mais vous êtes pas renvoyant quoi que ce soit - la méthode a un type de retour void.

LINQ ne modifie généralement pas le contenu d'une collection existante. Généralement, vous devez renvoyer une collection nouvelle à partir de la méthode d'extension. Par exemple:

public static IEnumerable<string> ReplaceAll 
    (this IEnumerable<string> myList, string toReplace, string replaceWith) 
{ 
    return toReplace.Select(x => x == toReplace ? replaceWith : x); 
} 

(je l'ai fait plus général ici - vous ne devriez pas commencer listes matérialisant à moins que vous avez vraiment besoin.)

Vous souhaitez alors appelez avec:

strings = strings.ReplaceAll("delete", "").ToList(); 

... ou changer le type de string-IEnumerable<string> et il suffit d'utiliser

strings = strings.ReplaceAll("delete", ""); 
+0

Merci pour les conseils, Jon. J'ai la mauvaise habitude de simplement utiliser List <> partout, puisque je n'ai pas encore été mordu en forçant un type de collection spécifique. Il est temps de changer mes habitudes! J'aime votre exemple de code ... merci. – Dave

2

Voici un indice: qu'est-ce que vous attendez du code ci-dessous?

void SetToTen(int y) 
{ 
    y = 10; 
} 

int x = 0; 
SetToTen(x); 

Si tout va bien, vous comprenez que la méthode SetToTen ci-dessus ne rien de significatif, puisqu'il ne modifie que la valeur de sa propre variable locale y et n'a pas d'effet sur la variable dont la valeur a été adoptée à elle (afin que pour se produire, le paramètre y doit être de type ref int et la méthode s'appellera SetToTen(ref x)).

En gardant à l'esprit que les méthodes d'extension ne sont réellement que des méthodes statiques dans des vêtements de fantaisie, il devrait être clair pourquoi votre ReplaceStringInListWithAnother ne fait pas ce que vous attendiez: il est réglage avec sa variable locale my_list à une nouvelle valeur, ayant pas effet sur l'original List<string> passé à la méthode.

Maintenant, il est à noter que la seule raison pour laquelle cela ne fonctionne pas pour vous est que votre code fonctionne par en définissant une variable à un nouvel objet *.Si vous deviez modifier le List<string> passé à ReplaceStringInListWithAnother, tout allait très bien:

public static void ReplaceStringInListWithAnother(this List<string> my_list, string to_replace, string replace_with) 
{ 
    for (int i = 0; i < my_list.Count; ++i) 
    { 
     if (my_list[i] == to_replace) 
     { 
      my_list[i] = replace_with; 
     } 
    } 
} 

Il est également noter que List<string> est un type de paramètre trop restrictif pour cette méthode; vous pourriez obtenir la même fonctionnalité pour n'importe quel type implémentant IList<string> (et donc je changerais le paramètre my_list pour être de type IList<string>). * En relisant votre question, il me semble clair que c'est le principal point de confusion pour vous. La chose importante que vous devez réaliser est que, par défaut, tout en C# est passé par valeur. Avec les types de valeur (tout ce qui est défini comme struct - int, double, DateTime, et bien d'autres), l'élément transmis est la valeur elle-même. Avec les types de référence (tout ce qui est défini comme class), l'élément qui est passé est une référence à un objet. Dans ce dernier cas, tous les appels de méthode sur les références à des objets de type mutable affectent réellement l'objet sous-jacent, car plusieurs variables de type référence peuvent pointer vers le même objet. Mais l'affectation est différente d'un appel de méthode; Si vous affectez une référence à un objet qui a été transmis par valeur à une nouvelle référence à un objet, vous ne faites rien à l'objet sous-jacent, et par conséquent rien de ce qui se passe ne sera reflété par la référence d'origine.

Ceci est un concept très important avec lequel de nombreux développeurs .NET ont des difficultés. Mais c'est aussi un sujet qui a été expliqué en détail ailleurs. Si vous avez besoin de plus d'explications, faites le moi savoir et je vais essayer de trouver un lien vers une page qui rende tout cela aussi clair que possible.

+0

Oui, je comprends, mais dans votre exemple, n'est-ce pas une petite différence puisque vous utilisez un type de valeur dans votre liste de paramètres, au lieu d'un type de référence? J'avais supposé que dans mon cas, lorsque j'utilisais une liste <>, on me donnait une référence, et que c'était acceptable de la réaffecter. Oops! – Dave

+0

Je n'ai pas lu vos modifications, désolé. Lire encore. – Dave

+0

@Dave: Est-ce que ma mise à jour a clarifié les choses pour vous, ou les a-t-elle simplement brouillées?Il semble que vous étiez sur la bonne voie; mais comme j'espère que vous le voyez maintenant, vous aviez une vision inexacte de ce que signifie passer un type de référence en tant que paramètre à une méthode. –