C#在List<T>中修改结构体

16

简短问题:如何修改 List 中的单个项目?(或者更准确地说,如何修改存储在 List 中的 struct 成员?)

完整解释:

首先,下面使用的 struct 定义:

public struct itemInfo
{
    ...(Strings, Chars, boring)...
    public String nameStr;
    ...(you get the idea, nothing fancy)...
    public String subNum;   //BTW this is the element I'm trying to sort on
}

public struct slotInfo
{
    public Char catID;
    public String sortName;
    public Bitmap mainIcon;
    public IList<itemInfo> subItems;
}

public struct catInfo
{
    public Char catID;
    public String catDesc;
    public IList<slotInfo> items;
    public int numItems;
}

catInfo[] gAllCats = new catInfo[31];

gAllCats在加载时被填充,随着程序运行而逐渐形成。

问题出现在我想要对subItems数组中的itemInfo对象进行排序时。 我使用LINQ来完成这个任务(因为似乎没有其他合理的方法来对非内置类型的列表进行排序)。 所以这就是我的代码:

foreach (slotInfo sInf in gAllCats[c].items)
{
    var sortedSubItems =
        from itemInfo iInf in sInf.subItems
        orderby iInf.subNum ascending
        select iInf;
    IList<itemInfo> sortedSubTemp = new List<itemInfo();
    foreach (itemInfo iInf in sortedSubItems)
    {
        sortedSubTemp.Add(iInf);
    }
    sInf.subItems.Clear();
    sInf.subItems = sortedSubTemp;   // ERROR: see below
}

错误为“Cannot modify members of 'sInf' because it is a 'foreach iteration variable'”。

a、这个限制没有意义;这不是foreach结构的主要用途吗?

b、(还出于怨恨)如果不修改列表,Clear()做了什么呢?(顺便说一下,如果我删除最后一行并运行它,List确实被清空了,根据调试器的显示。)

因此,我尝试采用不同的方法,并查看是否可以使用常规for循环来解决问题。(显然,这只有在gAllCats[c].items实际上是IList时才允许;我不认为它会允许您以这种方式索引常规List。)

for (int s = 0; s < gAllCats[c].items.Count; s++)
{
    var sortedSubItems =
        from itemInfo iInf in gAllCats[c].items[s].subItems
        orderby iInf.subNum ascending
        select iInf;
    IList<itemInfo> sortedSubTemp = new List<itemInfo>();
    foreach (itemInfo iInf in sortedSubItems)
    {
        sortedSubTemp.Add(iInf);
    }
    //NOTE: the following two lines were incorrect in the original post
    gAllCats[c].items[s].subItems.Clear();
    gAllCats[c].items[s].subItems = sortedSubTemp;   // ERROR: see below
}

这次出现的错误是"Cannot modify the return value of 'System.Collections.Generic.IList.this[int]' because it is not a variable."糟糕!它既不是变量,又什么呢?何时变成了'返回值'?

我知道一定有一种“正确”的方法来处理这个问题;我是从C背景下来的,我知道我可以在C中完成它(尽管需要进行大量手动内存管理)。

我搜索了一下,看起来ArrayList已经过时了,被泛型类型取代了(我使用的是3.0),而且我不能使用数组,因为大小需要是动态的。

5个回答

14
观察for循环的方法,这种错误(以及解决方法)在编译错误的文档中已经给出

尝试修改一个值类型,该类型是作为中间表达式的结果产生的,但未存储在变量中。当您尝试直接修改泛型集合中的结构体时,可能会发生此错误。

要修改结构体,请首先将其分配给局部变量,修改变量,然后将变量重新分配回集合中的项。

因此,在您的for循环中,更改以下行:
catSlots[s].subItems.Clear();
catSlots[s].subItems = sortedSubTemp;   // ERROR: see below

...转化为:

slotInfo tempSlot = gAllCats[0].items[s];
tempSlot.subItems  = sortedSubTemp;
gAllCats[0].items[s] = tempSlot;

我删除了对Clear方法的调用,因为我认为它没有什么作用。


谢谢您指出这一点。我知道修改结构体属性时的情况,但我不知道它适用于通用集合。我只是假设List本身是一个类,所以它的索引器将返回对结构体的引用。我想非泛型集合也是这样工作的。再次感谢。 - LoveMeSomeCode

