可变结构体的好处有哪些?

6

我知道在.Net中,可变结构体/值类型被认为是“邪恶”的。那么为什么还可以创建它们呢?有哪些好的用途可以证明将这个特性添加到CLR中是合理的呢?


1
我同意,这不是重复的问题。那篇文章是关于为什么它们是邪恶的,而你正在询问它们有什么好处。 - Chris Haas
更像彼此的反问题。虽然把它们链接起来很方便。 - Marc Gravell
3个回答

3
一个非常好的问题!就个人而言,我并不是很喜欢使用结构体。但是一些在CF/XNA等平台上运行的人会坚称他们需要从结构体中获取额外的性能优势,这通常涉及到结构体的可变性。如果直接在数组、公共字段或通过ref访问结构体,则该论点具有一定价值,但在许多情况下,重复在堆栈中多次生成一个超大的结构体可能会导致堆栈空间不足。
如果您正在使用结构体,无论可变与否,并且正在使用序列化,则完全不可变的结构体可能成为真正的问题。部分或“冰棒”不可变性可以解决此问题,但同样诱人的框架也往往有更多反射限制,使私有/基于字段的序列化变得棘手。因此,可变性很方便。
上述所有内容实际上都是使用结构体来保存被归类为对象的内容。纯粹的值永远不会改变。因此完全不可变性没有问题。
最后,虽然不能证明其存在的理由,但它是面试的绝佳素材,对于不慎犯错的人来说也是很好的陷阱。也许这是由充当顾问的人员设计的。;p

我刚刚修改了我正在工作的代码,使用可变类替换了可变结构体,在单线程的单元测试中运行了几次(多线程的测试时间不够稳定,无法告诉我任何东西),发现基于结构体的最差运行结果比基于引用的最佳运行结果快了10%,而且包括整个集合(即,包括不使用这些对象的代码)。平均值相差约为12-13%,这正好是Knuth在他著名的引用论文中优化所达到的程度。这不是我会公开的类型,所以很值得,也很容易实现。 - Jon Hanna
虽然是这样,但使用这些结构的公开类实现了ISerializable,因此序列化根本不必直接涉及它们,这也有所帮助。 - Jon Hanna

2
部分可变结构体适用于创建这些结构体的构建器。
因此,如果您有类似以下内容的东西:
namespace StructEx
{
    public struct AStruct
    {
        public int AnInt { get; internal set; }
    }

    public class AStructBuilder
    {
        public AStruct BuildStruct(int anInt)
        {
            return new AStruct { AnInt = anInt };
        }
    }
}

在一个程序集中,构建出的结构体不能被该程序集之外的任何内容修改,但构建它们很容易。

1
你可以认为在公共API上,这是一个不可变的结构体,并且AStruct也可以用构造函数来设置readonly字段(这不会改变API)- 但我明白你的意思。 - Marc Gravell
我本来想问为什么不直接在结构体构造函数中添加参数,但是我想起来你不能删除结构体的默认构造函数了。因此,这是一个合理的用法,给你点赞。 - Davy8
1
实际上,我刚才说的没有意义,因为仍然有可能创建一个空的 AStruct - Davy8
是的,这个玩具示例并不是最好的。我想到了StringBuilder,但是无法想出一个有趣的东西来构建。我同意,对于我的结构体,我使用readonly并从构造函数初始化。我没有使用上述模式。 - Matt Ellen

2
可变结构体是.net中唯一能实现可变值语义的数据类型。一些人(比如Eric Lippert)讨厌它们,因为它们会让编译器的生命更加复杂,但是它们提供的语义通常比引用类型更清晰明了。
例如,假设有一个类和一个结构体类型以及以下接口:
struct myStruct {public int v; ... other stuff...};
class myClass {public int v; ... other stuff...};
interface ISample {
  void useStructByValue(myStruct z);
  void useStructByReference(ref myStruct z);
  void useClassByValue(myClass z);
  void useClassByReference(ref myClass z);
}
并考虑以下方法:
void test(myStruct struct1, myStruct struct2, myClass class1, myClass class2,
  ISample munger)
{
  for(int i=0; i < 5; i++)
  {
    munger.useStructByValue(struct1);         // S1
    munger.useStructByReference(ref struct2); // S2
    munger.useClassByvalue(class1);           // S3
    munger.useClassByReference(ref class2);   // S4
  }
}
假设只要munger不使用任何不安全代码,哪些传入的项可能受到这四个语句的影响?我认为即使没有看到struct1的整个定义或者munger的任何部分实现,也可以确定struct1.v不会被任何一个语句改变,保证。struct2.v可能会被S2改变,但不会被其他语句改变,同样保证。然而,class1.v和class2.v的值可能会被任何一个语句改变;唯一知道哪个语句可以改变class1.v或class2.v的方法是检查每个现在或将来实现ISample接口的类型的代码。
换句话说,结构体提供了有限但明确定义的语义。类则没有。
顺便说一下,由于属性的工作方式的限制,不能直接修改也不能通过引用传递结构体属性的字段。即使如此,像这样的代码仍然可以使用:
List<myStruct> myList;
...
myStruct tempStruct = myList[1];
tempStrict.v = 5;
myList[1] = tempStruct;
虽然不是线程安全的,但语义清晰明了。将myStruct替换为myClass,则确定性变得模糊。如果列表中的每个项目都有一个未共享的myClass实例,那么可以简单地使用myList [1] .v = 5;。然而,确定列表项是否具有未共享的myClass实例几乎是不可能的。如果有人试图将myList [1]的值复制到myList [0]中并且写入myList [1] .v会影响到myList [0] .v,这样的语句虽然可用但会导致问题。结构体则不会存在这样的问题。

在你的最后一段第一行中,你把 myStruct 和 myClass 弄反了。 - Jouke van der Maas
@JoukevanderMaas:我在最后一段的观点是,虽然更改作为属性公开的结构体字段所需的代码比理想情况下更繁琐,但其语义是不言自明的,而类则不同;最后一段谈到了类所具有的问题,而结构体则没有。一个具有两个整数字段的结构体包含两个整数。而一个具有两个整数字段的类包含两个整数,加上另一个关键但不可检索的东西:对它的所有引用的集合。类类型的语义可能会因存在... - supercat
除非一个对象实例化另一个对象本身并且从未在任何地方公开引用,否则就没有一般方法可以知道是否存在对它的引用以及在哪里存在这样的引用。如果需要存储对象之间的关系,则这种相互连接可能很有用。然而,如果只是想存储数据,则这种相互连接只会增加负担和复杂性。 - supercat

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