如何处理返回结构的不变性?

时间:2010-09-01 13:43:22

标签: c# .net struct language-features immutability

我正在编写一个拥有庞大的“细胞”2D阵列的游戏。一个单元只需3个字节。我还有一个名为CellMap的类,它包含2D数组作为私有字段,并通过公共索引器提供对它的访问。

分析表明性能问题是由太多Cell对象的垃圾收集引起的。所以我决定让Cell成为一个结构(它是一个类)。

但现在这样的代码不起作用了:

cellMap[x, y].Population++;

我可以想到很多选择,但我真的不喜欢它们中的任何一种。

  1. 将数组公开,然后编写cellMap.Data[x, y].Population = 5;
  2. 停止使用CellMap类,直接使用2D数组。但是CellMap非常方便,因为它实现了自己的优化序列化,它公开了WidthHeight属性,比写cellMap.GetLength(0)
  3. 更方便
  4. 让细胞不可变。但那么代码看起来如何呢? cellMap[x, y] = IncrementCellPopulation(cellMap[x, y])?非常详细。
  5. 一些实用程序功能,例如cellMap.SetPopulationAt(x, y, 5)
  6. 在每个拥有CellMap的类中,添加实用程序属性,例如private Cell[,] CellData { get { return this.CellMap.GetInternalArray(); } },这样我的代码就像CellData[x, y].Population++
  7. 传统上如何解决这个问题?

6 个答案:

答案 0 :(得分:18)

所以这里实际上有两个问题。你实际上问的问题是:什么是处理结构应该是不可变的这一事实的技术,因为它们是按值复制的,但你想要改变一个。然后有一个问题是激励这个问题,“我怎样才能使我的程序的性能可以接受?”

我的另一个答案解决了第一个问题,但第二个问题也很有趣。

首先,如果探查器实际上已经确定性能问题是由于单元格的垃圾收集,则可能使单元格成为结构将有所帮助。也有可能它根本没有帮助,这样做可能会使情况变得更糟。

您的单元格不包含任何引用类型;我们知道这是因为你说他们只有三个字节。如果其他人在阅读本文时认为他们可以通过将类转换为结构来进行性能优化,那么它可能根本没有帮助,因为该类可能包含引用类型的字段,在这种情况下垃圾收集器仍然必须收集每个实例,即使它被转换为值类型。其中的引用类型也需要收集!如果Cell仅包含值类型,我建议仅出于性能原因尝试此操作,显然它是。

它可能会使情况变得更糟,因为价值类型不是灵丹妙药;他们也有成本。复制值的类型通常比引用类型更昂贵(它几乎总是寄存器的大小,几乎总是在适当的存储器边界上对齐,因此芯片高度优化以便复制它们)。并且值类型一直被复制。

现在,在你的情况下,你有一个小于的结构而不是引用;引用通常是四个或八个字节。你把它们放在一个数组中,这意味着你正在将数组打包下来;如果你有一千个,它将需要三千字节。这意味着其中每四个结构中有三个未对齐,这意味着有更多时间(在许多芯片架构上)从数组中获取值。您可以考虑将 padding 结构的影响测量到四个字节以查看是否会产生影响,前提是您仍然要将它们保存在一个数组中,这将我带到下一个点。 ..

单元格抽象可能只是一个糟糕的抽象,用于存储大量单元格的数据。如果问题是单元格是类,那么你要保留数千个单元格的数组,并且收集它们是昂贵的,那么除了将Cell变成结构之外,还有其他解决方案。例如,假设一个Cell包含两个字节的Population和一个字节的Color。这是Cell的机制,但肯定不是你要向用户公开的接口您的机制没有理由必须使用与界面相同的类型。因此,您可以按需生成Cell类的实例

interface ICell
{
   public int Population { get; set; }
   public Color Color { get; set; }
}
private class CellMap
{
    private ushort[,] populationData; // Profile the memory burden vs speed cost of ushort vs int
    private byte[,] colorData; // Same here. 
    public ICell this[int x, int y] 
    {
        get { return new Cell(this, x, y); }
    }