4
你在foreach中遇到的问题是结构体是值类型,因此循环迭代变量实际上不是指向列表中结构体的引用,而是结构体的副本。
我猜测编译器禁止你进行更改,因为它很可能不会按照你的预期工作。 subItems.Clear()问题较小,因为虽然该字段可能是列表元素的副本,但它也是对列表的引用(浅拷贝)。
最简单的解决方案可能是将此项从struct更改为class。或者使用完全不同的方法,例如for (int ix = 0; ix < ...; ix++)等。

有没有办法使用引用类型执行foreach()循环?另外,关于“for”方法,我已经尝试过了,就像注释中所述...虽然出现了不同的错误。 - andersop
类是引用类型... 类可以在 foreach 中使用... 所以是的...(不确定这个问题为什么问)... 对于“for”方法的细节不够详细感到抱歉... 我可能没有足够地关注你的问题。不过,我看到 Fredrik 已经澄清了。 - jerryjvl

2
foreach循环不起作用,因为sInf是items内部结构的副本。更改sInf不会更改列表中的“实际”结构。
Clear可以工作,因为您没有更改sInf,而是更改了sInf内部的列表,并且Ilist始终是引用类型。
当您在IList上使用索引运算符时,同样的事情会发生-它返回副本而不是实际结构。如果编译器允许catSlots[s].subItems = sortedSubTemp;,则将修改副本的subItems,而不是实际结构。现在您知道为什么编译器说返回值不是变量-无法再引用副本。
有一个相当简单的解决方法-在副本上操作,然后用副本覆盖原始结构。
for (int s = 0; s < gAllCats[c].items.Count; s++)
{
            var sortedSubItems =
                            from itemInfo iInf in gAllCats[c].items[s].subItems
                            orderby iInf.subNum ascending
                            select iInf;
            IList<itemInfo> sortedSubTemp = new List<itemInfo>();
            foreach (itemInfo iInf in sortedSubItems)
            {
                            sortedSubTemp.Add(iInf);
            }
            var temp = catSlots[s];
            temp.subItems = sortedSubTemp;
            catSlots[s] = temp;
}

是的,这会导致两次复制操作,但这就是为了实现值语义而付出的代价。


啊,indexing[] 也是一个副本,但只有在列表上才是这样 -- 对于数组来说,它是一个值类型。好的,知道了,谢谢。 - andersop

1

你提到的这两个错误与你使用的结构体有关,在C#中,结构体是值类型而不是引用类型。

在foreach循环中,你完全可以使用引用类型。如果你将你的结构体改为类,你只需要这样做:

    foreach(var item in gAllCats[c].items)
    {
        item.subItems = item.subItems.OrderBy(x => x.subNum).ToList();
    }

使用结构体,这将需要更改为:

    for(int i=0; i< gAllCats[c].items.Count; i++)
    {
        var newitem = gAllCats[c].items[i];
        newitem.subItems = newitem.subItems.OrderBy(x => x.subNum).ToList();
        gAllCats[c].items[i] = newitem;
    }

其他答案已经提供了更好的关于结构体和类之间的区别的信息,但我觉得我可以帮助解决排序部分的问题。

1
如果将subItems从接口IList更改为具体的列表,那么您就可以使用Sort方法。
public List<itemInfo> subItems;

那么你的整个循环就变成了:

foreach (slotInfo sInf in gAllCats[c].items)
    sInf.subItems.Sort();

这不需要修改struct的内容(通常是一件好事)。struct的成员仍将指向完全相同的对象。

此外,在C#中使用struct的好理由非常少。GC非常好,您最好使用class,直到在分析器中演示了内存分配瓶颈。

更简洁地说,如果gAllCats[c].items中的items也是一个List,则可以编写:

gAllCats[c].items.ForEach(i => i.subItems.Sort());

编辑:你太容易放弃了! :)

Sort非常容易自定义。例如:

var simpsons = new[]
               {
                   new {Name = "Homer", Age = 37},
                   new {Name = "Bart", Age = 10},
                   new {Name = "Marge", Age = 36},
                   new {Name = "Grandpa", Age = int.MaxValue},
                   new {Name = "Lisa", Age = 8}
               }
               .ToList();

simpsons.Sort((a, b) => a.Age - b.Age);

这将按年龄从小到大排序。(C# 3的类型推断不是很好吗?)


那么我将必须定义对这些对象进行排序的含义——我真的只想按照一个特定的项目进行排序。我想这可能涉及到重载itemInfo的Sort()方法,但是关于这方面的文档真的很糟糕。实际上,我把gAllCats设置为了一个数组,因为我无法弄清楚如何在使用List时操作它。呃……不管怎样,谢谢。 - andersop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接