Как изменить или удалить элементы из перечисляемой коллекции во время итерации по ней в C#

Мне нужно удалить несколько строк из таблицы данных. Я слышал, что нельзя менять коллекцию во время ее повторения. Таким образом, вместо цикла for, в котором я проверяю, соответствует ли строка требованиям для удаления, а затем помечаю ее как удаленную, я должен сначала выполнить итерацию по таблице данных и добавить все строки в список, затем выполнить итерацию по списку и отметить строки для удалений. Каковы причины этого и какие у меня есть альтернативы (вместо использования списка строк, которые я имею в виду)?


person kjv    schedule 21.11.2008    source источник
comment
Я отредактировал заголовок, чтобы упростить поиск этого вопроса. Был обман, который я закрыл ранее, но я вижу, как пользователь мог пропустить этот вопрос со старым заголовком.   -  person Jason Jackson    schedule 01.01.2009


Ответы (8)


Вы можете удалить элементы из коллекции, если используете простой цикл for.

Взгляните на этот пример:

        var l = new List<int>();

        l.Add(0);
        l.Add(1);
        l.Add(2);
        l.Add(3);
        l.Add(4);
        l.Add(5);
        l.Add(6);

        for (int i = 0; i < l.Count; i++)
        {
            if (l[i] % 2 == 0)
            {
                l.RemoveAt(i);
                i--;
            }
        }

        foreach (var i in l)
        {
            Console.WriteLine(i);
        }
person bruno conde    schedule 21.11.2008
comment
Это ошибочно, так как не все элементы будут проверены. Если вы удалите элемент с номером i, то элемент с номером i + 1 станет номером i. Когда i затем увеличивается для следующего цикла, он пропускает элемент, который только что заменил удаленный элемент (надеюсь, это имеет смысл). - person Andy Rose; 21.11.2008
comment
Я исправил это. Спасибо, Энди, за пощечину. Но моя точка зрения заключалась в том, что вы можете изменить коллекцию с помощью цикла for. - person bruno conde; 21.11.2008
comment
Могу ли я использовать l[i].Delete(), так как removeat создаст проблемы с адаптером таблицы в процедуре обновления (строки будут удалены из таблицы, а не просто помечены как удаленные). - person kjv; 21.11.2008
comment
Если вы просто хотите пометить эти объекты как удаленные, вы можете использовать цикл foreach, потому что на самом деле вы не удаляете какие-либо объекты, а коллекция остается прежней... - person bruno conde; 21.11.2008
comment
@Bruno - абсолютно согласен, что цикл for можно использовать для редактирования коллекции, и ваше редактирование устранило проблему пропуска элементов. - person Andy Rose; 21.11.2008
comment
@iulianchira: вы правы в том, что не хотите использовать RemoveAt со строками в DataTable, так как это сломает адаптер таблицы. Смотрите мой новый ответ ниже. - person MusiGenesis; 21.11.2008
comment
Не лучше ли повторить назад? for(int i = l.count; i › 0; i--). Таким образом, если вы удалите элемент, а на его место упадет следующий элемент, это не имеет значения, потому что вы уже проверили это. Или я что-то здесь упускаю? - person Michael Stum; 21.11.2008
comment
@Майкл - Нет, ты прав. Ваше решение более эффективно. +1 - person bruno conde; 21.11.2008
comment
Лучше использовать встроенный метод RemoveAll, который принимает в качестве параметра предикат. - person ChrisW; 01.01.2009

Итерация в обратном направлении по списку звучит как лучший подход, потому что если вы удаляете элемент, а другие элементы «попадают в пробел», это не имеет значения, потому что вы уже просмотрели их. Кроме того, вам не нужно беспокоиться о том, что ваша переменная-счетчик станет больше, чем .Count.

        List<int> test = new List<int>();
        test.Add(1);
        test.Add(2);
        test.Add(3);
        test.Add(4);
        test.Add(5);
        test.Add(6);
        test.Add(7);
        test.Add(8);
        for (int i = test.Count-1; i > -1; i--)
        {
            if(someCondition){
                test.RemoveAt(i);
            }
        }
