C#中的堆和栈(三)

原文链接:https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-iii/

导言

虽然在.NET Framework下编程,我们不需要主动的关注内存管理和垃圾回收(GC),但是为了更优的程序性能,我们还是应该关注内存管理和GC。当然从根本上理解了内存管理是如何工作的也会帮助我们了解程序中的每一个变量的工作方式。在本节中,我们将讨论由于在堆上的引用变量所引发的问题,并且使用ICloneable来修复它。

拷贝不是拷贝

为了把问题说的更清晰,我们来分析一下,有一个值类型被分配到堆上,与之相对的还有一个引用类型也在堆上,这会发生什么呢。首先来看值类型。看一下下面栗子中的类和结构。我们拥有一个Dude类,包含一个Name字段,和两个Shoe类型的字段。我们拥有一个CopyDude()方法,使得生成新的Dude对象更简单。

public struct Shoe{
    public string Color;
}
public class Dude
{
    public string Name;
    public Shoe RightShoe;
    public Shoe LeftShoe;
    public Dude CopyDude()
    {
        Dude newPerson = new Dude();
        newPerson.Name = Name;
        newPerson.LeftShoe = LeftShoe;
        newPerson.RightShoe = RightShoe;
        return newPerson;
    }
    public override string ToString()
    {
        return (Name + " : Dude!, I have a " + RightShoe.Color + " shoe on my right foot, and a " + LeftShoe.Color + " on my left foot.");
    }
}

我们的Dude是一个应用类型的,并且因为Shoe是Dude类的成员变量,所以他们都在堆上分配内存。

C#中的堆和栈(三)_第1张图片

当我们在执行下面的方法时:

public static void Main()
{
    Dude Bill = new Dude();
    Bill.Name = "Bill";
    Bill.LeftShoe = new Shoe();
    Bill.RightShoe = new Shoe();
    Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
    Dude Ted = Bill.CopyDude();
    Ted.Name = "Ted";
    Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
    Console.WriteLine(Bill.ToString());
    Console.WriteLine(Ted.ToString());
}

我们得到了预期的结果:
Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.

如果我们将Shoe改成引用类型的会发生什么呢?这就是问题的根本所在了,让我们把Shoe编程引用类型:

public class Shoe{
    public string Color;
}

Main()中的代码完全一致,输出结果变了:

Bill : Dude!, I have a Red shoe on my right foot, and a Red on my left foot
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot

Red shoe也穿到了Bill的脚上。现在错误就很清除了。你知道这是怎么发生的吗?下图是堆上的内存分配情况:

C#中的堆和栈(三)_第2张图片

因为我们现在把Shoe的类型从值类型替换为引用类型,当引用类型的内容被复制的时候,只有指针的内容被复制了,而不是那个被指向的对象,我们必须再做点什么以使得我们引用类型的Shoe看起来更像是值类型的。

幸运的是有一个接口可以帮助我们:ICloneable。这个接口主要规定了应用类型的变量是如何被复制的,所有的Dude都遵守这个规则的时候,就避免了鞋子被分享(shoe sharing)的错误。所有需要被拷贝的对象都继承接口ICloneable,包括Shoe类。

ICloneable只包含一个方法:Clone()

//--译者注:这里和原文的代码不一样
public interface ICloneable
{
    object Clone();
}

接下来看怎样修改Shoe类:

public class Shoe : ICloneable
{
    public string Color;
    #region ICloneable Members
    public object Clone()
    {
        Shoe newShoe = new Shoe();
        newShoe.Color = Color.Clone() as string;
        return newShoe;
    }
    #endregion
}

在方法Clone()里面,我们只是新生成了一个Shoe对象,复制了所有的引用类型和值类型的变量,并返回新的对象。可能你会注意到string类早就实现了ICloneable接口,所以我们可以直接调用Color.Clone()。因为Clone()方法返回的是一个object类型的引用,所以在我们将它的值赋值给shoe时,必须对类型进行转换。

然后,在方法CopyDude()中我们通过拷贝来克隆shoe的实例。

public Dude CopyDude()
{
    Dude newPerson = new Dude();
    newPerson.Name = Name;
    newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
    newPerson.RightShoe = RightShoe.Clone() as Shoe;
    return newPerson;
}

现在,再来运行Mian()方法:

public static void Main()
{
    Dude Bill = new Dude();
    Bill.Name = "Bill";
    Bill.LeftShoe = new Shoe();
    Bill.RightShoe = new Shoe();
    Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
    Dude Ted = Bill.CopyDude();
    Ted.Name = "Ted";
    Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
    Console.WriteLine(Bill.ToString());
    Console.WriteLine(Ted.ToString());
}

我们得到的结果如下:

Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot

正如我们所希望的那样。

C#中的堆和栈(三)_第3张图片

最后梳理一下

在一般情况下:我们既想要克隆引用类型,也想拷贝值类型,这将有效的降低你在调试过程中遇到的错误。

所以为了减少不必要的麻烦,我们进一步让Dude类实现ICloneable接口,并且用Clone()方法替换CopyDude()。

public class Dude: ICloneable
{
    public string Name;
    public Shoe RightShoe;
    public Shoe LeftShoe;
    public override string ToString()
    {
        return (Name + " : Dude!, I have a " + RightShoe.Color + " shoe on my right foot, and a " + LeftShoe.Color + " on my left foot.");
    }
    #region ICloneable Members
    public object Clone()
    {
        Dude newPerson = new Dude();
        newPerson.Name = Name.Clone() as string;
        newPerson.LeftShoe = LeftShoe.Clone() as Shoe;
        newPerson.RightShoe = RightShoe.Clone() as Shoe;
        return newPerson;
    }
    #endregion
}

并且要修改Main()方法以使用Dude.Clone()方法。

public static void Main()
{
    Dude Bill = new Dude();
    Bill.Name = "Bill";
    Bill.LeftShoe = new Shoe();
    Bill.RightShoe = new Shoe();
    Bill.LeftShoe.Color = Bill.RightShoe.Color = "Blue";
    Dude Ted = Bill.Clone() as Dude;
    Ted.Name = "Ted";
    Ted.LeftShoe.Color = Ted.RightShoe.Color = "Red";
    Console.WriteLine(Bill.ToString());
    Console.WriteLine(Ted.ToString());
}

最终的输出是这样的:

Bill : Dude!, I have a Blue shoe on my right foot, and a Blue on my left foot.
Ted : Dude!, I have a Red shoe on my right foot, and a Red on my left foot.

万事大吉。

值得注意的是,赋值运算(“=”)对System.String类实际上是复制了它的string,所以你不必担心它会复制一个引用。然鹅,你也必须要注意到,这使得我们的内存占用变多了。如果你回过头去看那些图示,因为string是引用类型,它确实应该是一个指向堆内存中的另一个对象的指针,但是在简易的栈内存里,它被当成了值类型。

总结

在一般情况下,如果打算通过拷贝创建对象,我们就需要实现和使用ICloneable接口。这使得我们的引用类型在一定程度上模仿了值类型。正如你所看到的,这对于我们追踪需要处理的变量的类型是非常重要的,因为值类型和引用类型在分配内存的时候是不一样的。

在下一篇中,我们将寻找一条优化内存占用的方式。

然后,祝你开心。

你可能感兴趣的:(C#)