c#:值类型、引用类型、装箱和拆箱、结构体、readonly、ref

2024-05-15 18:18

本文主要是介绍c#:值类型、引用类型、装箱和拆箱、结构体、readonly、ref,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

环境:

  • window10
  • vs2019
  • .net core 3.1 控制台

参考:
《C#中定义装箱和拆箱详解》
《c# struct 灵魂拷问》
《[译]C# 7系列,Part 6: Read-only structs 只读结构》
《[译]C# 7系列,Part 9: ref structs ref结构》
《.NET高性能编程 - C#如何安全、高效地玩转任何种类的内存之Span的本质(一)。》
《.NET高性能编程 - C#如何安全、高效地玩转任何种类的内存之Span的秉性特点(二)。》

说明:最近看到关于Span的介绍,其中涉及到《值类型、引用类型、装箱和拆箱、结构体、readonly、ref》的知识,这里做一下总结。

一、值类型和引用类型

c#的数据类型分两类:

  • 值类型: Sbyte、Byte、Short、Ushort、Int、Uint、Long、Ulong、Char、Float、Double、Bool、Decimal、枚举(enum)、结构(struct);
    它们全部隐式继承自:abstract class System.ValueType,也就是说代码定义时你只需要定义为struct,编译后它就会自动继承System.ValueType,而你在写代码的时候无法去给struct指定继承任何东西。

    反编译int类型的代码如下:
    在这里插入图片描述
    在这里插入图片描述

  • 引用类型:类、数组、接口、委托、字符串等;

程序中的主要内存类型:

  • 栈内存(stack):

    空间较小,方法的调用、代码执行、本地变量、方法参数都存储在这里。

  • 堆内存(heap)

    空间较大,引用类型变量的数据都存储在这里,同时栈内存中会有这里地址的引用。

示意图如下:
在这里插入图片描述
程序方法中对于值类型和引用类型的赋值情况如下:
在这里插入图片描述

二、装箱和拆箱

当我们代码中使用object类型变量指向一个int类型数据时,.net就会自动将这个int数据拷贝到堆上,然后将分配的地址告诉这个object类型变量,这叫装箱;
当我们将上面定的的object类型变量又强转成int类型时,.net就会自动将这个int数据从堆上拷贝到栈里,这叫拆箱;

考虑下面简单的代码:

public static void Main()
{// i是值类型,存储在栈中int i = 5;// obj是引用类型变量,将i复制到堆中,并把地址给objobject obj = i;// obj指向的堆中的数据复制到栈中转成int类型给变量ii = (int)obj;
}

观察下编译后的IL:
在这里插入图片描述

三、值类型的比较

我们知道,代码中比较两个对象是否相等是基本操作,对于引用类型默认调用Object.Equals方法,比较的是内存地址,那么至于值类型是怎么比较的呢?

我们可以直接看ValueType的Equals方法:
在这里插入图片描述

四、关键字ref

一般我们定义方法如下:

public void Show(Object p){}
public void Add(int i,int j){}

上面两个方法调用的时候都是将变量拷贝一下再传递进去的,称为传值。
对于引用类型变量,拷贝的变量在堆内存中的地址,虽然不是一个变量了,但它们指向的地址是相同的;
对于值类型变量,由于它们的数据就在栈中,所以拷贝的是数据本身,所以我们在上面的Add方法中修改i和j的值,无法反馈到外层调用者。为了解决这个问题,c#中提供了ref关键字,当方法参数前加上它后,.net在调用时就直接将值类型的地址传了进来,而不是传一个拷贝的数据,此时我们在Add方法中修改i和j的值,外层调用者也能看到,定义如下:

public void Add(ref int i,ref int j){};

对于引用类型变量参数,我们也可以加ref关键字,原理一致,效果就是我们在方法内对p赋值,那么外层调用者也能看到这个变量指向的堆地址改变了。

另外,ref关键字不仅能用在方法参数前,在方法中也可以使用,如下:

