การแก้ไขค่าพจนานุกรมในลูป foreach


192

ฉันกำลังพยายามสร้างแผนภูมิวงกลมจากพจนานุกรม ก่อนที่ฉันจะแสดงแผนภูมิวงกลมฉันต้องการเก็บข้อมูลให้เป็นระเบียบ ฉันจะลบพายชิ้นใด ๆ ที่จะน้อยกว่า 5% ของพายแล้ววางลงในชิ้นพาย "อื่น ๆ " อย่างไรก็ตามฉันได้รับCollection was modified; enumeration operation may not executeข้อยกเว้นในขณะทำงาน

ฉันเข้าใจว่าทำไมคุณไม่สามารถเพิ่มหรือลบรายการออกจากพจนานุกรมในขณะที่วนซ้ำมัน อย่างไรก็ตามฉันไม่เข้าใจว่าทำไมคุณไม่สามารถเปลี่ยนค่าสำหรับคีย์ที่มีอยู่ภายในลูป foreach ได้

ข้อเสนอแนะใด ๆ : แก้ไขรหัสของฉันจะได้รับการชื่นชม

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

คำตอบ:


262

การตั้งค่าในพจนานุกรมจะอัปเดต "หมายเลขเวอร์ชัน" ภายใน - ซึ่งจะทำให้ตัววนซ้ำใช้ไม่ได้และตัววนซ้ำใด ๆ ที่เกี่ยวข้องกับคีย์หรือการรวบรวมค่า

ฉันเห็นจุดของคุณ แต่ในเวลาเดียวกันมันจะแปลกถ้าการรวบรวมค่าสามารถเปลี่ยนการทำซ้ำกลาง - และเพื่อความง่ายมีหมายเลขรุ่นเดียวเท่านั้น

วิธีปกติของการแก้ไขสิ่งนี้คือการคัดลอกคอลเลกชันของคีย์ไว้ก่อนแล้ววนซ้ำสำเนาหรือทำซ้ำในคอลเลกชันดั้งเดิม แต่เก็บชุดของการเปลี่ยนแปลงที่คุณจะนำไปใช้หลังจากทำซ้ำ

ตัวอย่างเช่น:

การคัดลอกคีย์ก่อน

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

หรือ...

การสร้างรายการการแก้ไข

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
ฉันรู้ว่ามันเก่า แต่ถ้าใช้. NET 3.5 (หรือมัน 4.0?) คุณสามารถใช้และละเมิด LINQ ดังต่อไปนี้: foreach (คีย์สตริงใน colStates.Keys.ToList ()) {... }
Machtyn

6
@ Mactyn: แน่นอน - แต่คำถามเฉพาะเกี่ยวกับ. NET 2.0 มิฉะนั้นฉันจะใช้ LINQ แน่นอน
Jon Skeet

"หมายเลขรุ่น" เป็นส่วนหนึ่งของสถานะที่มองเห็นได้ของพจนานุกรมหรือรายละเอียดการใช้งานหรือไม่?
คนขี้ขลาดนิรนาม

@SEinfringescopyright: ไม่สามารถมองเห็นได้โดยตรง ความจริงที่ว่าการอัปเดตพจนานุกรมจะทำให้ตัววนซ้ำใช้งานไม่ได้
Jon Skeet

จากกรอบงาน Microsoft Docs .NET 4.8 : คำสั่ง foreach เป็นตัวล้อมรอบตัวแจงนับซึ่งอนุญาตเฉพาะการอ่านจากคอลเลกชันเท่านั้นไม่ใช่การเขียนลงไป ดังนั้นฉันจะบอกว่ามันเป็นรายละเอียดการใช้งานที่สามารถเปลี่ยนแปลงได้ในรุ่นอนาคต และสิ่งที่มองเห็นได้คือผู้ใช้ของ Enumerator ผิดสัญญา แต่ฉันผิด ... มันจะปรากฏให้เห็นจริงๆเมื่อพจนานุกรมต่อเนื่องกัน
คนขี้ขลาดนิรนาม

81

โทรToList()ในforeachวง ด้วยวิธีนี้เราไม่จำเป็นต้องคัดลอกตัวแปรชั่วคราว ขึ้นอยู่กับ Linq ซึ่งมีให้บริการตั้งแต่. 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;
    }
}

การปรับปรุงที่ดีมาก!
SpeziFish

1
มันจะเป็นการดีกว่าหากใช้foreach(var pair in colStates.ToList())เพื่อหลีกเลี่ยงการเข้าถึงคีย์และค่าซึ่งหลีกเลี่ยงการโทรเข้าcolStates[key]..
2864740

21

คุณกำลังแก้ไขคอลเล็กชันในบรรทัดนี้:

colStates [key] = 0;

โดยการทำเช่นนั้นคุณกำลังลบและใส่กลับเข้าไปใหม่บางสิ่งบางอย่าง ณ จุดนั้น (เท่าที่ IEnumerable เป็นห่วงอยู่แล้ว)

หากคุณแก้ไขสมาชิกของค่าที่คุณกำลังจัดเก็บนั่นจะเป็นเรื่องปกติ แต่คุณกำลังแก้ไขค่าของตัวเองและ IEnumberable ไม่ชอบสิ่งนั้น

วิธีแก้ปัญหาที่ฉันใช้คือกำจัดลูป foreach และใช้สำหรับลูป การวนรอบง่ายจะไม่ตรวจสอบการเปลี่ยนแปลงที่คุณรู้ว่าจะไม่มีผลต่อการรวบรวม

นี่คือวิธีที่คุณสามารถทำได้:

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

ฉันได้รับปัญหานี้ใช้สำหรับวง พจนานุกรม [ดัชนี] [สำคัญ] = "abc" แต่จะแปลงกลับเป็นค่าเริ่มต้น "xyz"
Nick Chan Abdullah