person Michael Stum    schedule 21.11.2008

Взяв код @bruno, я бы сделал это в обратном порядке.

Потому что, когда вы двигаетесь назад, отсутствующие индексы массива не мешают порядку вашего цикла.

var l = new List<int>(new int[] { 0, 1, 2, 3, 4, 5, 6 });

for (int i = l.Count - 1; i >= 0; i--)
    if (l[i] % 2 == 0)
        l.RemoveAt(i);

foreach (var i in l)
{
    Console.WriteLine(i);
}

А если серьезно, в наши дни я бы использовал LINQ:

var l = new List<int>(new int[] { 0, 1, 2, 3, 4, 5, 6 });

l.RemoveAll(n => n % 2 == 0);
person chakrit    schedule 22.11.2008
comment
+1 за осознание возможностей LINQ для чего-то подобного. Зачем делать что-то далеко, если вам не нужно - person BenAlabaster; 24.12.2008
comment
почему бы не определить список l как List‹int› вместо var, если вы знаете, что это будет List‹int›? +1 хотя - person sebagomez; 10.11.2009
comment
+ 1 для УдалитьВсе. Примечание. RemoveAll не является строго LINQ. Он доступен для любого класса List в .NET 2.0 без поддержки LINQ. - person Ray; 17.12.2009

Поскольку вы работаете с DataTable и должны иметь возможность сохранять любые изменения обратно на сервер с помощью адаптера таблицы (см. комментарии), вот пример того, как вы должны удалять строки:

DataTable dt;
// remove all rows where the last name starts with "B"
foreach (DataRow row in dt.Rows)
{
    if (row["LASTNAME"].ToString().StartsWith("B"))
    {
        // mark the row for deletion:
        row.Delete();
    }
}

Вызов удаления для строк изменит их свойство RowState на Deleted, но оставит удаленные строки в таблице. Если вам все еще нужно работать с этой таблицей, прежде чем сохранять изменения обратно на сервер (например, если вы хотите отобразить содержимое таблицы за вычетом удаленных строк), вам нужно проверить RowState каждой строки, когда вы повторяете ее, как это :

foreach (DataRow row in dt.Rows)
{
    if (row.RowState != DataRowState.Deleted)
    {
        // this row has not been deleted - go ahead and show it
    }
}

Удаление строк из коллекции (как в ответе Бруно) сломает адаптер таблицы и, как правило, не должно выполняться с DataTable.

person MusiGenesis    schedule 21.11.2008

Цикл while справится с этим:

int i = 0;
while(i < list.Count)
{
    if(<codition for removing element met>)
    {
        list.RemoveAt(i);
    }
    else
    {
        i++;
    }
}
person Andy Rose    schedule 21.11.2008
comment
Это решение сталкивается с той же проблемой, что и выше, ваши индексы будут отключены, если вы удалите элемент. - person Element; 23.07.2009
comment
Нет, так как индекс увеличивается только тогда, когда элемент не удаляется. - person Andy Rose; 23.07.2009

Решение chakrit также можно использовать, если вы нацелены на .NET 2.0 (без выражений LINQ/лямбда), используя делегат, а не лямбда-выражение:

public bool IsMatch(int item) {
    return (item % 3 == 1); // put whatever condition you want here
}
public void RemoveMatching() {
    List<int> x = new List<int>();
    x.RemoveAll(new Predicate<int>(IsMatch));
}
person Community    schedule 24.12.2008

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

Я часто использовал подход с двумя списками для решения проблемы:

ArrayList matches = new ArrayList();   //second list

for MyObject obj in my_list
{

    if (obj.property == value_i_care_about)
        matches.addLast(obj);
}

//now modify

for MyObject m in matches
{
    my_list.remove(m); //use second list to delete from first list
}

//finished.
person Philluminati    schedule 21.11.2008

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

person Adrian    schedule 22.11.2008