public static void Show()
{var person1 = new Person() { Age = 18 };// 将person1指向的内存地址赋值给person2var person2 = person1;// 改变person2指向的内存地址,此时person1指向的内存地址并没有变person2 = new Person() { Age = 19 };Debug.Assert(person1.Age == 18);//将person1变量的地址赋值给person3(注意:不是将person1指向的地址赋值给person3),此时可认为person3就是person1ref Person person3 = ref person1;person3 = new Person() { Age = 20 };Debug.Assert(person1.Age == 20);
}

五、结构体

int等数据类型除了是c#中的基础类型(值类型)之外,它们还都是结构体。

结构体和类看起来很像,但是它们有很大不同:

  • 类可以继承、可以多态,而结构体主要是对数据的一块封装,不能继承,当然也就没有多态;
  • 类的数据存储在堆中,结构体的数据存储在栈中;

关于struct的问题:

  • struct继承自抽象类ValueType,最终继承自Object,为什么它就是值类型呢?

    其实这是一个约定,c#中结构体并没有继承的功能,但总有一些方法要复用,所以编译器不让我们写继承,而是它自己偷偷的继承了ValueType。

  • struct能实现接口嘛,和类实现接口有什么不同?

    struct可以实现接口,因为接口本身是一系列方法的签名。
    不过,因为类和接口都是引用类型,而struct是值类型,所以当使用接口变量指向结构体时会将结构体装箱到堆中。当又使用结构体变量指向上面装箱的接口时就会发生拆箱的动作,这一装一拆可能会导致你对结构体更改的内容失效,如下代码:

     public static void Main(){var cat = new Cat { Id = 1 };//栈中的数据Console.WriteLine($"cat.Id={cat.Id}");//输出: cat.Id=1var animal = cat as IAnimal;animal.Update(2);//装箱后到了堆里,并在堆里改了值,打印出堆里的值Console.WriteLine($"animal.Show()={animal.Show()}");//输出: animal.Show()=2var cat2 = (Cat)animal;//拆箱后把数据从堆里复制到了栈,新的结构体Console.WriteLine($"cat2.Id={cat2.Id}");//输出: cat2.Id=2//栈中原来的数据并不受影响Console.WriteLine($"cat.Id={cat.Id}");//输出: cat.Id=1cat.Id = 3;//栈中的数据直接修改为Console.WriteLine($"cat.Id={cat.Id}");//输出: cat.Id=3//栈中的数据直接修改并不影响堆里的数据以及新的结构体数据Console.WriteLine($"animal.Show()={animal.Show()},cat2.Id={cat2.Id}");//输出: animal.Show()=2,cat2.Id=2}
    
  • struct中this是可以写入的?

    没错,和类不同,在struct方法中,你可以给this赋值,负值后这个结构体的数据奖杯覆盖,如下:

    class TestDemo
    {public static void Main(){var cat = new Cat { Id = 1 };var cat2 = new Cat { Id = 2, Name = "小明" };//相当于将cat2中的数据拷贝到cat中//但cat和cat2仍然是两块内存cat.Update(cat2);Console.WriteLine($"cat.Id={cat.Id},cat.Name={cat.Name}");//输出: cat.Id=2,cat.Name=小明}
    }public struct Cat
    {public int Id { get; set; }public string Name { get; set; }public void Update(Cat cat){this = cat;}
    }
    

六、readonly struct

即只读的struct,如果struct不是只读的话,那么它在装箱和拆箱的时候可能会影响到你的代码逻辑。

我们习惯对类属性的修改操作是一处修改,处处修改,因为类的数据存储在堆中,无论我们用接口或者是数据类型本身去引用它都不是拆箱装箱,不会产生数据副本,所以数据只有一份,自然是修改一处,处处修改。

但通过上面的示例,我们看到系统在自动拆箱装箱的时候会产生数据不一致的情况,所以出现了readonly,当我们把结构体声明为readonly的时候,就表示我们仅希望在创建结构体时对成员赋值,一旦创建完毕就不要再改动里面的值了,如下:

class TestDemo
{public static void Main(){var cat = new Cat(1, "小明");//因为cat是readonly的,所以系统仅允许我们构建时赋值,在装箱拆箱后均不允许修改结构体内容,所以能避免数据不一致的问题var animal = cat as IAnimal;var cat2 = (Cat)animal;// 因为除了在构造函数中不允许修改结构体数据,所以即使发生了装箱和拆箱也不影响最终数据的一致性// 但我们要知道,cat和cat2是栈内存中两个不同的地址,而animal引用的数据在堆中}
}public readonly struct Cat : IAnimal
{public readonly int Id { get; }public readonly string Name { get; }public Cat(int id, string name){this.Id = id;this.Name = name;}public Cat(Cat cat){// 构造函数中可以改数据,自然也可以对this赋值this = cat;}public void Update(int id){//因为readonly已经要求属性必须是只读的所以下面肯定报错//this.Id = id;}public void UpdateThis(Cat cat){//因为结构体是只读的,所以不允许对this赋值//this = cat;}public void UpdateId(int id){//readonly的,报错//this.Id = id;}
}public interface IAnimal
{void Update(int id);
}

可以看出加了readonly的struct虽然限制了其功能,但解决了数据一致性的问题!

七、ref readonly struct

在c#中我们能看到Span<T>结构体的定义如下:
在这里插入图片描述
那么在struct加上readonly ref是什么作用呢?

答: 限制结构体必须存储在栈中!!!

既然限制在栈中,那么就不会发生装箱和拆箱,那么还能实现接口嘛?不能了!!下面代码报错:
在这里插入图片描述
限制结构体只能存储在栈中的后果不止不能实现接口,还有:

  • 不能用作类的成员变量;

    因为类是存储在堆里的,而ref结构体不能存储在堆里,所以不能作为类的成员变量,示例报错代码:
    在这里插入图片描述

  • 不用用作非ref结构体的成员变量

    因为非ref结构体可以作为类属性存储在堆里,所以ref结构体不能作为非ref结构体的属性,如下代码:
    在这里插入图片描述

  • 不能用作泛型参数

    这里暂且理解为:因为委托是引用类型,所以ref结构体不能做它的泛型参数,如下代码:
    在这里插入图片描述

  • 不能用作异步方法(async/await)的参数

    因为异步方法的参数在编译时会被放进状态机的属性中,所以异步方法自然不能用ref结构体做参数,代码如下:
    在这里插入图片描述

  • 不能用作lamda表达式的参数

    直接看代码报错情况:
    在这里插入图片描述

那么,ref的结构体应该怎么使用呢?

答: 应该仅考虑将它同在局部变量或非异步的参数上。

如下使用示例:
在这里插入图片描述

这篇关于c#:值类型、引用类型、装箱和拆箱、结构体、readonly、ref的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/992560

相关文章

2. c#从不同cs的文件调用函数

1.文件目录如下: 2. Program.cs文件的主函数如下 using System;using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using System.Windows.Forms;namespace datasAnalysis{internal static

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

用命令行的方式启动.netcore webapi

用命令行的方式启动.netcore web项目 进入指定的项目文件夹,比如我发布后的代码放在下面文件夹中 在此地址栏中输入“cmd”,打开命令提示符,进入到发布代码目录 命令行启动.netcore项目的命令为:  dotnet 项目启动文件.dll --urls="http://*:对外端口" --ip="本机ip" --port=项目内部端口 例: dotnet Imagine.M

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

两个月冲刺软考——访问位与修改位的题型(淘汰哪一页);内聚的类型;关于码制的知识点;地址映射的相关内容

1.访问位与修改位的题型(淘汰哪一页) 访问位:为1时表示在内存期间被访问过,为0时表示未被访问;修改位:为1时表示该页面自从被装入内存后被修改过,为0时表示未修改过。 置换页面时,最先置换访问位和修改位为00的,其次是01(没被访问但被修改过)的,之后是10(被访问了但没被修改过),最后是11。 2.内聚的类型 功能内聚:完成一个单一功能,各个部分协同工作,缺一不可。 顺序内聚:

Mysql BLOB类型介绍

BLOB类型的字段用于存储二进制数据 在MySQL中,BLOB类型,包括:TinyBlob、Blob、MediumBlob、LongBlob,这几个类型之间的唯一区别是在存储的大小不同。 TinyBlob 最大 255 Blob 最大 65K MediumBlob 最大 16M LongBlob 最大 4G