C#结构:可变性与性能

时间:2013-01-12 22:28:58

标签: c# performance struct

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

假设我有结构,

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并使结构变得可变 - 我知道这样做是最好的。 - 从而“改变”已经创建的结构值。

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

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

任何人都可以澄清一个可变结构的内存和性能问题,而不是在这种极端情况下实现结构的正确方法吗?

3 个答案:

答案 0 :(得分:3)

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

当您尝试撰写value type时,应使用struct代替class。值类型的一个很好的例子是DateTime。无论发生什么,2013年1月12日下午3:33 GMT-7始终是2013年1月12日格林威治标准时间7点3点33分。颜色是另一个好例子;由RGB 255,0,0组成的两种颜色永远不会有任何不同。

您尝试创建的类型Vehicle没有value semantics,因为您希望它包含要针对特定​​汽车实例更新的状态。因此,您应该编写一个可变class而不是struct

答案 1 :(得分:2)

结构有两种不同的用例;在某些情况下,需要一个封装单个值的类型,其行为大多类似于一个类,但具有更好的性能和非null的默认值。在这种情况下,人们应该使用我称之为不透明的结构; MSDN的结构指南是在假设这是唯一的用例的情况下编写的。

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

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

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

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

顺便说一下,关于原始问题,我建议表示你的程序可能想要处理两种事情是有用的:(1)汽车,(2)与特定汽车相关的信息。我建议有一个结构CarState,并让Car的实例包含CarState类型的字段可能会有所帮助。这将允许Car的实例将其状态暴露给外部代码,并可能允许外部代码在受控环境下修改其状态(使用

等方法)
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的{​​{1}}和名为R的{​​{1}}的结构可被视为包含字段StringName,{ {1}}和X,它们是相应结构字段的别名。如果可能的话,它将极大地促进类型需要保持比先前预期更多状态的情况。我不认为现在的CIL允许在安全类型中进行这样的别名,但是没有概念上的理由它不能。

答案 2 :(得分:1)

我只会回答这个问题的表现方面。我将为您提供过早的优化警告和有关语义的讨论。

是的,分配整个结构具有性能成本。它引入了变异变体中不存在的任务。我不相信JIT可以优化它们,因为.NET JIT非常简单(它针对快速代码生成进行了优化)。

可能这些分配非常便宜,因为它们在内存中是连续的,并且已经加载了内存缓存行。不需要因为它们而发生内存事务。

性能差异很小(对于合理大小的结构)。不过,你可以测量它。

它还会在您的代码中引入混乱,为每个突变创建一个新的struct实例。我在实践中遇到过一些案例,我发现最好从代码质量角度使用可变结构。这只是少数情况,但它们存在。