Chỉnh sửa giá trị từ điển trong vòng lặp foreach


191

Tôi đang cố gắng xây dựng một biểu đồ hình tròn từ một từ điển. Trước khi tôi hiển thị biểu đồ hình tròn, tôi muốn thu dọn dữ liệu. Tôi đang loại bỏ bất kỳ lát bánh nào nhỏ hơn 5% chiếc bánh và đặt chúng vào lát bánh "Khác". Tuy nhiên tôi đang nhận được một Collection was modified; enumeration operation may not executengoại lệ trong thời gian chạy.

Tôi hiểu lý do tại sao bạn không thể thêm hoặc xóa các mục khỏi từ điển trong khi lặp lại chúng. Tuy nhiên tôi không hiểu tại sao bạn không thể thay đổi giá trị cho khóa hiện có trong vòng lặp foreach.

Bất kỳ đề xuất nào: sửa mã của tôi, sẽ được đánh giá cao.

Dictionary<string, int> colStates = new Dictionary<string,int>();
// ...
// Some code to populate colStates dictionary
// ...

int OtherCount = 0;

foreach(string key in colStates.Keys)
{

    double  Percent = colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

colStates.Add("Other", OtherCount);

Câu trả lời:


259

Đặt giá trị trong từ điển sẽ cập nhật "số phiên bản" bên trong của nó - làm mất hiệu lực của trình lặp và bất kỳ trình lặp nào được liên kết với bộ sưu tập khóa hoặc giá trị.

Tôi thấy quan điểm của bạn, nhưng đồng thời sẽ là kỳ quặc nếu bộ sưu tập giá trị có thể thay đổi giữa lần lặp - và để đơn giản chỉ có một số phiên bản.

Cách sửa lỗi thông thường này là sao chép bộ sưu tập khóa trước và lặp lại bản sao hoặc lặp lại bộ sưu tập gốc nhưng duy trì bộ sưu tập các thay đổi mà bạn sẽ áp dụng sau khi lặp xong.

Ví dụ:

Sao chép khóa trước

List<string> keys = new List<string>(colStates.Keys);
foreach(string key in keys)
{
    double percent = colStates[key] / TotalCount;    
    if (percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

Hoặc là...

Tạo một danh sách sửa đổi

List<string> keysToNuke = new List<string>();
foreach(string key in colStates.Keys)
{
    double percent = colStates[key] / TotalCount;    
    if (percent < 0.05)
    {
        OtherCount += colStates[key];
        keysToNuke.Add(key);
    }
}
foreach (string key in keysToNuke)
{
    colStates[key] = 0;
}

24
Tôi biết điều này đã cũ, nhưng nếu sử dụng .NET 3.5 (hoặc là 4.0?), Bạn có thể sử dụng và lạm dụng LINQ như sau: foreach (khóa chuỗi trong colStates.Keys.ToList ()) {...}
Machtyn

6
@Machtyn: Chắc chắn rồi - nhưng câu hỏi là cụ thể về .NET 2.0, nếu không tôi chắc chắn sẽ có được sử dụng LINQ.
Jon Skeet

Là "số phiên bản" của trạng thái hiển thị của Từ điển hoặc chi tiết triển khai?
Kẻ hèn nhát vô danh

@SEinfringescopyright: Không thể nhìn thấy trực tiếp; thực tế là việc cập nhật từ điển làm mất hiệu lực trình lặp vẫn hiển thị.
Jon Skeet

Từ Microsoft Docs .NET framework 4.8 : Câu lệnh foreach là một trình bao bọc xung quanh trình liệt kê, chỉ cho phép đọc từ bộ sưu tập, không ghi vào nó. Vì vậy, tôi sẽ nói rằng đó là một chi tiết triển khai có thể thay đổi trong phiên bản tương lai. Và những gì có thể nhìn thấy là người dùng của Enumerator đã vi phạm hợp đồng của nó. Nhưng tôi đã nhầm ... nó thực sự hiển thị khi một Từ điển được xuất bản.
Kẻ hèn nhát vô danh

81

Gọi ToList()trong foreachvòng lặp. Bằng cách này, chúng tôi không cần một bản sao biến tạm thời. Nó phụ thuộc vào Linq có sẵn kể từ .Net 3.5.

using System.Linq;

foreach(string key in colStates.Keys.ToList())
{
  double  Percent = colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

Cải tiến rất tốt đẹp!
SpeziFish

1
Sẽ tốt hơn nếu sử dụng foreach(var pair in colStates.ToList())để tránh có quyền truy cập vào Khóa Giá trị mà tránh phải gọi vào colStates[key]..
user2864740

21

Bạn đang sửa đổi bộ sưu tập trong dòng này:

colStates [key] = 0;

Bằng cách làm như vậy, về cơ bản, bạn đang xóa và xác nhận lại một cái gì đó tại thời điểm đó (theo như IEnumerable có liên quan.

Nếu bạn chỉnh sửa một thành viên của giá trị bạn đang lưu trữ, điều đó sẽ ổn, nhưng bạn đang chỉnh sửa giá trị đó và IEnumberable không như vậy.

Giải pháp tôi đã sử dụng là loại bỏ vòng lặp foreach và chỉ sử dụng vòng lặp for. Một vòng lặp đơn giản sẽ không kiểm tra các thay đổi mà bạn biết sẽ không ảnh hưởng đến bộ sưu tập.

Đây là cách bạn có thể làm điều đó:

List<string> keys = new List<string>(colStates.Keys);
for(int i = 0; i < keys.Count; i++)
{
    string key = keys[i];
    double  Percent = colStates[key] / TotalCount;
    if (Percent < 0.05)    
    {        
        OtherCount += colStates[key];
        colStates[key] = 0;    
    }
}

Tôi nhận được vấn đề này bằng cách sử dụng vòng lặp. dictionary [index] [key] = "abc", nhưng nó trở lại giá trị ban đầu "xyz"
Nick Chan Abdullah

1
Khắc phục trong mã này không phải là vòng lặp for: nó sao chép danh sách các khóa. (Nó vẫn hoạt động nếu bạn chuyển đổi nó thành vòng lặp foreach.) Giải quyết bằng cách sử dụng vòng lặp for có nghĩa là sử dụng colStates.Keysthay thế keys.
idbrii

6

Bạn không thể sửa đổi các khóa cũng như các giá trị trực tiếp trong ForEach, nhưng bạn có thể sửa đổi các thành viên của chúng. Ví dụ, điều này sẽ làm việc:

public class State {
    public int Value;
}

...

Dictionary<string, State> colStates = new Dictionary<string,State>();

int OtherCount = 0;
foreach(string key in colStates.Keys)
{
    double  Percent = colStates[key].Value / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key].Value;
        colStates[key].Value = 0;
    }
}

colStates.Add("Other", new State { Value =  OtherCount } );

3

Còn về việc thực hiện một số truy vấn linq đối với từ điển của bạn, và sau đó ràng buộc biểu đồ của bạn với kết quả của những điều đó thì sao? ...

var under = colStates.Where(c => (decimal)c.Value / (decimal)totalCount < .05M);
var over = colStates.Where(c => (decimal)c.Value / (decimal)totalCount >= .05M);
var newColStates = over.Union(new Dictionary<string, int>() { { "Other", under.Sum(c => c.Value) } });

foreach (var item in newColStates)
{
    Console.WriteLine("{0}:{1}", item.Key, item.Value);
}

Không phải Linq chỉ có sẵn trong 3.5? Tôi đang sử dụng .net 2.0.
Aheho

Bạn có thể sử dụng nó từ 2.0 với tham chiếu đến phiên bản System.Core.DLL 3.5 - nếu đó không phải là điều bạn muốn thực hiện hãy cho tôi biết và tôi sẽ xóa câu trả lời này.
Scott Ivey

1
Tôi có thể sẽ không đi theo con đường này, tuy nhiên đó là một gợi ý tốt. Tôi đề nghị bạn để câu trả lời tại chỗ trong trường hợp người khác có cùng vấn đề vấp phải nó.
Aheho

3

Nếu bạn cảm thấy sáng tạo, bạn có thể làm một cái gì đó như thế này. Lặp lại thông qua từ điển để thực hiện các thay đổi của bạn.

Dictionary<string, int> collection = new Dictionary<string, int>();
collection.Add("value1", 9);
collection.Add("value2", 7);
collection.Add("value3", 5);
collection.Add("value4", 3);
collection.Add("value5", 1);

for (int i = collection.Keys.Count; i-- > 0; ) {
    if (collection.Values.ElementAt(i) < 5) {
        collection.Remove(collection.Keys.ElementAt(i)); ;
    }

}

Chắc chắn là không giống nhau, nhưng dù sao bạn cũng có thể quan tâm ...


2

Bạn cần tạo một Từ điển mới từ cũ thay vì sửa đổi tại chỗ. Đôi khi thích (cũng lặp đi lặp lại trên KeyValuePair <,> thay vì sử dụng tra cứu khóa:

int otherCount = 0;
int totalCounts = colStates.Values.Sum();
var newDict = new Dictionary<string,int>();
foreach (var kv in colStates) {
  if (kv.Value/(double)totalCounts < 0.05) {
    otherCount += kv.Value;
  } else {
    newDict.Add(kv.Key, kv.Value);
  }
}
if (otherCount > 0) {
  newDict.Add("Other", otherCount);
}

colStates = newDict;

1

Bắt đầu với .NET 4.5 Bạn có thể thực hiện việc này với ConcurrencyDipedia :

using System.Collections.Concurrent;

var colStates = new ConcurrentDictionary<string,int>();
colStates["foo"] = 1;
colStates["bar"] = 2;
colStates["baz"] = 3;

int OtherCount = 0;
int TotalCount = 100;

foreach(string key in colStates.Keys)
{
    double Percent = (double)colStates[key] / TotalCount;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        colStates[key] = 0;
    }
}

colStates.TryAdd("Other", OtherCount);

Tuy nhiên, lưu ý rằng hiệu suất của nó thực sự tệ hơn nhiều so với đơn giản foreach dictionary.Kes.ToArray():

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class ConcurrentVsRegularDictionary
{
    private readonly Random _rand;
    private const int Count = 1_000;

    public ConcurrentVsRegularDictionary()
    {
        _rand = new Random();
    }

    [Benchmark]
    public void ConcurrentDictionary()
    {
        var dict = new ConcurrentDictionary<int, int>();
        Populate(dict);

        foreach (var key in dict.Keys)
        {
            dict[key] = _rand.Next();
        }
    }

    [Benchmark]
    public void Dictionary()
    {
        var dict = new Dictionary<int, int>();
        Populate(dict);

        foreach (var key in dict.Keys.ToArray())
        {
            dict[key] = _rand.Next();
        }
    }

    private void Populate(IDictionary<int, int> dictionary)
    {
        for (int i = 0; i < Count; i++)
        {
            dictionary[i] = 0;
        }
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<ConcurrentVsRegularDictionary>();
    }
}

Kết quả:

              Method |      Mean |     Error |    StdDev |
--------------------- |----------:|----------:|----------:|
 ConcurrentDictionary | 182.24 us | 3.1507 us | 2.7930 us |
           Dictionary |  47.01 us | 0.4824 us | 0.4512 us |

1

Bạn không thể sửa đổi bộ sưu tập, thậm chí không có giá trị. Bạn có thể lưu những trường hợp này và loại bỏ chúng sau. Nó sẽ kết thúc như thế này:

Dictionary<string, int> colStates = new Dictionary<string, int>();
// ...
// Some code to populate colStates dictionary
// ...

int OtherCount = 0;
List<string> notRelevantKeys = new List<string>();

foreach (string key in colStates.Keys)
{

    double Percent = colStates[key] / colStates.Count;

    if (Percent < 0.05)
    {
        OtherCount += colStates[key];
        notRelevantKeys.Add(key);
    }
}

foreach (string key in notRelevantKeys)
{
    colStates[key] = 0;
}

colStates.Add("Other", OtherCount);

Bạn có thể sửa đổi bộ sưu tập. Bạn không thể tiếp tục sử dụng một trình vòng lặp cho một bộ sưu tập đã sửa đổi.
dùng2864740

0

Tuyên bố miễn trừ trách nhiệm: Tôi không làm nhiều C #

Bạn đang cố gắng sửa đổi đối tượng DictionaryEntry được lưu trữ trong HashTable. Hashtable chỉ lưu trữ một đối tượng - ví dụ DictionaryEntry của bạn. Thay đổi Khóa hoặc Giá trị là đủ để thay đổi HashTable và khiến cho điều tra viên trở nên không hợp lệ.

Bạn có thể làm điều đó bên ngoài vòng lặp:

if(hashtable.Contains(key))
{
    hashtable[key] = value;
}

bằng cách trước tiên tạo một danh sách tất cả các khóa của các giá trị bạn muốn thay đổi và lặp lại qua danh sách đó.


0

Bạn có thể tạo một bản sao danh sách dict.Values, sau đó bạn có thể sử dụng List.ForEachhàm lambda cho phép lặp, (hoặc một foreachvòng lặp, như được đề xuất trước đó).

new List<string>(myDict.Values).ForEach(str =>
{
  //Use str in any other way you need here.
  Console.WriteLine(str);
});

0

Cùng với các câu trả lời khác, tôi nghĩ tôi muốn lưu ý rằng nếu bạn nhận được sortedDictionary.Keyshoặc sortedDictionary.Valuessau đó lặp lại chúng foreach, bạn cũng sẽ thực hiện theo thứ tự được sắp xếp. Điều này là do các phương thức đó trả về System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionhoặc SortedDictionary<TKey,TValue>.ValueCollectioncác đối tượng, duy trì loại từ điển gốc.


0

Câu trả lời này là để so sánh hai giải pháp, không phải là một giải pháp được đề xuất.

Thay vì tạo một danh sách khác như các câu trả lời khác được đề xuất, bạn có thể sử dụng một forvòng lặp bằng từ điển Countcho điều kiện dừng vòng lặp và Keys.ElementAt(i)để lấy khóa.

for (int i = 0; i < dictionary.Count; i++)
{
    dictionary[dictionary.Keys.ElementAt(i)] = 0;
}

Tại Firs tôi nghĩ rằng điều này sẽ hiệu quả hơn vì chúng ta không cần phải tạo một danh sách chính. Sau khi chạy thử tôi thấy rằng forgiải pháp vòng lặp ít hơn nhiều hiệu quả . Tại danh sách trên PC của tôi.

Kiểm tra:

int iterations = 10;
int dictionarySize = 10000;
Stopwatch sw = new Stopwatch();

Console.WriteLine("Creating dictionary...");
Dictionary<string, int> dictionary = new Dictionary<string, int>(dictionarySize);
for (int i = 0; i < dictionarySize; i++)
{
    dictionary.Add(i.ToString(), i);
}
Console.WriteLine("Done");

Console.WriteLine("Starting tests...");

// for loop test
sw.Restart();
for (int i = 0; i < iterations; i++)
{
    for (int j = 0; j < dictionary.Count; j++)
    {
        dictionary[dictionary.Keys.ElementAt(j)] = 3;
    }
}
sw.Stop();
Console.WriteLine($"for loop Test:     {sw.ElapsedMilliseconds} ms");

// foreach loop test
sw.Restart();
for (int i = 0; i < iterations; i++)
{
    foreach (string key in dictionary.Keys.ToList())
    {
        dictionary[key] = 3;
    }
}
sw.Stop();
Console.WriteLine($"foreach loop Test: {sw.ElapsedMilliseconds} ms");

Console.WriteLine("Done");

Các kết quả:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.