为什么在将结构体用作通用字典键时,C#会生成垃圾?

9
我有一个通用字典,由一个结构体作为键和一个类引用作为值组成。
Dictionary<IntVector2, SpriteRenderer> tileRendererMap;

我通过坐标检索渲染器的引用,并进行以下更改:
tileRendererMap[tileCoord].color = Color.cyan;

每次使用它都会产生0.8KB的垃圾。这是默认行为吗?我认为查找字典应该是最高效的操作之一。由于我正在为移动平台工作,而且这是一个关键系统,所以我需要找到一种解决方法。
有什么想法可能是我的问题或如何获得无需分配的查找?
更多测试后编辑:
使用int作为键而不是我的自定义结构按预期工作。没有分配,没有问题,我认为使用ID作为键不是奇怪的事情。然而,对于我的游戏,我想将与网格中特定瓦片相关联的渲染器存储在一个网格中,这实际上不需要任何类型的对象,因为我只关心网格位置。因此,我认为IntVector2可能是一个实用的标识符,但如果必须,我可以绕过它。
当使用我的结构时,我会生成垃圾,我使用Unity3D Profiler进行测量。它在Dictionary_get_item中报告了0.8KB,特别是DefaultComparer.Equals()。显然,这是一个装箱/取消装箱问题,但即使我实现了我的自定义重写,它仍然会生成垃圾,只比以前少一点。
我的基本结构实现:
public struct IntVector2
{
    public int x, y;

    public override bool Equals(object obj)
    {
        if (obj == null || obj is IntVector2 == false)
            return false;

        var data = (IntVector2)obj;
        return x == data.x && y == data.y;
    }

    public override int GetHashCode()
    {
        return x.GetHashCode() ^ y.GetHashCode();
    }
}

“接受答案后编辑:”
“当在通用字典中使用时,我的结构体的无分配版本。”
public struct IntVector2 : IEquatable<IntVector2>
{
    public int x;
    public int y;

    public IntVector2(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    public bool Equals(IntVector2 other)
    {
        return x == other.x && y == other.y;
    }
}

字典类使用IEquatable接口而不是对象重写Equals方法,这就是为什么我的第一个实现在搜索键时每次检查相等性都会将其中一个值装箱的原因。

1
当需要时,0.8kb的垃圾应该自动清除。 - Master117
你是如何确定产生了0.8 kb的垃圾?你分析了那些垃圾包含的类型吗? - Lasse V. Karlsen
它根本不会生成任何垃圾,你只是在SpriteRenderer对象上设置属性。 "颜色"是引用类型的可能性非常小,甚至更不可能占用800字节。 你测量垃圾的方式有问题。 - Hans Passant
我正在使用Unity 3D Profiler 进行测量。它报告了dictionary_get_item中的分配情况,特别是在DefaultComparer Equals方法中。我现在正在测试是否可以通过改变比较器来获得不同的结果。 - Xarbrough
2个回答

14

由于您只重写了 Equals 方法,而没有实现 IEquatable<IntVector2> 接口,因此在字典比较两个实例是否相等时,字典会将其中一个实例装箱(boxing),因为它将一个实例传递给了一个接收 objectEquals 方法。

如果您实现 IEquatable<IntVector2> 接口,那么字典就可以(也将会)使用接受 IntVector2 参数的 Equals 版本进行比较,它不需要进行装箱操作。


2
这样做就可以了。从分析器消息中,我推断它调用了常规的重写 Equals,但使用接口确实变成了无垃圾调用。这正是我所希望的。我仍然应该重新考虑字典键在这个上下文中是否有意义。 - Xarbrough

0
你的 IntVector2 是一个 struct 类型。但不确定使用 struct 作为字典键的意义是什么?请记住,struct 是值类型,因此每个键都会复制值,并且根据该结构体的大小可能会生成相应的内存占用。

你可能想要使用该结构体的特定属性作为键。例如,你的结构体

public struct IntVector2
{
  string key;
}

然后你可以像使用那个属性一样使用它

Dictionary<string, SpriteRenderer> tileRendererMap;

Intvector2 iv2 = new Intvector2();
iv2.key = "test";

tileRendererMap = new Dictionary<string, SpriteRenderer>();
tileRendererMap.Add(iv2.key, new SpriteRenderer{prop1 = value});

那么,将值分配给作为结构体的字典键会首先为该键构建全新的结构体,然后再将新值分配给它?为什么不在找到不可变结构体后将其保持不变,并将新的字典值分配给它呢? - Quantic
2
@Quantic - 尽管您在键周围使用方括号,但仍在调用方法并将结构作为参数传递,因此它仍然必须被复制到堆栈上。 - Richard Irons
3
使用结构体作为字典的键有什么意义呢?但是,人们经常使用int作为字典键,而int本身就是一个结构体。 - Arturo Torres Sánchez
@RichardIrons 啊,这很有道理。我想我们需要知道分析器是如何工作的,因为将结构参数复制到堆栈上不应该显示为GC必须处理的“垃圾”,对吧?OP只是看到堆栈内存使用量增加,然后将其归因于“垃圾”吗? - Quantic
@ArturoTorresSánchez,使用INT与使用自定义结构体是不同的,因为您定义的结构体可能会更加复杂,并且可能需要比只需要4个字节的INT占用更多的空间。这就是我在那个声明中所指的。 - Rahul
显示剩余2条评论

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