25.值类型和引用类型补充

25.值类型和引用类型补充


25.1 知识点

知识回顾

  • 值类型
    • 无符号: byte, ushort, uint, ulong
    • 有符号: sbyte, short, int, long
    • 浮点数: float, double, decimal
    • 特殊: char, bool
    • 枚举: enum
    • 结构体: struct
  • 引用类型
    • string
    • 数组
    • class
    • interface
    • 委托
  • 值类型和引用类型的本质区别
    • 值的具体内容存在栈内存上
    • 引用的具体内容存在堆内存上

如何判断 值类型和引用类型

使用 F12 进到类型的内部去查看,是 class 就是引用,是 struct 就是值。

int i = 12;            // public readonly struct Int32
string str = "123";    // public sealed class String

语句块

主函数内

语句块语句块基本结构:

  • 命名空间
  • 类、接口、结构体
  • 函数、属性、索引器、运算符重载等(类、接口、结构体)
  • 条件分支、循环

语句块层级:

  • 上层语句块:类、结构体
  • 中层语句块:函数
  • 底层的语句块: 条件分支、循环

我们的逻辑代码写在哪里?

  • 函数、条件分支、循环-中底层语句块中

我们的变量可以申明在哪里?

  • 上、中、底都能申明变量
  • 上层语句块中:成员变量
  • 中、底层语句块中:临时变量

变量的生命周期

class语句块类 主函数外 声明类的成员变量b

static int b = 666;    // 声明类的成员变量b

编程时大部分都是临时变量,在中底层申明的临时变量(函数、条件分支、循环语句块等)语句块执行结束,没有被记录的对象将被回收或变成垃圾。
值类型被系统自动回收,引用类型栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次GC回收。

临时变量正常回收实例

int i2 = 1;            // 值类型当这个语句块正常执行完时被系统自动回收
string str2 = "123";   // 引用类型当这个语句块正常执行完时栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次GC回收

使用不同语句块的同名变量

{
    int b = 999;    // 声明临时变量b
    // 这个语句块结束后临时变量b就会回收
    
    // 这个语句块直接输出b输出的是这个语句块的b
    Console.WriteLine("临时变量b:" + b);    // 临时变量b: 999
    
    // 因为这个语句块还有一个临时变量b,所以输出类的成员变量b要用类名点出来
    Console.WriteLine("类的成员变量b:" + Program.b);    // 类的成员变量b: 666
}
// 在这个语句块只有类的成员变量b了,直接输出即可
Console.WriteLine("类的成员变量b:" + b);    // 类的成员变量b: 666

减少临时变量的创建 提高循环效率

//每次循环都会创建一个临时变量 会不停的回收临时变量 造成一定的性能开销
//while (true)
//{
//    //每次的index1其实都不是同一个index1
//    int index1 = 1;
//}

//在上一层级的代码块创建一个临时变量 供循环内使用 就不用每次循环都创建一个临时变量了
//int index2 = 0;
//while (true)
//{
//    //每次的index2其实是同一个index2
//    index2 = 1;
//}

用更高级别的语句块的变量记录函数里要记录的变量,创建测试类

//测试类
class TestClass
{
    //用更高级别的语句块的变量记录函数里要记录的变量
    int testRecordInt = 0;
    //记录方法
    public void RecordInt(int recordInt)
    {
        testRecordInt = recordInt;
    }

    //静态变量记录要记录的变量
    public static int testStaticRecordInt = 0;

    //测试类内
}

记录低级别代码块的变量

//想要不被回收或者不变垃圾 必须将其记录下来
//如何记录?在更高层级的变量记录或者使用静态全局变量记录
int recordInt = 10;//要记录的值
//静态变量记录要记录的变量
TestClass.testStaticRecordInt = recordInt;
//用更高级别的语句块的变量记录函数里要记录的变量
TestClass testClass = new TestClass();
testClass.RecordInt(recordInt);

结构体中的值类型和引用类型

创建测试结构体

