C# 记录一下值类型和引用类型的使用并踩踩坑

2022-06-21

说明

本文简单的介绍一下值类型和引用类型,以及在平常工作中经常碰到的坑,大多数概念都是参考别人的博客,只是加了点自己的理解,并动手体验了一把。

参考:c#中的值类型有哪些 - CSDN

值类型

值类型主要包括:结构体(struct)、枚举(Enum)

如我们常见的:int、long、bool、byte、char、DateTime、double、float、decimal,我们也可以使用 struct 关键字自定义值类型

c#long源码结构

值类型的特征
  • 值类型不可被继承,也不可继承,但是可以实现接口

image-20220613164212724

  • 值类型不能包含Null值

image-20220613164530725

  • 值类型具有默认值

image-20220613165308179

类型默认值
int0
Stringnull
long0L
double0.0d
float0.0f
char\u0000
byte(byte)0
short(short)0
存储位置

值类型总是分配在它声明的地方:作为字段时,跟随其所属的变量(实例)存储;作为局部变量时,存储在栈上。

值类型在内存管理方面具有更好的效率,并且不支持多态,适合用作存储数据的载体。当作用域结束时,所占空间自行释放,效率高,无需进行地址转换

函数参数传递

值类型经过函数传递,并且在函数中进行将值修改,外部的值是不会受影响的,如下我们在函数中将value的值加1,而外部value的值并没有改变

image-20220616144416367

如何比较

如何比较两个值类型是否一致呢?最简单的方法就是==符号了,再有一个就是Equals

==比较有限制,两边的类型需要”一致“,如两边都是数值类型或者布尔类型,而不管你是整形还是浮点型,只要两边的数值一样,都会返回True。

通过重载,基本值类型之间都可以通过Equals进行比较,如布尔类型与数值类型比较。只有两者类型相同且数值相同,才会返回True,如整形和浮点型通过Equals进行比较,永远返回False

image-20220618162808390

引用类型

值类型主要包括:类(class)、数组(array)、接口(interface)、委托(delegate)、object、字符串、匿名类

如我们常见的:string[]、int[]、用户自定义的类、List、Dictionary

引用类型的特征
  • 当我们读取引用对象时,实际上我们读取的是他指向的引用地址,他变我也变(string类型除外)

image-20220614102601863

  • 判断对象是否相等时,不可直接使用==比较,(string除外)

image-20220621100200275

存储位置

引用类型在栈中存储一个引用,其实际的存储位置位于托管堆,即引用类型存储在托管推上。

引用类型支持多态,适合用于定义应用程序的行为,引用类型由GC来控制其回收,需要进行地址转换,效率降低

函数参数传递

引用类型经过函数传递,并且在函数中进行将值修改,外部的值也会受影响。因为函数传递过去是变量的引用地址。如下我们在函数中将age的值加1,外部age的值也跟着改变了(string字符串类型除外)

image-20220616144732934

如何比较

如何比较两个值类型是否一致呢?最常见的两种方式:Equals、ReferenceEquals

Equals、ReferenceEquals判断的是引用,当两个对象指向同一个引用地址时,则返回True(当然匿名类,string类除外)

image-20220621101131012

其他

Visual Studio如何查看引用地址

开启调试—>头部菜单【调试】—>选择【窗口】选项—>选择【内存】选项—>选择【内存1】

image-20220614180908272

打开如下图,我么只要关注【地址】输入框即可

image-20220614183034695

接着我们把对应的变量名输入到【地址】框,回车后就可以看到地址了,如输入a、b,我们可以看到这两个变量的内存地址是一致的,说明他俩指向的是同一个内存地址

image-20220614183440613

如何判断值类型或者引用类型

image-20220614170202033

特殊引用类型——string

参考:C# 引用类型之特例string - 走看看 (zoukankan.com)

字符串的赋值操作

正常创建引用类型我们都需要使用关键词new,才能得到一个对象,而string却可以像值类型一样直接用赋值。这是微软为了方便大家,可以直接定义字符串变量并且赋值操作(具体怎么回事,没查到...)看起来大概如下

image-20220614174942520

字符串的引用地址

正常引用类型a赋值给b,那么a、b均指向同一个内存地址,而字符串a赋值给b,指向的却是不同的内存地址

我们先来看一下对象a、b的内存地址,我们可以看到他俩都指向同一个引用地址

ab对象引用地址

再来看字符串aa、bb的引用地址,尽管字符串也是引用类型,但这俩的引用地址并不一样

这是因为我们给字符串赋值时,默认会创建一个新字符串对象——aa = new String('你好')。此时如果字符串的值不一样,那么就会默认指向另一个的地址。

aabb字符串引用地址

字符串的函数参数传递

字符串通过引用传递,并且在函数中将值修改,并不会修改函数外部的值。如下

image-20220616145746713

这是由于在c#中字符串是不可变(sealed)的,当我们将x重新赋值时,并不会更改原来的值,而是重新分配一块内存,创建一个新的对象。如下图我们分别查看value、x的引用地址,这两个指向的并不是同一个地址

290

字符串的比较

在C#中字符串作为引用类型除了Equals、ReferenceEquals还可以像值类型一样使用==做判断,原因是string类中已经帮我们实现==的判断方法,而且Equals也可以直接比较值而不是引用地址。我们可以看一下string的源码预览,如下

image-20220620111336542

image-20220620112552533

字符串的内存驻留

当我们创建两个字符串且值一致时,这两个字符串对象指向的是同一个内存地址,但是有个很强制的要求,那就是这两个字符串的创建方式要以赋值的方式创建

如上代码,我们先来看一下执行效果,为了方便查看我直接把相应字段的内存地址直接截图查看。

我们可以看到除了a、d这两个变量所指向的地址和对应aa、dd一致,而以其他方式得到的string对象指向的都是不同内存地址

image-20220620114301870

如何将函数参数传递的值类型或string类型变成引用传递呢

在c#中我们可以借助ref、out关键词,将函数参数传递的值类型或string类型变成引用传递

ref、out都能实现一样的效果,只是ref需要先定义再使用,而out不需要但一定要在被调用函数内对其进行赋值操作,记住这一点就行.

接下来我们来对比一下使用ref、out和不使用的区别,ref、out真的是非常实用的语法糖

image-20220620155958723

如何避免引用传递呢

如现在有两个对象,我想把对象A拷贝到B,但我不希望改对象B的时候影响到对象A

重新创建对象

image-20220620161929483

序列化与反序列化

先将对象A序列化成json串,再将json串反序列化成对象B。不过当对象A很大,有可能会超内存,速度也会受到影响

image-20220620163142498

深拷贝

浅拷贝:修改复制之后的对象,如果是值类型不会改变原对象,但如果是引用类型,原对象也会跟着改变,=赋值就是浅拷贝

深拷贝:新旧对象不是指向的不是同一个引用地址,修改新对象不会改变就对象

改一下class A,实现ICloneable接口

image-20220620165205726

其他

当然如何避免引用传递,其实有很多方法,如反射、Auto MapperAdapt Mapper

集合的对象引用

如两个集合之间如何避免引用传递呢,最简单直接用Linq的Select方法

image-20220620172635545

集合元素的对象引用

集合元素如果是引用类型,仅仅用Linq的Select方法是无法避免集合元素的引用传递,这一点需要注意一下。如下:

image-20220621103027183

那应该怎么处理呢?参考【如何避免引用传递呢】循环一个个的处理里面的元素,或者直接用Auto MapperAdapt Mapper

image-20220621103321789