Оператор LINQ больше не работает после переноса в метод расширения - PullRequest
1 голос
/ 17 августа 2010

Мне был нужен метод, который мог бы взять коллекцию строк и заменить все вхождения определенной строки другой.

Например, если у меня есть List<string>, который выглядит следующим образом:

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

и я хочу заменить «удалить» на «», я бы использовал этот оператор LINQ:

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

и это прекрасно работает. Но потом я решил, что должен сделать это методом расширения, так как я, скорее всего, буду использовать его позже. В этом случае я просто хочу написать следующее:

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

Пока мой код компилируется, а оператор LINQ работает внутри метода расширения, когда я возвращаю коллекцию, возвращается к исходному содержимому:

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

Так что может показаться, что я только что изменил копию списка ... но когда я посмотрел на код для Pop, он так же изменяет коллекцию, но изменения остаются, поэтому я предположил, что объявления параметров моего метода правильный.

Может кто-нибудь объяснить, что я здесь делаю не так?

Ответы [ 3 ]

8 голосов
/ 17 августа 2010

Оператор LINQ, который вы написали, не изменяет коллекцию, он фактически создает новую.

Метод расширения, который вы написали, создает эту новую коллекцию, а затем отбрасывает ее.Назначение является избыточным: вы присваиваете локальный параметр, который выходит за пределы области сразу после.

Когда вы вызываете метод, вы также отбрасываете его результат, а не присваиваете его обратно.

Следовательно, вы должны написать метод следующим образом:

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();
}

и вызов как этот:

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

Кстати, вы можете сделатьФункция более полезна, сделав ее общей:

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();
}

Таким образом, вы можете использовать ее для других целей, а не только для string с.Кроме того, вы также можете объявить, что он использует IEnumerable<T> вместо 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);
}

Таким образом, вы можете использовать его для любой коллекции уравниваемых предметов, а не только для List<T>.Обратите внимание, что List<T> реализует IEnumerable<T>, так что вы все равно можете передать List в эту функцию.Если вы хотите получить список, просто позвоните .ToList() после вызова этого.

Обновление: Если вы действительно хотите заменить элементы в списке вместосоздания нового, вы все еще можете сделать это с помощью метода расширения, и он все еще может быть общим, но вы не можете использовать Linq и вы не можете использовать 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;
}

Thisне вернет новый список, но вместо этого изменит старый, поэтому он имеет тип возврата void , как ваш оригинал.

2 голосов
/ 17 августа 2010

Вот подсказка: что вы ожидаете от кода ниже?

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

int x = 0;
SetToTen(x);

Надеюсь, вы понимаете, что метод SetToTen, описанный выше, не делает ничего значащего, поскольку он только изменяет значение своего собственноголокальная переменная y и не влияет на переменную, значение которой было передано ей (для этого параметр y должен иметь тип ref int, а метод будет вызываться как SetToTen(ref x)).

Принимая во внимание, что методы расширения на самом деле являются просто статическими методами в модной одежде, должно быть понятно, почему ваш ReplaceStringInListWithAnother не делает того, что вы ожидали: он только устанавливает local my_list переменная для нового значения, не влияющая на исходную List<string>, переданную методу.

Теперь стоит отметить, что единственная причина, по которой это не работает для вас, заключается в том, что ваш код работает установив переменную для нового объекта *.Если бы вы изменили List<string>, переданное ReplaceStringInListWithAnother, все бы работало просто отлично:

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;
        }
    }
}

Это также стоит отметить, что List<string>является чрезмерно ограничительным типом параметра для этого метода;Вы можете достичь той же функциональности для любого типа, реализующего IList<string> (и поэтому я бы изменил параметр my_list на тип IList<string>).


* Повторное чтение вопросаМне кажется ясным, что это главная причина путаницы для вас.Важно понимать, что по умолчанию все в C # передается по значению.С типами значений (все, что определено как struct - int, double, DateTime и многие другие), вещь , которая передана , является само значением.С ссылочными типами (все, что определено как class), вещь , которая передана , является ссылкой на объект .В последнем случае все вызовы методов для ссылок на объекты изменяемых типов действительно влияют на базовый объект, поскольку несколько переменных ссылочного типа могут указывать на один и тот же объект.Но присваивание отличается от вызова метода;если вы назначаете ссылку на объект, который был передан по значению какой-либо новой ссылке на объект, вы ничего не делаете с базовым объектом, и, следовательно, не происходит ничего, что могло бы отразиться на исходной ссылке.

Это действительно важная концепция, с которой сталкиваются многие разработчики .NET.Но это также тема, которая была подробно объяснена в другом месте.Если вам нужно больше объяснений, дайте мне знать, и я постараюсь найти ссылку на страницу, которая сделает все это максимально понятным.

1 голос
/ 17 августа 2010

Вы не показали код "Pop", поэтому трудно понять, что вы имеете в виду. Вы говорите «когда я возвращаю коллекцию», но вы не возвращаете что-либо - у метода есть void тип возврата.

LINQ обычно не меняет содержимое существующей коллекции. Обычно вы должны возвращать новую коллекцию из метода расширения. Например:

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

(Я сделал это более общим - вам не следует начинать материализацию списков, если вам действительно не нужно.)

Затем вы назвали бы это:

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

... или измените тип string на IEnumerable<string> и просто используйте

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