// 测试结构体
struct TestStruct
{
    // 结构体中的引用类型变量
    public TestClass testClass;
    // 结构体中的值类型变量
    public int i;
}

结构体本身是值类型,结构体是存在栈中的,前提:该结构体没有做为其它类的成员。
在结构体中的值类型存在栈中,存在栈中的是存储值具体内容。
在结构体中的引用类型存在堆中,存在栈上的是指向堆中的地址,堆中的对应地址存储引用的具体内容。

//实例
TestStruct testStruct1 = new TestStruct();//结构体在栈中 结构体的new 不是new一个对象而是调用隐式构造函数给变量赋值
testStruct1.i = 1;//结构体中的值类型 存在栈中 存储具体值
testStruct1.testClass = new TestClass();//结构体中的引用类型 存在栈上的是堆中地址 堆中地址存储引用的具体内容

类中的值类型和引用类型

测试类内

// 类中的引用类型变量
public string str = "123";
// 类中的值类型变量
public TestStruct testStruct = new TestStruct();

类本身是引用类型,类是存在堆中的。
在类中的值类型存在堆中,和这个类在堆中所在的大房间是一起的,堆中存储具体的值。
在类中的引用类型存在堆中,和这个类在堆中所在的大房间不是一起的,在类中存储的其实是指向其他堆房间的地址,其他堆房间中存储具体的值。
值类型变量是根据他是结构体中(这个结构体可能也在堆上)还是类中的成员判断他在栈上还是堆上,引用类型始终在堆上。

实例

TestClass testClass1 = new TestClass();    // 类在堆中,开辟类堆房间
testClass1.str = "Hello World!";    // 不在类堆房间中,在其他新开的堆房间中存储具体内容,类堆房间存的是其他新开的堆房间的地址
testClass1.testStruct = new TestStruct();    // 在类堆房间中存储具体内容

数组中的存储规则

数组本身是引用类型。
值类型数组栈上存的是指向堆中第一个元素房间的地址,堆中房间存具体内容。
引用类型数组栈上存的是指向堆中第一个元素房间的地址,堆中房间存具体内容的地址逐个映射指向具体内容的堆房间里。

int[] arrayInt = new int[5];    // 开辟五个堆房间,里面存具体值(默认值0),栈上存第一个堆房间的地址
object[] objs = new object[5];    // 开辟五个堆房间,里面存具体值的地址(默认值null),栈上存第一个堆房间的地址

结构体继承接口

声明测试接口和测试结构体2

// 测试接口
interface ITest
{
    int Value
    {
        get;
        set;
    }
}

// 测试结构体2 继承测试接口
struct TestStruct2 : ITest
{
    int value;
    public int Value
    {
        get
        {
            return value;
        }
        set
        {
            this.value = value;
        }
    }
}

利用里氏替换原则,用接口容器装载结构体存在装箱拆箱。

// new一个TestStruct2类型变量testStruct21 设置Value
TestStruct2 testStruct21 = new TestStruct2();
testStruct21.Value = 1;
Console.WriteLine("testStruct21.Value:" + testStruct21.Value);    // testStruct21.Value:1

// 再创建TestStruct2类型变量testStruct22 让testStruct22 = testStruct21 这样就会把testStruct21的Value赋值过来
TestStruct2 testStruct22 = testStruct21;
Console.WriteLine("testStruct22.Value:" + testStruct22.Value);    // testStruct22.Value:1

// 修改testStruct22的Value打印结果
testStruct22.Value = 2;
Console.WriteLine("testStruct22.Value:" + testStruct22.Value);    // testStruct22.Value:2
Console.WriteLine("testStruct21.Value:" + testStruct21.Value);    // testStruct21.Value:1
// 注意:值类型的=赋值是复制多一份在栈上,而不是指向同一地址,所以修改一个对象的变量另一个对象的变量不受影响

