在字典中修改结构变量

37

我有一个这样的结构体:

public struct MapTile
{
    public int bgAnimation;
    public int bgFrame;
}

但是,当我使用 foreach 循环来更改动画帧时,我无法这样做...

下面是代码:

foreach (KeyValuePair<string, MapTile> tile in tilesData)
{
        if (tilesData[tile.Key].bgFrame >= tilesData[tile.Key].bgAnimation)
        {
            tilesData[tile.Key].bgFrame = 0;
        }
        else
        {
            tilesData[tile.Key].bgFrame++;
        }
}

它给我编译错误:

Error 1 Cannot modify the return value of 'System.Collections.Generic.Dictionary<string,Warudo.MapTile>.this[string]' because it is not a variable
Error 2 Cannot modify the return value of 'System.Collections.Generic.Dictionary<string,Warudo.MapTile>.this[string]' because it is not a variable

为什么我无法更改字典中结构体内的值?


14
呃……可变结构体。(必须有人提出这个问题) - Justin Niessner
3
@Justin Niessner,这为什么不好? - NewProger
2
@Lurler - 因为它们可能导致大规模混乱。人们开始像使用类一样使用它们,并期望它们的行为与类相同...但事实并非如此(这就是你在代码中遇到问题的原因)。 - Justin Niessner
2
@Lurler:这正是可变结构的问题所在——行为很棘手。大多数情况下,出于性能原因选择结构体是一种过早的优化。最好编写可用和可维护的代码,然后进行分析,必要时再进行优化。在这种情况下,我怀疑这实际上不会更快,特别是如果它存储在字典中。 - Reed Copsey
@Justin Niessner,我使用结构体是因为我习惯在Delphi中使用它们... 我希望它们可以以类似的方式工作,但我错了... - NewProger
显示剩余2条评论
5个回答

49
索引器将返回该值的一个“副本”。对该副本进行更改不会影响字典中的值...编译器阻止您编写有错误的代码。如果要修改字典中的值,您需要使用类似以下的内容:
// Note: copying the contents to start with as you can't modify a collection
// while iterating over it
foreach (KeyValuePair<string, MapTile> pair in tilesData.ToList())
{
    MapTile tile = pair.Value;
    tile.bgFrame = tile.bgFrame >= tile.bgAnimation ? 0 : tile.bgFrame + 1;
    tilesData[pair.Key] = tile;
}

请注意,这也避免了无谓的多次查找,而你原来的代码就是这样做的。
个人建议不要使用可变结构体作为起点...
当然,另一个选择是将其作为引用类型,并在此时使用:
// If MapTile is a reference type...
// No need to copy anything this time; we're not changing the value in the
// dictionary, which is just a reference. Also, we don't care about the
// key this time.
foreach (MapTile tile in tilesData.Values)
{
    tile.bgFrame = tile.bgFrame >= tile.bgAnimation ? 0 : tile.bgFrame + 1;
}

5
这就是为什么我发布了一个评论而不是一个答案。当可变结构体的话题出现时,我知道Jon Skeet一定会跟着出现。 - Justin Niessner
啊,现在我明白问题出在哪里了。谢谢你的帮助! - NewProger
1
@JustinNiessner 无论如何,Jon Skeet提供了非常好的和整洁的答案! - marc wellman

7

tilesData[tile.Key]不是一个存储位置(即它不是一个变量)。它是与字典tilesData中键tile.Key相关联的MapTile实例的副本。这就是struct的工作原理。它们的实例副本被传递和返回到各个地方(这也是为什么可变结构被认为是邪恶的重要原因之一)。

你需要做的是:

    MapTile tile = tilesData[tile.Key];
    if (tile.bgFrame >= tile.bgAnimation)
    {
        tile.bgFrame = 0;
    }
    else
    {
        tile.bgFrame++;
    }
    tilesData[tile.Key] = tile;

我以为结构体不是引用类型,而是变量类型...奇怪。谢谢你的解释。 - NewProger
现在我明白为什么它们被认为是邪恶的了 :) 我可能会在未来改变它的工作方式 :) - NewProger
1
struct 是值类型。值类型的显著特点是它们按值复制,并且相等性由值相等性定义。 - jason

2

自 C# 10.0 开始(由 @GuruStron 友情修复),您可以使用 with 语法来创建一个新的副本,覆盖任意字段。

mapTile = mapTile with { bgAnimation = 1 };

1
C# 10,而不是C# 9。文档。据我所知,C# 9仅为记录引入了此关键字。 - Guru Stron

2
我建议创建一个实用类:
public class MutableHolder<T>
{
    public T Value;
    public MutableHolder(T value)
    {
        this.Value = value;
    }
}

然后,在每个字典槽中存储一个新创建的MutableHolder<MapTile>,而不是直接存储MapTile。这样可以轻松地更新与任何特定键相关联的地图图块,而无需修改字典本身(否则,至少会使您的foreach循环使用的枚举器无效)。


这个框架里已经有通用的MutableHolder<T>了吗?我现在应该使用它吗?我大多数情况下都是自己创建的。 - Michael Covelli
@MichaelCovelli:可以使用一个单元素数组,当然,这在一开始就是可用的,但我不知道是否存在与上述相等的标准框架实用类。另一方面,MutableHolder足够简单,使用自己的并没有太多的劣势。 - supercat

-2

将结构体转换为类

之前:

public struct MapTile
{
    public int bgAnimation;
    public int bgFrame;
}

之后:

public Class MapTile
{
    public int bgAnimation;
    public int bgFrame;
}

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