• Array,List,Struct可能被大家忽略的问题


    Q1:

    首先定义一个结构

    public struct MyStruct { public int T; }

    定义一个泛型List来存放结构体,然后访问第一个元素去修改T,输出T:

    List<MyStruct> arrLis =new List<MyStruct>(){new MyStruct()};

    arrLis[0].T = 100;

    Console.WriteLine(arrLis[0].T);

    大家猜是什么结果?

    很遗憾不是100,arrLis[0].T = 100;VS提示该语句有错误。Cannot modify the expression because it is not a variable.

    image

    说修改的不是一个变量。

    这是为什么呢?

    关于这个问题我们首先来看一下List的源码

    image

    其实List[]被称做索引器。索引的实现其实类似属性,靠一对Get,Set方法来实现的。索引器其实只是C#的语法糖而已。那么很明显我们上面的语句其实只是调用了get_Item方法而已,且返回值MyStruct是个值类型。所以get_Item方法返回的是一个值(value)。你也许会说,那又怎么样,我为什么就不能修改这个值。很不辛,在.NET中值(value)是不能被修改的,只有变量(variable)才能够被修改,这就是为什么变量称之为”变量”了:)。

    Q2:

    再看下面的代码,我们修改一下,把泛型List改为Array数组。

    MyStruct[] arrStr =new MyStruct[1]{new MyStruct()};

    arrStr[0].T = 100;

    Console.WriteLine(arrStr[0].T);

    你是否觉得这次赋值语句也会报错?

    其实不然,代码顺利通过编译,运行成功。

    结果输出:100

    这太奇怪啦,为什么把List改成Array就没有问题了呢。

    让我们继续查看一下源码

    image

    看到没,对于一维数组的访问其实是访问到了这个GetValue方法。该方法的意思是使用typeReference去取到位于index位置的对象的引用,然后转换为Object返回。看来原因就在这里了,对于数组的[]索引器其实是返回了对象的一个引用(地址),也就是相当于我们使用Array[0]访问的是得到的是一个变量(variable),所以可以直接给内部的成员变量赋值。

    对于这段源码也许不是那么好理解,不妨看看IL。

    image

    ldelema:将位于指定数组索引的数组元素的地址作为 & 类型(托管指针)加载到计算堆栈的顶部。

    这就很清楚了,在IL里面也清楚的显示,操作的是对象的地址。

    到这里,Array跟List索引访问的区别出来了,Array是返回了对象的引用,而List返回的就是对象的值(值类型对象就是内部的值,引用类型对象是引用的地址)。

    Q3:

    还没完,既然直接给赋值不行,那我用一个Set方法包装起来,去设置内部变量的值如何?

    public struct MyStruct {

    public int T;

    public void SetT(int t)

      { T = t; }

    }

    改造一下,加了一个SetT方法。

    把List初始化语句也改一下,去掉一些语法糖,因为我们要查IL,语法糖会影响我们的判断。

    A:

    List<MyStruct> arrLis = new List<MyStruct>();

    var myStruct = new MyStruct();

    arrLis.Add(myStruct);

    arrLis[0].SetT(100);

    Console.WriteLine(arrLis[0].T);

    以上代码顺利通过。

    输出:0

    那为什么直接访问方法就可以呢。其实arrLis[0].SetT(100); 这也可以算是一个语法糖。上面A段代码到了IL层面其实就相当于下面B段代码,IL还是会用一个局部变量去接arrLis[0]返回的值。

    B:

    List<MyStruct> arrLis = new List<MyStruct>();

    var myStruct = new MyStruct();

    arrLis.Add(myStruct);

    var temp = arrLis[0];

    temp.SetT(100);

    Console.WriteLine(arrLis[0].T);

    不信我们查一下IL:

    左边是A段代码,右边是B段代码:

    image

    这2段IL只有红线画出来的地方不一样,其实就是一个变量命名不一样而已。

    Q4:

    那上面A段代码输出为什么是0呢?

    这个也很好理解,既然arrLis[0].SetT(100); 相当于var temp = arrLis[0]; 那么值类型赋值操作,其实是把右边的值(副本)赋值给了左边的变量,我们用SetT来修改T的时候只是在修改temp里面的T而已。这个不用多解释吧。

    总结:

    当我们在List里面使用值类型的时候一定要格外小心,特别是使用结构体的时候,因为从表象上来说更像一个引用类型(结构可以定义方法,成员变量等),不知不觉你就会用引用类型对象的惯用法去处理问题,说不定就掉坑了。所以结构体最好定义为不可变的。

    参考:

    why-can-struct-change-their-own-fields

    what-is-the-difference-between-listt-and-array-indexers

    internals-of-array

  • 相关阅读:
    linux 内核防火墙配置规则
    postfix 配置邮件服务器
    JDK 和 tomcat 安装 配置
    ifstat-网络接口监测工具
    一个比较通用的Makefile
    [转]vim常用命令
    [转]Valgrind简单用法
    [转]UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching)
    [转]Reed Solomon纠删码
    [转]分布式文件系统FastDFS架构剖析
  • 原文地址:https://www.cnblogs.com/kklldog/p/3449415.html
Copyright © 2020-2023  润新知