CS3L5——构造函数、析构函数和垃圾回收

本章代码关键字

1
2
3
this            //this代表类自己,可以显式的指定指明自己的成员
~类名() //析构函数,对象被销毁释放时调用,不常用
GC.Collect(); //主动触发GC的方法,但是不会频繁使用,因为性能消耗较大,可能会卡顿

构造函数

在实例化对象时,会调用的用于初始化的函数,主要就是用来初始化成员变量,如果不写则默认存在一个无参构造函数

构造函数的声明和使用

没有返回值,函数名与类名相同,没有特殊需求时一般都是 public​ 的

类中允许自己申明无参构造函数的,但是结构体不允许

1
2
3
4
5
6
7
8
9
10
11
class Person
{
public string name;
public int age;

public Person()
{
name = "liuqi";
age = 18;
}
}

new​ 对象时,就会调用构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person
{
public string name;
public int age;

public Person()
{
Console.WriteLine("无参构造函数被调用了");
name = "liuqi";
age = 18;
}
}

internal class Program
{
static void Main(string[] args)
{
Person p = new Person();
}
}

输出:

1
无参构造函数被调用了

构造函数可以被重载,方便初始化函数,可以通过 this​ 来调用函数的对象自己

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person
{
public string name;
public int age;

public Person()
{
name = "liuqi";
age = 18;
}

public Person(string name, int age):this()
{
this.name = name;
this.age = age;
}
}

注意!如果一个类不实现无参构造函数而实现了有参构造函数,会失去默认的无参构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person
{
public string name;
public int age;

public Person()
{
name = "liuqi";
age = 18;
}

public Person(string name, int age):this()
{
this.name = name;
this.age = age;
}
}

internal class Program
{
static void Main(string[] args)
{
// Person p = new Person();
Person p = new Person("MrTang", 18)
}
}

构造函数的重用

可以通过 this​,重用构造函数代码 会先使用 this()​ 的构造函数

声明语法为:访问修饰符 构造函数名(参数列表) : this(参数1,参数2...)

如果重用 有参数的构造函数,则需要传入对应的参数,
可以使用被外部调用的构造函数的形参,也可以用表达式或者常量,只要参数类型正确即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person
{
public string name;
public int age;

public Person(string name)
{
this.name = name;
}

// 这里被重用的函数是 Person(string name)
public Person(string name, int age) : this(name + "这里也可以写表达式,只要参数类型是正确的!")
{
WriteLine("双参数构造函数被调用了");
this.age = age;
}

// 这里被重用的函数是 Person(string name, int age)
public Person() : this("MrTang", 18)
{
WriteLine("无参构造函数被调用了");
}
}

internal class Program
{
static void Main(string[] args)
{
Person p = new Person();
}
}

输出:

1
2
双参数构造函数被调用了
无参构造函数被调用了

析构函数

当引用类型的堆内存被回收时,会调用该函数,对于需要手动管理内存的语言(如C++),需要在析构函数中做一些内存回收处理
但由于 C# 中由自动垃圾回收机制 GC,所以 C# 使用析构函数较少,一般用在某一个对象被垃圾回收的时候,做一些特殊处理

注意:析构函数在Unity的开发中几乎不使用

析构函数声明语法:~类名()

1
2
3
4
5
6
7
class Person
{
~Person()
{
WriteLine("垃圾被回收了");
}
}

C#的垃圾回收机制

垃圾回收,英文简写GC (Garbage Collecter),垃圾回收的过程是遍历堆 (heap) 上动态分配的所有对象
通过识别他们是否被引用来确定那些对象是垃圾 哪些对象仍然要被引用
所谓垃圾就是没有被任何变量 对象引用的内容,垃圾就需要被回收释放

1
2
3
4
5
6
7
8
internal class Program
{
static void Main(string[] args)
{
Person p = new Person();
p = null;
}
}

上面的代码中,p = null​ 后,之前 new​ 出来的 Person​ 对象就成为了垃圾,因为没有任何变量再引用它存在于堆上的数据了

垃圾回收有很多种方法,如:

  • 引用计数(Reference Counting)
  • 标记清除(Mark Sweep)
  • 标记整理(Mark Compact)
  • 复制集合(Copy Collection)

注意:GC 只负责堆(heap)内存的回收,引用类型都是存放在堆(heap)中的,所以它的分配和释放都通过垃圾回收机制来管理
栈(Stack)上的内存是由系统自动管理的,值类型在栈(Stack)中分配内存的,它们有自己的生命周期,不用对它们进行管理,会自动分配和释放

C#中内存回收机制的大概原理

C#将内存分为三代内存:0 代内存、1 代内存、2 代内存

其中, 是垃圾回收机制使用的一种算法(分代算法)
新分配的对象都会被配置在第 0 代内存里,每次分配都可能会进行垃圾回收以释放内存
当 0 代内存满时而又要放入新数据时,就会就会触发垃圾回收

在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步:

  1. 标记对象 从根(静态字段、方法参数)开始检查引用对象,

    标记后为可达对象,未标记为不可达对象,不可达对象就被认为是垃圾

    例如一个实例化后没有变量再引用的对象,就是垃圾

  2. 搬迁对象,并压缩堆(这时会挂起执行托管代码进程),释放未被标记的对象,搬迁可达对象,修改引用地址

    例如 0 代内存里可达对象就会被搬迁到 1 代内存,不可达的对象被当作垃圾处理回收,搬迁完成后需要将对应地址进行改变为连续的数值
    若 1 代内存满后也会触发垃圾回收,同时也会触发 0 代垃圾回收(不管 0 代内存满没满)
    而 1 代可达对象也搬迁到 2 代内存,其他步骤类似于上个步骤

    若 2 代内存满后也会触发垃圾回收,同时也会触发 0 代和 1 代的垃圾回收,步骤同上

    0 代和 1 代内存的速度会快于 2 代内存速度,因为在 2 代内存中可能存在的垃圾更多

85000字节(83kb)以上的对象就是大对象,大对象总被认为是 2 代内存
目的是减少性能消耗,提高性能,不会对大对象进行搬迁压缩

主动触发GC的方法

GC是可以手动触发的,而不需要等待内存满后触发

1
2
3
4
5
6
7
internal class Program
{
static void Main(string[] args)
{
GC.Collect(); //主动触发GC的方法,但是不会频繁使用,因为性能消耗较大,可能会卡顿
}
}

建议在类似于读取等需要玩家等待的时段进行主动GC,例如显示读取条时