4

我意识到结构是不可变的,变异结构是邪恶的,改变结构中的值的正确方法是创建新实例。但是,我不清楚新实例与允许结构可变的内存和性能方面/问题。

假设我有结构,

struct Vehicle
{
    public readonly int Number;
    public readonly double Speed, Direction;

    public Vehicle(int number, double speed, double direction)
    {
        this.Number = number;
        this.Speed = speed;
        this.Direction = direction;
    }
}

并将其创建为:

Vehicle car;

Vehicle plane = new Vehicle(1234, 145.70, 73.20)

如果我需要稍后分配给 Number、Speed 或 Direction,我可以删除 readonly 并使结构可变 - 我知道这样做很糟糕。- 从而“变异”一个已经创建的结构值。

或者,我可以创建一个新的结构实例。所以不要说 car.Speed = 120.7; 我可以说 car = new Vehicle(car.Number, 178.55, car.Direction);。这将创建一个新的结构值,几乎就像旧值一样,只是速度发生了变化。但它不会改变现有的结构值。

这是问题所在。假设,作为一个极端的例子,我需要每秒更新数万次速度和/或方向。我认为创建这么多实例会严重影响内存和性能,在这种情况下,最好允许结构是可变的。

谁能澄清可变结构的内存和性能问题与在这种极端情况下实现结构的正确方法?

4

3 回答 3

3

结构有两种不同的用例;在某些情况下,人们想要一种封装单个值并且行为类似于类的类型,但具有更好的性能和非空默认值。在这种情况下,应该使用我所说的不透明结构。MSDN 的结构指南是在假设这是唯一的用例的基础上编写的。

然而,在其他情况下,结构的目的只是将一些变量与胶带绑定在一起。在这些情况下,应该使用一个透明的结构,它只是将这些变量公开为公共字段。这种类型并没有什么坏处。邪恶的是一切都应该表现得像一个类对象,或者一切都应该被“封装”的概念。如果结构的语义是这样的:

  1. 有一些固定的可读成员集(字段或属性)暴露了它的整个状态
  2. 给定这些成员的任何一组所需值,可以使用这些值创建一个实例(禁止值的组合)。
  3. 结构的默认值应该是将所有这些成员设置为其各自类型的默认值。

并且对它们的任何更改都会破坏使用它的代码,那么该结构的未来版本可能会做任何透明结构无法做到的事情,透明结构也不会允许未来的任何事情结构的版本将能够防止。因此,封装会增加成本而不增加价值。

我建议,只要可行,就应该努力使所有结构透明或不透明。此外,我建议由于 .net 处理结构方法的方式存在缺陷,因此应该避免修改不透明结构的公共成员this,除非可能在属性设置器中。具有讽刺意味的是,虽然 MSDN 的指导方针建议不应将结构用于不代表“单个值”的事物,但在常见情况下,人们只是想将一组变量从一段代码传递到另一段代码,透明结构比不透明结构或类类型要优越得多,并且优势幅度随着字段数量的增加而增长。

顺便说一句,关于最初的问题,我建议表示您的程序可能想要处理两种事情是有用的:(1)汽车,以及(2)与特定汽车相关的信息。我建议拥有一个 struct并拥有一个持有 type 字段的CarState实例可能会有所帮助。这将允许 的实例将其状态暴露给外部代码,并可能允许外部代码在受控情况下修改其状态(使用诸如CarCarStateCar

delegate void ActionByRef<T1,T2>(ref T1 p1, ref T2 p2);
delegate void ActionByRef<T1,T2,T3>(ref T1 p1, ref T2 p2, T3 p3);

void UpdateState<TP1>(ActionByRef<CarState, TP1> proc, ref TP1 p1)
{ proc(ref myState, ref p1); }
void UpdateState<TP1,TP2>(ActionByRef<CarState, TP1,TP2> proc, ref TP1 p1, ref TP2 p2)
{ proc(ref myState, ref p1, ref p2); }

请注意,此类方法提供了将汽车状态设为可变类的大部分性能优势,但没有与混杂对象引用相关的危险。允许外部代码可以Car通过上述方法更新汽车的状态,而不允许外部代码在任何其他时间修改其状态。

顺便说一句,我真的希望.net 有一种方法可以指定“安全”结构或类应该被视为封装其一个或多个组成部分的成员[例如,这样可以将包含被Rectangle调用RString被调用的结构Name视为有一个 fields X, Y, Width, 以及Height对应的结构字段的别名。如果这是可能的,它将极大地促进类型需要保持比以前预期的更多状态的情况。我不认为目前的 CIL 允许在安全类型中使用这种别名,但没有概念上的原因它不能。

于 2013-01-12T23:41:01.777 回答
3

您需要自己回答的问题是“我正在编写的类型的预期语义是什么?”

当您尝试编写值类型struct时,应使用A而不是 a 。值类型的一个很好的例子是. 不管发生什么其他事情,2013 年 1 月 12 日下午 3:33 GMT-7 始终是 2013 年 1 月 12 日下午 3:33 GMT-7。颜色是另一个很好的例子。由 RGB 255, 0, 0 组成的两种颜色永远不会有任何不同。classDateTime

您尝试创建的类型Vehicle, 没有值语义,因为您希望它包含旨在为特定汽车实例更新的状态。因此,您应该编写一个 mutableclass而不是struct.

于 2013-01-12T22:37:40.217 回答
1

I'll only answer the performance aspect of this question. I'll spare you the premature optimization warning and the discussion about semantics.

Yes, assigning the whole struct has a performance cost. It introduces assignments which were not present in the mutating variant. I do not trust the JIT to optimize them out as the .NET JIT is pretty simple (it is optimized for fast code-gen).

Probably, these assignments are very cheap because they are contiguous in memory and the memory cache line has been loaded already. No memory transaction needs to occur because of them.

The performance difference will be small (for reasonably sized structs). You will be able to measure it, though.

It also introduces clutter into your code to create a new instance of the struct for each mutation. I have encountered cases in practice where I found it to be best to use mutable structs from a code quality perspective. Those are just a few cases but they exist.

于 2013-01-12T22:47:35.873 回答