替换列表中的项目——就地

Replace items in a list -- in place

我遇到了一些简单的 C# 代码的问题,我可以在 C/C++ 中轻松解决这些问题。 我想我错过了什么。

我想执行以下操作(修改列表中的项目 -- 就地):

//pseudocode
void modify<T>(List<T> a) {
  foreach(var item in a) {
    if(condition(item)) {
      item = somethingElse;
    }
  }
}

我知道 foreach 在一个被视为不可变的集合上循环,所以上面的代码不能工作。

因此我尝试了以下方法:

void modify<T>(List<T> a) {
  using (var sequenceEnum = a.GetEnumerator())
  {
    while (sequenceEnum.MoveNext())
    {
      var m = sequenceEnum.Current;
      if(condition(m)) {
         sequenceEnum.Current = somethingElse;
      }
    }
  }
}

天真地认为 Enumerator 是指向我的元素的某种指针。显然枚举数也是不可变的。

在 C++ 中我会这样写:

template<typename T>
struct Node {
    T* value;
    Node* next;
}

然后能够修改 *value 而无需触及 Node 中的任何内容,因此在父集合中:

Node<T>* current = a->head;
while(current != nullptr) {
    if(condition(current->value)) 
        current->value = ...
    }
    current = current->next;
}

我真的必须使用不安全代码吗?

或者我是否将调用下标的可怕之处卡在了循环中?

使用这样的东西:

List<T> GetModified<T>(List<T> list, Func<T, bool> condition, Func<T> replacement)
{
   return list.Select(m => if (condition(m)) 
                        { return m; }
                       else
                        { return replacement(); }).ToList();
}

用法:

originalList = GetModified(originalList, i => i.IsAwesome(), null);

但这也会让您在 cross-thread 操作中遇到麻烦。尽可能使用不可变实例,尤其是 IEnumerable。

如果你真的很想修改list的实例:

//if you ever want to also remove items, this is magic (why I iterate backwards)
for (int i = list.Count - 1; i >= 0; i--)
{
   if (condition)
   {
     list[i] = whateverYouWant;
   }
}

简而言之 - 不要修改列表。您可以使用

达到预期效果
a = a.Select(x => <some logic> ? x : default(T)).ToList()

一般来说,C# 中的列表在迭代期间是不可变的。您可以使用 .RemoveAll 或类似的方法。

您也可以使用简单的 for 循环。

void modify<T>(List<T> a)
{
    for (int i = 0; i < a.Count; i++)
    {
        if (condition(a[i]))
        {
            a[i] = default(T);
        }
    }
}

如文档 here 中所述,System.Collections.Generic.List<T>System.Collections.ArrayList 的通用实现,其索引访问器的复杂度为 O(1)。非常像 C++ std::vector<>,元素 insertion/addition 的复杂性是不可预测的,但访问是时间常数(complexity-wise,关于缓存等)。

你的 C++ 代码片段的等价物是 LinkedList

关于你的集合在迭代过程中的不可变性,在GetEnumerator方法here的文档中有明确说明。实际上,在枚举期间(在 foreach 内或直接使用 IEnumerator.MoveNext):

Enumerators can be used to read the data in the collection, but they cannot be used to modify the underlying collection.

此外,修改列表会使枚举器失效,通常会抛出异常:

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined.

我相信这种在各种类型的集合之间的接口契约一致性会导致您的误解:可以实现一个可变列表,但接口契约并不要求它。

假设您想实现一个在枚举期间可变的列表。枚举器是否会保留对条目或条目本身的引用(或检索方式)?条目本身会使它不可变,例如,在链接列表中插入元素时,引用将无效。

如果您想使用标准集合库,@Igor 提出的简单 for 循环似乎是最好的方法。否则,您可能需要自己重新实现它。