可变结构体 vs. 类?

3

我不确定是否使用可变结构体或可变类。 我的程序存储了一个包含大量对象的数组。 我注意到使用类会使所需内存量加倍。但是,我希望这些对象是可变的,并且有人告诉我使用可变结构体是邪恶的。 这是我的类型:

struct /* or class */ Block
{
    public byte ID;
    public bool HasMetaData; // not sure whether HasMetaData == false or
                             // MetaData == null is faster, might remove this
    public BlockMetaData MetaData; // BlockMetaData is always a class reference
}

分配大量对象,例如(请注意,下面的两个代码都运行了81次):

// struct
Block[,,] blocks = new Block[16, 256, 16];

使用这种方法时,大约会占用35 MiB的内存:

// class
Block[,,] blocks = new Block[16, 256, 16];
for (int z = 0; z < 16; z++)
for (int y = 0; y < 256; y++)
for (int x = 0; x < 16; x++)
    blocks[x, y, z] = new Block();

使用约100 MiB的内存。

综上所述,我的问题如下:

对于我的Block类型,我应该使用struct还是class?实例应该是可变的,并存储几个值和一个对象引用。


6
你是否考虑过使用不可变结构体并交换整个值呢?就像 DateTime.AddDays(...) 返回一个新的/不同的 DateTime,但 DateTime 本身永远不会改变。 - Marc Gravell
4
a) 为什么它们应该是可变的?这一点在这里并不明显。 b) 100 MB 不是很多。 - H H
5
存储HasMetaData没有意义,这会浪费资源(它会被对齐等),而空值检查很简单 - 不比bool值检查更差。 - Marc Gravell
10
马克和亨克是正确的。首先,如果您担心内存使用,则应消除所有可能的冗余。保留一个必须与指针同步的布尔值不仅浪费内存,而且容易出现错误。第二,我看不出为什么这个结构体应该是可变的。第三,为什么您需要这个结构体呢?它似乎将字节ID与元数据块相关联;在世界上只有256个可能的BlockMetaData吗?或者很多它们具有相同的ID?您可能可以找到更有效的数据结构,从而更节省空间。 - Eric Lippert
3
第四点,如果你真的想节省内存,那么可以分配两个数组:一个字节数组和一个引用数组。请记住,引用必须对齐到字边界,因此包含一个引用和一个字节的结构体与包含两个引用的结构体使用的内存量相同。 - Eric Lippert
显示剩余2条评论
3个回答

16

首先,如果你真的想节省内存,那么就不要使用结构体或类。

byte[,,] blockTypes = new byte[16, 256, 16]; 
BlockMetaData[,,] blockMetadata = new BlockMetaData[16, 256, 16];

您想要紧密地将类似的东西放在内存中。如果可能,您绝不想将一个字节放在引用旁边; 这样的结构将自动浪费三到七个字节。在.NET中,引用必须对齐。

其次,我假设您正在构建一个体素系统。根据它们的分布,可能有更好的方法来表示体素,而不是使用3D数组。如果您将制作大量这些物品,则将它们存储在不可变的八叉树中。通过使用不可变八叉树的持久化特性,您可以创建具有四万亿体素的立方体结构,只要您所代表的宇宙是“成团”的。也就是说,整个世界都存在大量相似区域。您会换取略大的O(lg n)时间来访问和更改元素,但您将得到更多可操作的元素。

第三,“ID”是表示“类型”的一种非常糟糕的方式。当我看到“ID”时,我会认为该数字唯一地标识该元素,而不是描述它。考虑将名称更改为更少混淆的名称。

第四,有多少元素具有元数据?如果具有元数据的元素数量与总元素数量相比较小,则可以比使用引用数组更好地完成。考虑稀疏数组方法; 稀疏数组更具空间效率。


谢谢你的回答。老实说,我并没有完全理解你所说的内容(部分原因是英语不是我的母语,另一方面是因为我从来都不擅长数学),但我一定会去研究一下,因为它看起来非常有趣。再次感谢! - haiyyu
@Eric:你的帖子正在这里讨论。 - Patrick Hofman

4

它们真的需要可变吗?您可以将其制作为不可变结构体,并使用方法创建一个新值,其中一个字段不同:

struct Block
{
    // I'd definitely get rid of the HasMetaData
    private readonly byte id;
    private readonly BlockMetaData metaData;

    public Block(byte id, BlockMetaData metaData)
    {
        this.id = id;
        this.metaData = metaData;
    }

    public byte Id { get { return id; } }
    public BlockMetaData MetaData { get { return metaData; } }

    public Block WithId(byte newId)
    {
        return new Block(newId, metaData);
    }

    public Block WithMetaData(BlockMetaData newMetaData)
    {
        return new Block(id, newMetaData);
    }
}

说实话,我仍然不确定是否将其设置为结构体 - 但无论如何,我都会尝试使其不可变。

在内存和速度方面,您的性能要求是什么?不可变类与这些要求有多接近?


看了你的代码,它们并不一定要是可变的。你现在写的方式也可以正常工作。谢谢你。我的性能需求并不是很高,虽然我不想在这样的项目中浪费内存,如果有更便宜的方法就应该采用。我不太理解你的第二个问题,对不起,因为我也不太明白不可变类的意义所在。难道不应该创建不会改变的类型结构体更加明智吗? - haiyyu
@haiyyu:不,那绝对不是唯一的标准。以string为例-那是一个不可变类,我非常高兴它是这样的。不可变性通常会使代码更易读,并且更简单地实现线程安全-当然要适度使用。 - Jon Skeet
@JonSkeet:不可变性可能是一件好事,但结构体实例的不可变性取决于它所在的存储位置,而不是结构体类型。说structField = new StructType(whatever);会创建一个新的StructType实例,然后改变structField,使其所有字段与新实例的字段匹配。当最初写出避免可变结构体的建议时,C#编译器允许人们编写愚蠢的代码,例如ListOfStructs[4].SomeField = 5,即使该代码实际上无法工作。禁止对任何结构体实例进行这样的更改是一种方式... - supercat
1
@supercat:我认为在每个回答中推荐不使用可变结构体没有讨论的必要。我们能不能就同意有分歧并放弃这个话题呢?一遍又一遍地重复同样的内容对任何人都没有帮助。 - Jon Skeet
...让编译器拒绝这样的代码(除了更改编译器,这可能是唯一的方法)。然而,由于C#编译器现在拒绝这样的代码,即使结构体类型允许单独操作字段和属性,在不违反结构体应该维护的任何不变量的情况下禁止单独修改字段实际上没有任何好处。 - supercat
@JonSkeet:我之前没有提到过C#编译器自建以来已经发生了变化。至于这是否会改变您对建议的看法,谁知道呢。但我认为这是一个有趣的点,我之前没有提到过。 - supercat

0
一个结构体数组将比一个包含相同字段的不可变类实例的引用数组提供更好的存储效率,因为后者除了需要前者所需的所有内存之外,还需要内存来管理类实例和保存引用所需的内存。尽管如此,你设计的结构体布局非常低效。如果你真的关心空间,并且每个项确实需要独立地存储一个字节、一个布尔值和一个类引用,那么你最好的选择可能是要么有两个字节数组(一个字节实际上比一个布尔值小),和一个类引用的数组,要么有一个字节数组,一个元素数量大约是BitVector32这样的东西的数组,以及一个类引用的数组。

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