• 【C# .Net】List循环add,出现数据相同现象? 引发对引用类型和值类型的底层逻辑的思考。


    赶项目时发现了一个问题,定义一个引用对象,如果在循环外定义对象,在循环内list.add(object)。最后的结果却是所有的对象值都是一样的,即每add一次,都会把之前的数据覆盖。

    解决方法:把对象在循环里new就行了,这样并不会造成很大的内存消耗,因为循环结束new的对象很快都被GC了。

    虽然解决了此问题,但想感觉里面的逻辑有点意思,想深入了解里面包含了一些.net底层存储的知识,引用类型和值类型的区别,还有String这个特殊的引用类型的探究。

    首先说一下结论:对于List<T>来说,如果T是引用类型,那保存的是引用,如果是值类型,保存的是值本身。

    下面是demo,分别是 object,int ,string 。

             

    结果分别是:

                  

    如果不在循环内部创建对象, 一般情况下,引用类型(除了string)会被覆盖,而值类型不会被覆盖,这是什么原因呢?

    分析:

    请观察下图,new User1对象,然后用User2 = User1 给User2 赋值。

     

    值类型(int,stuct,bool,enum,float,decimal),声明后,无论是否有值,编译器先分配其内存(分配在栈)。

    引用类型(object,interface,delegate,array)引用类型当声明一个类时,只在栈中分配内存用于容纳地址,而此时并没有为其分配堆上的内存空间。当new 一个实例时,分配至堆上,并把堆的地址保存到栈上。

    回到上面的例子,对于引用类型,在循环外new了 user 对象后,这个对象的引用地址就确定了。到第二次ladd时,list[0]中保存的User对象和list[1]对象是同一个对象,使用的是同一个地址,也就是说在添加list[1]是,list[0]也被修改了,因为它俩指针指向同一个地址,结果就是list都是最后的list[i]的数据。 

    其他:String是一种特殊的引用类型

    String类型直接继承自Object,这使得它成为一个引用类型,也就是说栈上不会有任何字符串。但是与其他引用类型不同是,string具有不变性。

    String的不变性:

    String的值改变时,会检查内存,如果与原来的值不同,则会重新分配内存空间,分配地址到栈上,数据到堆上,而不会影响到原有的值。

    这个原因也是为什么字符串大范围修改要用StringBuilder,而不是string,每次改变string时,会消耗内存,频繁的处理string对象,会消耗大量的内存。

    发散一下思考:

    出现以下情况是因为 == 如果比较的是引用类型,那么比较的是引用地址指向的数据是否是同一个,而不是底层对象的实际值。

    public class A
    {
      public string Name;
      public A(string n) { Name = n; }
    }

    A a1 = new A("sima");
    A a2 = new A("sima");
    Console.WriteLine(d1 == d2);     // False
    A a3 = a1;
    Console.WriteLine(d1 == d3);     // true

    第二种问题,无法交换两个string。

    出现以下原因是因为,参数传递是默认是值传递,Swap方法中的a,b是新在栈中开辟的内存数据,并非参数本身。

     解决方法也很简单:使用ref关键字传参改为引用传递。

    P.S string是特殊的引用类型,值存储在栈上。

           

     再进一步思考,比如 List 这种东西就有一个奇怪的事情。如果在传参的时候直接把List变为空,竟然无法修改值,引用类型为什么无法修改原有的值

     但是如果我对List进行Add 或者Remove 或者赋值的时候原来的值还是会改变???

                

     思考了许久,看了下上文描述引用类型标红语句 引用类型当声明一个类时,只在栈中分配内存用于容纳地址,而此时并没有为其分配堆上的内存空间

    再参考了栈堆分配的图,明白了为什么会这样。

    因为 List (a1) 在主方法是  值传递过去 创建副本List(a2) 这个栈中属于不同的两个内存,但是他们储存的引用地址是一样的,指向的是堆中同一个内存。

    所以副本 List(a2) =null 不影响 List(a1) ,但是对引用到的堆中的数据的修改,会使得指向同一个堆的两个不同 List(a1) List(a2) 结果相同。 

     

  • 相关阅读:
    帧锁定同步算法
    为 Raft 引入 leader lease 机制解决集群脑裂时的 stale read 问题
    etcd:从应用场景到实现原理的全方位解读
    给定一个二叉搜索树(BST),找到树中第 K 小的节点
    UDP如何实现可靠传输
    理解TCP/IP三次握手与四次挥手的正确姿势
    Redis持久化
    Redis提供的持久化机制(RDB和AOF)
    redis渐进式 rehash
    redis rehash
  • 原文地址:https://www.cnblogs.com/simawenbo/p/14368365.html
Copyright © 2020-2023  润新知