【问题标题】:Efficient cloning of cached objects缓存对象的高效克隆
【发布时间】:2010-10-22 10:49:26
【问题描述】:

我们有一个应用程序可以对数据对象进行比较,以确定对象的一个​​版本是否与另一个版本不同。我们的应用程序还对这些对象进行了一些广泛的缓存,在进行这些比较时我们遇到了一些性能问题。

这是工作流程:

  1. 数据项 1 是内存中的当前项。该项目最初是从缓存中检索并深度克隆的(所有子对象,例如字典等)。然后编辑数据项 1,并修改其属性。
  2. 然后我们将此对象与存储在缓存中的原始版本进行比较。由于数据项 1 已被克隆且其属性已更改,因此这些对象应该不同。

这里有几个问题。

主要问题是我们的深度克隆方法非常昂贵。我们针对浅克隆对其进行了分析,它的速度慢了 10 倍。那是废话。这是我们深度克隆的方法:

    public object Clone()    
    {
        using (var memStream = new MemoryStream())
        {
            var binaryFormatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.Clone));
            binaryFormatter.Serialize(memStream, this); 
            memStream.Seek(0, SeekOrigin.Begin);
            return binaryFormatter.Deserialize(memStream);
        }
    }

我们最初使用以下内容进行克隆:

public object Clone()
{
    return this.MemberwiseClone();
}

这样的性能更高,但是因为它进行了浅层克隆,所以作为该对象属性的所有复杂对象(例如字典等)都没有被克隆。该对象仍将包含与缓存中的对象相同的引用,因此在比较时属性将相同。

那么,有没有人有一种有效的方法对 C# 对象进行深度克隆,包括克隆整个对象图?

【问题讨论】:

  • 假设您想要通用的 Clone() 方法,因为您不想在所有东西上实现 ICloneable?
  • 这只是克隆一个特定的对象。这个对象是我们应用程序中的核心数据对象。这能回答你的问题吗?

标签: c# performance clone deep-copy


【解决方案1】:

您可以通过两种方式进行深度克隆:通过实现 ICloneable(并调用 Object.MemberwiseClone 方法),或通过二进制序列化。

第一道

第一种(可能更快,但并不总是最好的)方法是在每种类型中实现 ICloneable 接口。下面的示例说明了这一点。 C类实现了ICloneable,由于这个类引用了其他的D类和E类,那么后者也实现了这个接口。在 C 的 Clone 方法中,我们调用了其他类型的 Clone 方法。

Public Class C
Implements ICloneable

    Dim a As Integer
    ' Reference-type fields:
    Dim d As D
    Dim e As E

    Private Function Clone() As Object Implements System.ICloneable.Clone
        ' Shallow copy:
        Dim copy As C = CType(Me.MemberwiseClone, C)
        ' Deep copy: Copy the reference types of this object:
        If copy.d IsNot Nothing Then copy.d = CType(d.Clone, D)
        If copy.e IsNot Nothing Then copy.e = CType(e.Clone, E)
        Return copy
    End Function
End Class

Public Class D
Implements ICloneable

    Public Function Clone() As Object Implements System.ICloneable.Clone
        Return Me.MemberwiseClone()
    End Function
End Class

Public Class E
Implements ICloneable

    Public Function Clone() As Object Implements System.ICloneable.Clone
        Return Me.MemberwiseClone()
    End Function
End Class

现在,当您为 C 的实例调用 Clone 方法时,您会得到该实例的深度克隆:

Dim c1 As New C
Dim c2 As C = CType(c1.Clone, C)   ' Deep cloning.  c1 and c2 point to two different 
                                   ' locations in memory, while their values are the 
                                   ' same at the moment.  Changing a value of one of
                                   ' these objects will NOT affect the other.

注意:如果 D 类和 E 类具有引用类型,则必须像我们对 C 类所做的那样实现它们的 Clone 方法。以此类推。

警告: 1-只要没有循环引用,上面的示例就有效。例如,如果 C 类有一个自引用(例如,C 类型的字段),实现 ICloneable 接口并不容易,因为 C 中的 Clone 方法可能会进入无限循环。

2-另外需要注意的是,MemberwiseClone 方法是 Object 类的受保护方法。这意味着您只能在类的代码中使用此方法,如上所示。这意味着你不能将它用于外部类。

因此,实现 ICloneable 只有在上述两个警告不存在时才有效。否则,您应该使用二进制序列化技术。

第二种方式

二进制序列化可用于深度克隆而不会出现上述问题(尤其是循环引用)。这是一个使用二进制序列化执行深度克隆的通用方法:

Public Class Cloning
    Public Shared Function DeepClone(Of T)(ByVal obj As T) As T
        Using MStrm As New MemoryStream(100)    ' Create a memory stream.
            ' Create a binary formatter:
            Dim BF As New BinaryFormatter(Nothing, New StreamingContext(StreamingContextStates.Clone))

            BF.Serialize(MStrm, obj)    ' Serialize the object into MStrm.
            ' Seek the beginning of the stream, and then deserialize MStrm:
            MStrm.Seek(0, SeekOrigin.Begin)
            Return CType(BF.Deserialize(MStrm), T)
        End Using
    End Function
End Class

这个方法的使用方法如下:

Dim c1 As New C
Dim c2 As C = Cloning.DeepClone(Of C)(c1)   ' Deep cloning of c1 into c2.  No need to 
                                            ' worry about circular references!

【讨论】:

    【解决方案2】:

    我的回答可能不适用于您的情况,因为我不知道您的限制和要求是什么,但我认为通用克隆可能存在问题。正如您已经遇到的那样,性能可能是一个问题。某些东西需要识别对象图中的唯一实例,然后创建一个精确的副本。这就是二进制序列化程序为您所做的事情,但它还可以做更多事情(序列化本身)。看到它比您预期的要慢,我并不感到惊讶。我有类似的经历(顺便也与缓存有关)。我的方法是自己实现克隆;即为实际需要克隆的类实现 IClonnable。您正在缓存的应用程序中有多少类?如果有太多(手动编码克隆),考虑一些代码生成是否有意义?

    【讨论】:

      【解决方案3】:

      如果不对所有需要克隆的数据对象显式实现 ICloneable,您将无法获得比通用二进制序列化更好的结果。另一种可能的途径是反射,但如果您正在寻找性能,您也不会对此感到满意。

      我会考虑使用 ICloneable 进行深度复制和/或 IComparable 来比较对象是否不同......如果性能对你来说是个大问题。

      【讨论】:

        【解决方案4】:

        也许你不应该深度克隆呢?

        其他选项:

        1) 让你的“缓存”对象记住它的原始状态,并让在每次发生任何变化时更新“已更改”标志。

        2) 不记得原始状态,只要有任何变化就将对象标记为脏。然后从原始源重新加载对象进行比较。我敢打赌,您的对象更改的频率低于不更改的频率,甚至更不频繁地更改回相同的值。

        【讨论】:

        • 脏标志?这是一大堆工作,你会失去自动属性,代码中到处都是 SetIsDirty() 并且很容易忘记设置标志(容易产生错误)。他将通过使用排序等实现 IComparable 获得更多好处......
        • 除非您使用 PostSharp,否则您无需更改任何内容,并且您可以获得 PropertyChanged 的​​所有好处,并且您可以使用它来做更多事情。
        猜你喜欢
        • 2012-04-18
        • 2011-11-07
        • 2015-02-08
        • 1970-01-01
        • 1970-01-01
        • 2013-01-03
        • 2017-02-11
        • 2011-07-10
        • 1970-01-01
        相关资源
        最近更新 更多