    private sealed class Cell : ICell
    {
        private CellMap map;
        private int x;
        private int y;
        public Cell(CellMap map, int x, int y)
        {
            this.map = map; // etc
        }
        public int Population  
        {
            get { return this.map.populationData[this.x, this.y]; } 
            set { this.map.populationData[this.x, this.y] = (ushort) value; } 
        }

等等。 按需制造电池。如果它们是短暂的,它们几乎会立即被收集。 CellMap是一个抽象,因此使用抽象来隐藏凌乱的实现细节。

使用这种架构,您没有任何垃圾收集问题,因为您几乎没有活动的Cell实例,但您仍然可以说

map[x,y].Population++;

没问题,因为第一个索引器制造了一个不可变对象,知道如何更新地图的状态 Cell 不需要是可变的;注意Cell类是完全不可变的。 (哎呀,Cell在这里可能是一个结构,但当然把它投射到ICell只会把它装箱。)这是 map 是可变的,并且该单元改变了地图用户。

答案 1 :(得分:9)

如果你想让Cell不可变 - 就像它应该是一个结构一样 - 那么一个好的技术是在Cell上建立一个实例方法的工厂:

struct C
{
    public int Foo { get; private set; }
    public int Bar { get; private set; }
    private C (int foo, int bar) : this()
    {
        this.Foo = foo;
        this.Bar = bar;
    }
    public static C Empty = default(C);
    public C WithFoo(int foo)
    {
        return new C(foo, this.Bar);
    }
    public C WithBar(int bar)
    {
        return new C(this.Foo, bar);
    }
    public C IncrementFoo()
    {
        return new C(this.Foo + 1, bar);
    }
    // etc
}
...
C c = C.Empty;
c = c.WithFoo(10);
c = c.WithBar(20);
c = c.IncrementFoo();
// c is now 11, 20

所以你的代码就像

map[x,y] = map[x,y].IncrementPopulation();

然而,我认为这可能是一条死胡同;最好不要在一开始就没有那么多Cell,而不是试图优化一个有数千个Cell的世界。我会在那上面写another answer

答案 2 :(得分:4)

6。在变量值的方法中使用ref参数,将其称为IncrementCellPopulation(ref cellMap[x, y])

答案 3 :(得分:2)

如果您的单元格图实际上是“稀疏的”,也就是说,如果有很多相邻单元格没有值或某些默认值,我建议您不要为这些单元格创建单元格对象。仅为实际具有某种非默认状态的单元格创建对象。 (这可能会使细胞总数减少很多,从而减轻垃圾收集器的压力。)

这种方法当然要求您找到一种存储细胞图的新方法。您必须远离将单元格存储在数组中(因为它们不是稀疏的),并且包含不同类型的数据结构,可能是树。

例如,您可以将地图细分为多个统一区域,以便将任何单元格坐标转换为相应的区域。 (您可以根据相同的想法进一步将每个区域细分为子区域。)然后,您可以在每个区域都有一个搜索树,其中单元格坐标作为树的关键。

这样的方案允许您只存储所需的单元格,同时仍然可以快速访问地图中的任何单元格。如果在树中找不到某些指定坐标的单元格,则可以假定它是默认单元格。

答案 4 :(得分:1)

封装您希望CellMap执行的操作,并仅允许通过IncrementPopupation(int x, int y)等适当方法访问实际数组。在大多数情况下,使数组(或任何变量,例如)public是一种严重的代码味道,就像在.NET中返回一个数组一样。

出于性能原因,请考虑使用单维数组;那些在.NET中更快。

答案 5 :(得分:0)

Eric Lippert的方法很好,但我建议使用基类而不是间接访问器的接口。下面的程序演示了一个类似于稀疏点数组的类。如果一个人永远不会持有任何PointRef(*)类型的东西,那么事情应该很漂亮。话说:

  MyPointHolder(123) = somePoint

  MyPointHolder(123).thePoint = somePoint

将创建一个临时的pointRef对象(在一种情况下为pointRef.onePoint;在另一种情况下为pointHolder.IndexedPointRef)但扩展的类型转换用于维护值语义。当然,如果(1)值类型的方法可以被标记为mutators,并且(2)编写通过property访问的结构的字段可以自动读取属性,编辑临时结构,并写入,事情会容易得多。回来了。这里使用的方法有效,尽管我不知道如何使它成为通用的。

(*)PointRef类型的项只应该由属性返回,并且永远不应该存储在变量中,或者作为参数用于除转换为Point的setter属性以外的任何参数。

MustInherit Class PointRef
    Public MustOverride Property thePoint() As Point
    Public Property X() As Integer
        Get
            Return thePoint.X
        End Get
        Set(ByVal value As Integer)
            Dim mypoint As Point = thePoint
            mypoint.X = value
            thePoint = mypoint
        End Set
    End Property
    Public Property Y() As Integer
        Get
            Return thePoint.X
        End Get
        Set(ByVal value As Integer)
            Dim mypoint As Point = thePoint
            mypoint.Y = value
            thePoint = mypoint
        End Set
    End Property
    Public Shared Widening Operator CType(ByVal val As Point) As PointRef
        Return New onePoint(val)
    End Operator
    Public Shared Widening Operator CType(ByVal val As PointRef) As Point
        Return val.thePoint
    End Operator
    Private Class onePoint
        Inherits PointRef

        Dim myPoint As Point

        Sub New(ByVal pt As Point)
            myPoint = pt
        End Sub

        Public Overrides Property thePoint() As System.Drawing.Point
            Get
                Return myPoint
            End Get
            Set(ByVal value As System.Drawing.Point)
                myPoint = value
            End Set
        End Property
    End Class
End Class


Class pointHolder
    Dim myPoints As New Dictionary(Of Integer, Point)
    Private Class IndexedPointRef
        Inherits PointRef

        Dim ref As pointHolder
        Dim index As Integer
        Sub New(ByVal ref As pointHolder, ByVal index As Integer)
            Me.ref = ref
            Me.index = index
        End Sub
        Public Overrides Property thePoint() As System.Drawing.Point
            Get
                Dim mypoint As New Point(0, 0)
                ref.myPoints.TryGetValue(index, mypoint)
                Return mypoint
            End Get
            Set(ByVal value As System.Drawing.Point)
                ref.myPoints(index) = value
            End Set
        End Property
    End Class

    Default Public Property item(ByVal index As Integer) As PointRef
        Get
            Return New IndexedPointRef(Me, index)
        End Get
        Set(ByVal value As PointRef)
            myPoints(index) = value.thePoint
        End Set
    End Property

    Shared Sub test()
        Dim theH1, theH2 As New pointHolder
        theH1(5).X = 9
        theH1(9).Y = 20
        theH2(12).X = theH1(9).Y
        theH1(20) = theH2(12)
        theH2(12).Y = 6
        Dim h5, h9, h12, h20 As Point
        h5 = theH1(5)
        h9 = theH1(9)
        h12 = theH2(12)
        h20 = theH1(20)
    End Sub
End Class