// 创建一个ITest接口对象iTest1装结构体对象testStruct21 结构体用接口装存在装箱 TestStruct2继承了ITest接口 里氏替换原则 父类装子类
ITest iTest1 = testStruct21;
// 接口是引用类型的,具体是要把testStruct21从栈上复制移动到堆上,iTest1在指向testStruct21的地址,这就是装箱
Console.WriteLine("iTest1.Value:" + iTest1.Value);    // iTest1.Value:1

// 再创建一个ITest接口对象iTest2指向iTest1
ITest iTest2 = iTest1;
Console.WriteLine("iTest2.Value:" + iTest2.Value);    // iTest2.Value:1
Console.WriteLine("iTest1.Value:" + iTest1.Value);    // iTest1.Value:1

// 修改iTest2的Value打印结果


iTest2.Value = 99;
Console.WriteLine("iTest1.Value:" + iTest1.Value);    // iTest1.Value:99
Console.WriteLine("iTest2.Value:" + iTest2.Value);    // iTest2.Value:99
// 注意:iTest1和iTest2指向同一地址,所以修改一个对象的变量另一个对象的变量同样会受影响

// 创建一个ITest接口对象iTest3 把接口强转成结构体存在拆箱
TestStruct2 iTest3 = (TestStruct2)iTest1;
// 结构体是值类型的,具体是要把iTest1从堆上复制移动到栈上,iTest3的Value和iTest1的Value值一样,这就是拆箱
Console.WriteLine("iTest1.Value:" + iTest1.Value);    // iTest1.Value:99
Console.WriteLine("iTest3.Value:" + iTest3.Value);    // iTest1.Value:99

25.2 知识点代码

using System;
using System.Collections.Generic;

namespace Lesson24_值类型和引用类型补充
{
    //class语句块外 namespace语句块内

    #region 问题三 变量的生命周期

    //测试类
    class TestClass
    {
        //用更高级别的语句块的变量记录函数里要记录的变量
        int testRecordInt = 0;
        //记录方法
        public void RecordInt(int recordInt)
        {
            testRecordInt = recordInt;
        }

        //静态变量记录要记录的变量
        public static int testStaticRecordInt = 0;

        //测试类内

        #region 问题五 类中的值类型和引用类型

        //类中的引用类型变量
        public string str = "123";
        //类中的值类型变量
        public TestStruct testStruct = new TestStruct();

        #endregion
    }

    #endregion

    #region 问题四 结构体中的值类型和引用类型

    //测试结构体
    struct TestStruct
    {
        //结构体中的引用类型变量
        public TestClass testClass;
        //结构体中的值类型变量
        public int i;
    }

    #endregion

    #region 问题七 结构体继承接口

    //测试接口
    interface ITest
    {
        int Value
        {
            get;
            set;
        }
    }

    //测试结构体2 继承测试接口
    struct TestStruct2 : ITest
    {
        int value;
        public int Value
        {
            get
            {
                return value;
            }
            set
            {
                this.value = value;
            }

        }
    }

    #endregion

    class Program
    {
        //class语句块类 主函数外

        #region 问题三 变量的生命周期

        static int b = 666;//声明类的成员变量b

