• 译文---C#堆VS栈(Part Three)


    前言

             在本系列的第一篇文章《C#堆栈对比(Part Two)》中,介绍了值类型和引用类型在参数传递时的不同,本文将讨论如何应用ICloneable接口实现去修复引在堆上的用变量所带来的问题。

             本文是系列文章的第三部分。

             注:限于本人英文理解能力,以及技术经验,文中如有错误之处,还请各位不吝指出。

    目录

    C#堆栈对比(Part One)

    C#堆栈对比(Part Two)

    C#堆栈对比(Part Three

    C#堆栈对比(Part Four)

    拷贝不是复制那么简单

      为了更清楚的表达这个问题,我们来考察一下堆上的值类型与堆上的引用类型。首先,我们来看看值类型。跟随如下的类和结构体,我们有一个包含Name和两个Shoe字段的Dude类。我们有一个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结构体是类的一个成员,他们都在堆上。

      注:这里体现了值类型是在栈上还是在堆上,完全取决于其声明时的地点。

      当我们运行如下的方法时:

    public static void Main()
    {
                   Class1 pgm = new Class1();
     
                      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改为引用类型呢?那将就是个问题,更改如下:

    public class Shoe
    {
             public string Color;
    }

      更改之后再次运行代码,得到的结果如下:

      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

      红色的鞋子在另一个人身上,这明显是错的,你能看出这是怎么发生的吗?下图就是内存引用示例:

      因为现在我们用Shoe的引用类型代替值类型,并且拷贝引用类型内容时仅仅是拷贝了指针(不是指针真正指向的对象),我们必须做一些额外工作使我们的引用类型的Shoe更符合值类型的行为。

      注:上面这个例子中当Shoe为值类型时,已经伴随Dude的构造方法生成了一个完全独立的结构体Shoe对象,所以Bill为蓝色的鞋,Ted为红色的鞋;当Shoe为引用类型时,Shoe仅仅初始化了一次,所以Ted在使用Shoe时,其实更改的还是唯一初始化一次时的Shoe的内容,所以导致了最后大家都为红鞋。下文会应用深拷贝解决引用类型复制指针的问题。

      幸运的是,我们有一个ICloneable接口来帮我们解决问题。这个接口是一个基本的契约,所有Dudes遵守这个契约并且规定如何按顺序的复制避免Shoe Sharing问题。我们所有将被复制的类应该使用ICloneable接口,包括Shoe类。

      ICloneable包括一个方法:Clone()

    下面我们将实现这个接口:
    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内部,我们仅仅是New了一个新的Shoe对象,复制所有引用类型并且拷贝值类型,然后返回一个新对象。你可能注意到了String类已经实现了ICloneable接口,所以我们能调用Color.Clone方法。因为Clone返回一个对象的引用,我们必须在设置Shoe的颜色之前将类型显示转换成Shoe类型。

      注:String类型是一种特殊的引用类型,其表现形式类似于值类型,因为字符串不可改变,如果改变则产生一个新对象,请参考这里

      下一步,在我们的CopyDude方法中我们需要克隆Shoes代替拷贝。

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

      现在我们运行主方法:

    public static void Main()
    {
                   Class1 pgm = new Class1();
     
                      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

      这就是我们想要的。

    将事物包裹起来

      作为一个练习,我们希望总是克隆引用类型和复制值类型。(这将降低当你调试程序错误时所购买治疗头疼的阿司匹林的数量)

      所以,在头疼降低的情况下,让我们走的更远一些并且让我们整理下Dude类实现ICloneable接口方法代替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
     }

      我们所要做的仅仅是通过使用Dude.Clone改变Main方法中的内容。

    public static void Main()
    {
                   Class1 pgm = new Class1();
     
                      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的操作符“=”真是的克隆了字符串,所以你不必担心重复的引用。然而你必须注意内存膨胀。如果你回头看看图示,字符串是引用类型,它真的本应该是一个指向堆的指针,但是简单起见,它的作用类似于值类型。

    总结

      作为一个练习,如果我们打算每次都拷贝对象,我们应该实现ICloneable接口。这将确保我们的引用类型有点像模仿值类型的行为。正如你所见到的那样,记录我们正在处理的变量是重要的,因为引用类型和值类型在创建内存上的区别。

      在这下一篇文章中,我们将审视一种降低内存印记的方式。

      1.  引用类型的拷贝一定要注意是深拷贝,还是简单的指针复制的浅拷贝。

      2.  System.String类型是特殊的引用类型,实际作用效果类似于值类型。

      3.  引用类型应该实现ICloneable接口,实现深拷贝,即对象拷贝而非指针拷贝。

  • 相关阅读:
    Sea Battle<海战>(思路题)
    Hat's Fibonacci hdu 1250
    Nearest Neighbor Search
    Divisible by Seven CodeForces
    手机自动化测试:appium源码分析之bootstrap六
    手机自动化测试:appium源码分析之bootstrap五
    手机自动化测试:appium源码分析之bootstrap四
    手机自动化测试:appium源码分析之bootstrap三
    手机自动化测试:appium源码分析之bootstrap二
    手机自动化测试:appium源码分析之bootstrap一
  • 原文地址:https://www.cnblogs.com/cuiyansong/p/4419021.html
Copyright © 2020-2023  润新知