文章目录
- 一、值类型和引用类型汇总补充
- 1、值类型和引用类型汇总
- 2、值类型和引用类型的区别
- 3、简单的判断值类型和引用类型
- 二、变量的生命周期与性能优化
- 1、**栈和堆的区别**
- 2、**变量生命周期**
- 3、**垃圾回收(GC)机制**
- 4、**代码示例与优化**
- 4.1. 临时变量的生命周期与回收
- 4.2. 临时变量的性能问题:每次创建新变量
- 4.3. 性能优化:减少不必要的变量创建
- 4.4. 使用成员变量或者静态变量
- 5、其他注意事项
- 6、总结
- 三、值类型和引用类型组合使用
- 1、结构体中的值类型和引用类型
- 2、类中的值类型和引用类型
- 3、数组中的值类型和引用类型
- 4、结构体继承接口
- 4.1 示例
- 4.2装箱拆箱
- 4.3 性能考虑
- 5、简单记忆口诀
- 专栏推荐
- 完结
一、值类型和引用类型汇总补充
1、值类型和引用类型汇总
我们学了很多新的值类型和引用类型,这里列个表全部分类一下,方便大家查看对比
类型类别 | 类型 | 描述 |
---|---|---|
值类型 | byte | 无符号 8 位整数 (范围:0 到 255) |
ushort | 无符号 16 位整数 (范围:0 到 65,535) | |
uint | 无符号 32 位整数 (范围:0 到 4,294,967,295) | |
ulong | 无符号 64 位整数 (范围:0 到 18,446,744,073,709,551,615) | |
sbyte | 有符号 8 位整数 (范围:-128 到 127) | |
short | 有符号 16 位整数 (范围:-32,768 到 32,767) | |
int | 有符号 32 位整数 (范围:-2,147,483,648 到 2,147,483,647) | |
long | 有符号 64 位整数 (范围:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807) | |
float | 单精度浮点数 (32 位,范围大约为 ±1.5 x 10^−45 到 ±3.4 x 10^38) | |
double | 双精度浮点数 (64 位,范围大约为 ±5.0 × 10^−324 到 ±1.7 × 10^308) | |
decimal | 高精度十进制数(128 位,用于财务和其他需要高精度的应用) | |
char | 单一字符 (16 位 Unicode 字符) | |
bool | 布尔值 (true 或 false ) | |
enum | 枚举类型,定义一组命名常数值 | |
struct | 结构体类型,可以包含字段、方法、属性等,常用于表示值对象 | |
引用类型 | string | 字符串类型,表示文本,实际上是 char[] 的封装 |
array | 数组类型,可以是任何数据类型的集合 | |
class | 类类型,用于定义对象的蓝图 | |
interface | 接口类型,用于定义类和结构体的契约 | |
delegate | 委托类型,用于定义引用方法的类型 |
2、值类型和引用类型的区别
-
值类型:
- 存储:直接存储数据值。
- 分配方式:在栈(stack)上分配内存。
- 赋值行为:赋值时会复制数据,两个变量的修改不会相互影响。
-
引用类型:
- 存储:存储的是数据的引用(即指向内存中数据的地址)。
- 分配方式:在堆(heap)上分配内存。
- 赋值行为:赋值时会复制引用,两个变量指向同一内存位置,修改其中一个会影响另一个。
3、简单的判断值类型和引用类型
可以看到前面有这么多的数据类型,记不住怎么办?
我们可以在编辑器按F12
或者ctrl
+鼠标左键点击进去类型的内部查看信息
- 如果是
class
(类)就是引用类型 - 如果是
struct
(结构体)就是值类型
比如int就是值类型
string就是引用类型
二、变量的生命周期与性能优化
1、栈和堆的区别
C# 中的变量存储在栈(Stack)和堆(Heap)上,这取决于变量的类型:
- 值类型(Value Types):包括
int
、double
、struct
等。值类型的变量直接存储数据本身,它们通常被分配在栈上。当超出作用域时,栈上的值类型会自动被回收。 - 引用类型(Reference Types):包括
class
、string
、array
、delegate
等。引用类型的变量存储的是指向实际数据(在堆上的对象)的引用(地址)。当引用类型变量超出作用域时,栈上的引用会被回收,但堆上的对象不会立即被销毁,而是会等待垃圾回收(GC)机制回收。
2、变量生命周期
- 栈上值类型变量:当栈上的值类型变量超出作用域时,它会被立即销毁。
- 堆上引用类型变量:引用类型的变量在栈上存储的是对象的地址,当栈上的引用类型变量超出作用域时,指向堆上对象的引用会被清除,但堆上的对象不会立刻被销毁,只有在垃圾回收(GC)时,它们才会被回收。
3、垃圾回收(GC)机制
- 垃圾回收:C# 使用垃圾回收机制来自动管理内存,特别是对堆上的对象进行内存管理。当对象没有任何引用时,它会被垃圾回收器标记为垃圾并释放其占用的内存空间。
- 值类型的回收:栈上存储的值类型变量会在超出作用域后自动销毁,不需要显式回收。
- 引用类型的回收:栈上存储的引用会在超出作用域时被回收,但堆上的对象仍然需要垃圾回收器来管理。垃圾回收器会在堆上标记不再使用的对象并释放内存。
4、代码示例与优化
4.1. 临时变量的生命周期与回收
C# 中,当一个临时变量(如局部变量)超出其作用域时,它会被销毁,特别是在语句块(如函数、条件语句、循环语句等)执行结束时,栈上的局部变量会自动回收。
示例 1:
void Example()
{int i = 5; // 局部变量 i
} // 变量 i 超出作用域后,会被销毁
- 当
Example
方法执行完后,i
超出了作用域,栈上分配给i
的内存会被回收。
下面这里会报错的原因就是,栈是先进后出原则,{}
包裹语句块,执行完成,i2
就被回收了,所以外面打印不到i2
内容
4.2. 临时变量的性能问题:每次创建新变量
在某些情况下,频繁创建临时变量可能会带来性能上的开销,尤其是在循环中。例如:
示例 2(性能问题):
while (true)
{int i = 1; // 每次循环都会创建新的 i
}
- 每次进入循环时,
int i
都会在栈上分配内存,循环执行多次时,会频繁地分配和销毁i
,这会带来一定的性能开销。
4.3. 性能优化:减少不必要的变量创建
为了避免每次循环都重新创建新的变量,可以将变量声明移到循环外部。这样,变量只会被创建一次,循环内部只修改变量的值,而不需要反复创建。
优化方法 1:
int i = 1; // 移动到循环外部,减少变量创建次数
while (true)
{i = 1; // 只是赋值,避免每次循环都重新创建变量
}
- 这样,
i
只会在循环外部创建一次,而每次循环只需要修改它的值,不会进行重复的内存分配和回收。
4.4. 使用成员变量或者静态变量
如果变量在多个方法或类实例之间共享,你可以考虑将变量声明为 成员变量 或 静态变量。这可以避免频繁创建临时变量,提高性能。
优化方法 2(成员变量):
class Test
{int i; // 成员变量public void TestMethod(){while (true){i = 1; // 只修改成员变量的值}}
}
优化方法 3(静态成员变量):
class Test
{static int i; // 静态成员变量public void TestMethod(){while (true){i = 1; // 只修改静态变量的值}}
}
- 成员变量:如果
i
只是与某个对象的状态相关,可以将i
声明为成员变量。 - 静态变量:如果
i
在所有对象之间共享,可以将其声明为静态变量。注意,静态变量是类级别的,而非实例级别的。
5、其他注意事项
- 静态变量与垃圾回收:静态变量的生命周期与应用程序的生命周期相同,在整个程序运行期间,它们会一直存在,直到程序结束时才会被垃圾回收。
- 避免不必要的内存分配:在高频率执行的循环中,尽量避免在每次循环中创建新变量,尤其是值类型。可以考虑将变量移到循环外部,或者使用静态变量和成员变量。
6、总结
- 值类型:存储在栈上,超出作用域后会自动回收。
- 引用类型:存储在堆上,栈上的引用超出作用域时会被回收,但堆上的对象直到垃圾回收器执行时才会被回收。
- 性能优化:减少不必要的内存分配和销毁,避免在循环中频繁创建局部变量。可以通过将变量移到循环外部或使用成员变量、静态变量来优化性能。
三、值类型和引用类型组合使用
1、结构体中的值类型和引用类型
我们知道,在C#中,结构体 (struct
) 是值类型存储在栈上,而引用类型(如 string
)存储在堆上。那么结构体中的字段算是值类型还是引用类型呢?尤其是如何区分这两种类型的内存分配方式。
-
结构体 (
struct
) 本身是值类型,因此当你复制结构体时,结构体内的所有字段(无论是值类型还是引用类型)都会被复制,而不是对象的实际内容。 -
结构体中的值类型字段:存储的是实际的值,并且这个值直接存储在结构体的实例内存区域中。如果结构体作为方法参数传递,它会被 复制,因此对结构体字段的修改不会影响原始结构体。
-
结构体中的引用类型字段:这些字段存储的是对堆中对象的引用(即地址)。即使结构体是值类型,结构体内部的引用类型字段仍然会引用堆中的对象。当结构体复制时,引用类型字段的引用会被复制,因此多个结构体实例可以引用同一个堆对象。
2、类中的值类型和引用类型
-
类(
class
)是引用类型,意味着它的实例会在堆上分配内存,变量存储的是对对象的引用(地址),而不是对象的实际内容。 -
类中的值类型字段:在类中的值类型字段(如 int、float 等)存储的是实际的值,这些值存储在对象的内存区域。因为类本身是引用类型,所以这些值会随着类的对象一起存储在堆上。
-
类中的引用类型字段:这些字段存储的是对堆中对象的引用。当引用类型字段被赋值时,实际上是将引用的地址传递给另一个变量,因此修改一个引用类型字段会影响所有引用该对象的变量。
3、数组中的值类型和引用类型
- 数组本身是引用类型:这意味着当你创建一个数组时,实际上是创建了一个指向堆(
heap
)中数据的引用。栈(stack
)上只保存了这个引用,而实际的数据存储在堆中。 - 类中的值类型字段:如果数组是值类型的数组(如
int[]
,double[]
),那么数组中的每个元素都是独立的值类型实例,它们直接存储在数组所在的堆内存中。 - 类中的引用类型字段:如果数组是引用类型的数组(如
string[]
,object[]
),那么数组中的每个元素都是引用,指向堆中某个对象的实际位置。
4、结构体继承接口
在C#中,结构体(struct)可以实现接口,尽管结构体是值类型而接口是引用类型。
4.1 示例
比如我们新增一个结构体继承接口
interface ITest { int Value {get;set;}
}struct TestStruct : ITest
{private int value;public int Value { get => value; set => this.value = value; }
}
根据里氏替换原则,父类可以装子类。所以我们可以用接口容器(父类)装载结构体(子类)。
TestStruct ts1 = new TestStruct();
ts1.Value = 1;
Console.WriteLine(ts1.Value);TestStruct ts2 = ts1;
ts2.Value = 2;
Console.WriteLine(ts1.Value);
Console.WriteLine(ts2.Value);ITest it1 = ts1;//装箱
ITest it2 = it1;
it2.Value = 100;
Console.WriteLine(it1.Value);
Console.WriteLine(it2.Value);
结果
根据结果我们可以发现,后面it1
it2
的值打印都是100,相当于我们强行把值类型变成了引用类型。
4.2装箱拆箱
用接口容器装载结构体存在装箱拆箱
。当你将一个实现了接口的结构体赋给接口类型的变量时,会发生装箱操作,即将值类型转换为引用类型。相反的过程称为拆箱。
TestStruct ts1 = new TestStruct();
ITest it1 = ts1;//装箱
TestStruct ts3 = (TestStruct)it1;//拆箱
4.3 性能考虑
频繁的装箱和拆箱会对性能产生负面影响,尤其是在循环或大量数据处理的情况下。为了避免这种性能问题,你可以考虑以下策略:
- 使用类而不是结构体:如果需要频繁地将对象存储在接口容器中,考虑使用类而非结构体,以避免装箱开销。
- 减少装箱次数:尽量减少不必要的装箱操作,例如通过缓存已经装箱的对象。
- 泛型:使用泛型可以避免装箱。例如,List 可以持有值类型而不发生装箱。
5、简单记忆口诀
值类型跟大哥走,引用类型很自我
专栏推荐
地址 |
---|
【从零开始入门unity游戏开发之——C#篇】 |
【从零开始入门unity游戏开发之——unity篇】 |
【制作100个Unity游戏】 |
【推荐100个unity插件】 |
【实现100个unity特效】 |
【unity框架开发】 |
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~