1
การแก้ไขในรหัสนี้ไม่ใช่การวนซ้ำ: เป็นการคัดลอกรายการคีย์ (มันจะยังคงทำงานถ้าคุณแปลงมันห่วง foreach.) แก้โดยใช้สำหรับวงจะหมายถึงการใช้ในสถานที่ของcolStates.Keys keys
idbrii

6

คุณไม่สามารถแก้ไขคีย์หรือค่าโดยตรงใน ForEach แต่คุณสามารถแก้ไขสมาชิกได้ เช่นนี้ควรใช้งานได้:

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

ลองทำคำสั่ง linq กับพจนานุกรมของคุณแล้วผูกกราฟของคุณกับผลลัพธ์เหล่านั้น ...

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

Linq ไม่มีประโยชน์เฉพาะใน 3.5 หรือไม่? ฉันใช้. net 2.0
Aheho

คุณสามารถใช้งานได้จาก 2.0 โดยอ้างอิงถึงรุ่น 3.5 ของ System.Core.DLL - หากนั่นไม่ใช่สิ่งที่คุณต้องการรับแจ้งให้เราทราบและฉันจะลบคำตอบนี้
สกอตต์ไอวี่ย์

1
ฉันอาจจะไม่ใช้เส้นทางนี้ แต่ก็เป็นข้อเสนอแนะที่ดี ฉันแนะนำให้คุณทิ้งคำตอบไว้ในกรณีที่คนอื่นที่มีปัญหาเดียวกันสะดุด
Aheho

3

หากคุณรู้สึกสร้างสรรค์คุณสามารถทำสิ่งนี้ได้ วนกลับไปที่พจนานุกรมเพื่อทำการเปลี่ยนแปลง

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

}

ไม่เหมือนกันแน่นอน แต่คุณอาจสนใจต่อไป ...


2

คุณจำเป็นต้องสร้างพจนานุกรมใหม่จากเก่าแทนที่จะแก้ไขในสถานที่ Somethine like (เช่นวนซ้ำ KeyValuePair <,> แทนที่จะใช้การค้นหาคีย์:

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

เริ่มต้นด้วย. NET 4.5 คุณสามารถทำได้ด้วยConcurrentDictionary :

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

อย่างไรก็ตามโปรดทราบว่าประสิทธิภาพการทำงานนั้นแย่ลงกว่าเดิมอย่างมาก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>();
    }
}

ผลลัพธ์:

              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

คุณไม่สามารถแก้ไขคอลเล็กชันได้แม้จะเป็นค่า คุณสามารถบันทึกกรณีเหล่านี้และลบออกในภายหลัง มันจะจบลงเช่นนี้:

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

คุณสามารถปรับเปลี่ยนคอลเลกชัน คุณไม่สามารถใช้ตัววนซ้ำกับคอลเลกชันที่ได้รับการแก้ไขต่อไป
2864740

0

คำเตือน: ฉันไม่ได้ทำ C # มาก

คุณกำลังพยายามปรับเปลี่ยน DictionaryEntry วัตถุที่เก็บไว้ใน HashTable Hashtable เก็บเพียงหนึ่งวัตถุ - อินสแตนซ์ของ DictionaryEntry ของคุณ การเปลี่ยนคีย์หรือค่านั้นเพียงพอที่จะเปลี่ยน HashTable และทำให้ตัวแจงนับไม่ถูกต้อง

คุณสามารถทำได้นอกวง

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

โดยการสร้างรายการคีย์ทั้งหมดของค่าที่คุณต้องการเปลี่ยนและทำซ้ำในรายการนั้นแทน


0

คุณสามารถทำสำเนารายการของdict.Valuesจากนั้นคุณสามารถใช้List.ForEachฟังก์ชั่นแลมบ์ดาสำหรับการทำซ้ำ (หรือforeachวนซ้ำตามที่แนะนำไว้ก่อนหน้านี้)

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

0

นอกเหนือจากคำตอบอื่น ๆ ฉันคิดว่าฉันทราบว่าถ้าคุณได้รับsortedDictionary.KeysหรือsortedDictionary.Valuesจากนั้นวนรอบพวกเขาด้วยforeachคุณจะต้องเรียงลำดับ นี่เป็นเพราะวิธีการเหล่านั้นส่งคืน System.Collections.Generic.SortedDictionary<TKey,TValue>.KeyCollectionหรือSortedDictionary<TKey,TValue>.ValueCollectionวัตถุที่รักษาเรียงลำดับของพจนานุกรมต้นฉบับ


0

คำตอบนี้ใช้สำหรับการเปรียบเทียบโซลูชันสองวิธีไม่ใช่โซลูชันที่แนะนำ

แทนที่จะสร้างรายการอื่นตามคำแนะนำอื่น ๆ คุณสามารถใช้การforวนซ้ำโดยใช้พจนานุกรมCountสำหรับเงื่อนไขการหยุดแบบวนซ้ำและKeys.ElementAt(i)เพื่อรับรหัส

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

ฉันคิดว่าสิ่งนี้จะมีประสิทธิภาพมากขึ้นเพราะเราไม่จำเป็นต้องสร้างรายการคีย์ หลังจากใช้การทดสอบฉันพบว่าforโซลูชันลูปมีประสิทธิภาพน้อยกว่ามาก เหตุผลก็เพราะElementAtเป็น O (n) บนdictionary.Keysคุณสมบัติมันค้นหาจากจุดเริ่มต้นของคอลเลกชันจนกว่าจะได้รับไปยังรายการที่ n

ทดสอบ:

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");

ผล:

Creating dictionary...
Done
Starting tests...
for loop Test:     2367 ms
foreach loop Test: 3 ms
Done
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.