        #endregion
        static void Main(string[] args)
        {
            Console.WriteLine("值类型和引用类型补充");

            //主函数内

            #region 知识回顾

            //值类型
            //无符号:byte,ushort,uint,ulong
            //有符号:sbyte,short,int,long
            //浮点数:float,double,decimal
            //特殊:char,bool
            //枚举:enum
            //结构体:struct

            //引用类型
            //string
            //数组
            //class
            //interface
            //委托

            //值类型和引用类型的本质区别
            //值的具体内容存在栈内存上
            //引用的具体内容存在堆内存上

            #endregion

            #region 问题一 如何判断 值类型和引用类型

            //F12进到类型的内部去查看
            //是class就是引用
            //是struct就是值

            //实例
            int i = 12;//public readonly struct Int32
            string str = "123";//public sealed class String

            #endregion

            #region 问题二 语句块

            //语句块语句块基本结构
            //命名空间
            //   ↓
            //类、接口、结构体
            //   ↓
            //函数、属性、索引器、运算符重载等(类、接口、结构体)
            //   ↓
            //条件分支、循环

            //语句块层级
            //上层语句块:类、结构体
            //中层语句块:函数
            //底层的语句块: 条件分支 循环等

            //我们的逻辑代码写在哪里?
            //函数、条件分支、循环-中底层语句块中

            //我们的变量可以申明在哪里?
            //上、中、底都能申明变量
            //上层语句块中:成员变量
            //中、底层语句块中:临时变量

            #endregion

            #region 问题三 变量的生命周期

            //编程时大部分都是 临时变量
            //在中底层申明的临时变量(函数、条件分支、循环语句块等)
            //语句块执行结束 
            //没有被记录的对象将被回收或变成垃圾
            //值类型:被系统自动回收
            //引用类型:栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次GC回收

            //临时变量正常回收实例
            int i2 = 1;//值类型当这个语句块正常执行完时 被系统自动回收
            string str2 = "123";//引用类型当这个语句块正常执行完时 栈上用于存地址的房间被系统自动回收,堆中具体内容变成垃圾,待下次GC回收

            //使用不同语句块的同名变量
            {
                int b = 999;//声明临时变量b
                //这个语句块结束后 临时变量b就会回收

                //这个语句块 直接输出b输出的是这个语句块的b
                Console.WriteLine("临时变量b:" + b);//临时变量b: 999

                //因为这个语句块还有一个临时变量b 所以输出类的成员变量b 要用类名点出来
                Console.WriteLine("类的成员变量b:" + Program.b);//类的成员变量b: 666
            }
            //在这个语句块 只有 类的成员变量b了 直接输出即可
            Console.WriteLine("类的成员变量b:" + b);//类的成员变量b: 666


            //减少临时变量的创建 提高循环效率
            //每次循环都会创建一个临时变量 会不停的回收临时变量 造成一定的性能开销
            //while (true)
            //{
            //    //每次的index1其实都不是同一个index1
            //    int index1 = 1;
            //}

            //在上一层级的代码块创建一个临时变量 供循环内使用 就不用每次循环都创建一个临时变量了
            //int index2 = 0;
            //while (true)
            //{
            //    //每次的index2其实是同一个index2
            //    index2 = 1;
            //}

            //记录低级别代码块的变量
            //想要不被回收或者不变垃圾 必须将其记录下来
            //如何记录?在更高层级的变量记录或者使用静态全局变量记录
            int recordInt = 10;//要记录的值
            //静态变量记录要记录的变量
            TestClass.testStaticRecordInt = recordInt;
            //用更高级别的语句块的变量记录函数里要记录的变量
            TestClass testClass = new TestClass();
            testClass.RecordInt(recordInt);

            #endregion

            #region 问题四 结构体中的值类型和引用类型

            //结构体本身是值类型 结构体是存在栈中的
            //前提:该结构体没有做为其它类的成员
            //在结构体中的值类型 存在栈中 存在栈中的是 存储值具体内容
            //在结构体中的引用类型 存在堆中 存在栈上的是 指向堆中的地址 堆中的对应地址 存储引用的具体内容
            //注意:只要是引用类型 始终都存在堆中 就算是结构体中的引用类型变量也只是纯哥堆中的地址顺藤摸瓜摸到具体内容里的

            //实例
            TestStruct testStruct1 = new TestStruct();//结构体在栈中 结构体的new 不是new一个对象而是调用隐式构造函数给变量赋值
            testStruct1.i = 1;//结构体中的值类型 存在栈中 存储具体值
            testStruct1.testClass = new TestClass();//结构体中的引用类型 存在栈上的是堆中地址 堆中地址存储引用的具体内容


            #endregion

            #region 问题五 类中的值类型和引用类型

            //类本身是引用类型 类是存在堆中的
            //在类中的值类型 存在堆中 和这个类在堆中所在的大房间是一起的 堆中存储具体的值
            //在类中的引用类型 存在堆中 和这个类在堆中所在的大房间不是一起的 在类中存储的其实是指向其他堆房间的地址 其他堆房间中存储具体的值
            //注意:值类型变量是根据他是结构体中(这个结构体可能也在堆上)还是类中的成员 判断他在栈上还是堆上 引用类型始终在堆上

            //实例
            TestClass testClass1 = new TestClass();//类在堆中 开辟类堆房间
            testClass1.str = "Hello World!";//不在类堆房间中 在其他新开的堆房间中存储具体内容 类堆房间存的是其他新开的堆房间的地址
            testClass1.testStruct = new TestStruct();//在类堆房间中 存储具体内容

            #endregion

            #region 问题六 数组中的存储规则

            //数组本身是引用类型
            //值类型数组 栈上存的是指向堆中第一个元素房间的地址 堆中房间存具体内容
            //引用类型数组 栈上存的是指向堆中第一个元素房间的地址 堆中房间存具体内容的地址 逐个映射指向具体内容的堆房间里

            //实例
            int[] arrayInt = new int[5];//开辟五个堆房间 里面存具体值(默认值0) 栈上存第一个堆房间的地址
            object[] objs = new object[5];//开辟五个堆房间 里面存具体值的的地址(默认值null) 栈上存第一个堆房间的地址

            #endregion

            #region 问题七 结构体继承接口

            //利用里氏替换原则,用接口容器装载结构体存在装箱拆箱

            //new一个TestStruct2类型变量testStruct21 设置Value
            TestStruct2 testStruct21 = new TestStruct2();
            testStruct21.Value = 1;
            Console.WriteLine("testStruct21.Value:" + testStruct21.Value);//testStruct21.Value:1

            //再创建TestStruct2类型变量testStruct22 让testStruct22 = testStruct21 这样就会把testStruct21的Value赋值过来
            TestStruct2 testStruct22 = testStruct21;
            Console.WriteLine("testStruct22.Value:" + testStruct22.Value);//testStruct22.Value:1

            //修改testStruct22的Value打印结果
            testStruct22.Value = 2;
            Console.WriteLine("testStruct22.Value:" + testStruct22.Value);//testStruct22.Value:2
            Console.WriteLine("testStruct21.Value:" + testStruct21.Value);//testStruct21.Value:1
            //注意:值类型的=赋值是复制多一份在栈上 而不是指向同一地址 所以修改一个对象的变量另一个对象的变量不收影响

            //创建一个ITest接口对象iTest1装结构体对象testStruct21 结构体用接口装存在装箱 TestStruct2继承了ITest接口 里氏替换原则 父类装子类 
            ITest iTest1 = testStruct21;
            //接口是引用类型的 具体是要把testStruct21从栈上复制移动到堆上 iTest1在指向testStruct21的地址 这就是装箱
            Console.WriteLine("iTest1.Value:" + iTest1.Value);//iTest1.Value:1

            //再创建一个ITest接口对象iTest2指向iTest1 
            ITest iTest2 = iTest1;
            Console.WriteLine("iTest2.Value:" + iTest2.Value);//iTest2.Value:1
            Console.WriteLine("iTest1.Value:" + iTest1.Value);//iTest1.Value:1

            //修改iTest2的Value打印结果
            iTest2.Value = 99;
            Console.WriteLine("iTest1.Value:" + iTest1.Value);//iTest1.Value:99
            Console.WriteLine("iTest2.Value:" + iTest2.Value);//iTest2.Value:99
            //注意:iTest1和iTest2指向同一地址 所以修改一个对象的变量另一个对象的变量同样会影响

            //创建一个ITest接口对象iTest3 把接口强转成结构体存在拆箱 
            TestStruct2 iTest3 = (TestStruct2)iTest1;
            //结构体是值类型的 具体是要把iTest1从堆上复制移动到栈上 iTest3的Value和iTest1的Value值一样 这就是拆箱
            Console.WriteLine("iTest1.Value:" + iTest1.Value);//iTest1.Value:99
            Console.WriteLine("iTest3.Value:" + iTest3.Value);//iTest1.Value:99

            #endregion
        }
    }



}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