值类型和引用类型
值类型
所有的值类型都直接或间接地继承自System.ValueType
,值类型的变量包含类型的实例,在赋值、传参、作为返回值的时候,都会直接拷贝其完整的字段 —— 即赋值完整的实例,所以对副本进行的修改不会对原始数据进行改动。
基本上,值类型的内存分配以及操作是直接在栈区实现
常见的值类型数据类型:
- 结构类型
- 内置数据类型(也称之为简单类型):
- 整型
- 浮点型
bool
char
- 值元组
struct
- 内置数据类型(也称之为简单类型):
- 枚举类型
enum
引用类型
引用类型存储的是数据的地址,然而实际数据存放在堆中,所以当一个引用类型被赋值给其他变量时,会拷贝一份引用(即生成两份别名),所以对副本的修改也会使得原始数据(被引用的对象)被修改。
常见的引用类型:
- 内置引用类型
dynamic
object
string
- 声明引用类型使用的关键字
class
interface
delegate
record
一点简单例子
C#
Person Tom = new Person("Tom");
var temp = Tom;
temp.Name = "Jack";
Console.WriteLine(Tom.Name);
假设有一个Person
类,其中封装了Name
成员。在上述代码中 ——
- 实例化了一个
Tom
对象,并且在堆区为它进行了内存的分配 - 创建了一个
temp
引用对象,指向Tom
在堆区的内存 - 直接修改
temp
,等价于直接修改Tom
在堆区的数据 - 所以最后输出
Tom.Name
为Jack
IMPORTANT
- 如果不是对引用对象进行内部成员的修改,是不会操作堆中数据的;
- 直接对一个引用对象赋值,相当于指向了另一个已实例化对象在托管堆中的数据
Method Modifiers
ref
, out
, in
其底层定义都是T&
,使用指针传递,所以对于大型的值类型(比如超大结构体)来说,使用这些来进行传参,可以提高效率(少了一次复制)
ref
ref
关键字有点类似在 C 中使用取地址符使得一个函数直接对实参进行操作 —— 在C#
中,即为把方法内部对参数的修改回传给调用者
C#
void Add5(ref int x) => x += 5;
int x = 0;
Add5(ref x);
Console.WriteLine(x);
使用ref
修饰参数,使得最后x
的值为5 ( 0 + 5 = 5 )
注意!——
- 声明与调用都要加
ref
关键字。 - 调用前变量必须已被初始化,否则编译报错。
- 方法内既能读取也能写回调用者的变量。
底层原理:
- IL 中的参数类型为
T&
(ByRef)。 - 传入的是变量的地址,JIT 在栈或寄存器上传递这个地址,方法直接操作该内存。
out
常用于Try-Prase
等模式,其和ref
有点类似,都是作为引用传递。但是必须在函数体内对out
修饰的参数进行赋值。
例子
C#
bool TryPrase(string s, out int x)
{
try
{
int.TryParse(s, out x);
return true;
}
catch
{
x = 0;
return false;
}
}
string s = "31";
if (TryPrase(s, out int x))
Console.WriteLine($"{s} convert to {x}");
in
in
关键字让参数按引用传递且只读(函数体内无法修改)