CSharp中构造函数、析构函数和IDisposable使用细节探究

1. 先来一个简单的Demo

1.1. 定义一个类

public class ParentClass
{
    public ParentClass()
    {
        Console.WriteLine("ParentClass构造函数");
    }

    public void DoSomeThing()
    {
        Console.WriteLine("ParentClass做点什么DoSomeThing()");
    }

    ~ParentClass()
    {
        Console.WriteLine("ParentClass析构函数");
    }
}

1.2. Main函数调用

internal class Program
{
    static void Main(string[] args)
    {
        ReadInfo();
        Console.ReadKey();
    }

    private static void ReadInfo()
    {
        Console.WriteLine("ReadInfo开始调用............");
        ParentClass parentClass = new ParentClass();
        parentClass.DoSomeThing();
        Console.WriteLine("ReadInfo结束调用............");
    }
}

1.3. 查看一下输出

ReadInfo开始调用............
ParentClass构造函数
ParentClass做点什么DoSomeThing()
ReadInfo结束调用............

2. 看一下概念

2.1. 构造函数

在 C# 中,构造函数就是与类(或结构体)具有相同名称的成员函数,它在类中的地位比较特殊,

在 C# 中,构造函数是一种特殊类型的方法,用于初始化类的新实例。它的主要任务是为对象的成员变量赋初值、执行一些初始化操作或者为对象分配必要的资源。

构造函数的名称与类名相同,没有返回类型(甚至不能使用 void),并且不能被显式地调用。它在创建类的实例时由 .NET 运行时自动调用。

构造函数有两种主要类型:

  1. 默认构造函数:如果你没有显式定义构造函数,C# 编译器将自动为类生成一个默认的无参构造函数。默认构造函数没有任何参数,通常用于初始化对象的成员变量或执行其他初始化操作。例如:
class MyClass
{
    // 默认构造函数,无参构造函数
    public MyClass()
    {
        // 初始化操作
    }
}
  1. 自定义构造函数:你可以在类中定义自己的构造函数,可以有参数或没有参数,根据需求进行初始化。自定义构造函数可以根据提供的参数执行不同的初始化逻辑。例如:
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    // 自定义构造函数,有参构造函数
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

当你创建一个类的新实例时,会自动调用合适的构造函数来完成对象的初始化。例如:

Person person1 = new Person("Alice", 30); // 使用自定义构造函数
Person person2 = new Person(); // 使用默认构造函数

需要注意的是,如果你自定义了构造函数,则默认构造函数不再自动生成。如果你希望保留默认构造函数,同时定义自己的构造函数,需要显式地提供默认构造函数。例如:

class MyClass
{
    public MyClass()
    {
        // 默认构造函数
    }

    public MyClass(int value)
    {
        // 自定义构造函数
    }
}

构造函数在类的实例化过程中扮演着非常重要的角色,确保对象在创建时处于正确的状态。

2.2. 析构函数

在 C# 中,析构函数又称为 “Finalize” 方法。它是一种特殊类型的方法,用于在对象被垃圾回收之前执行一些清理工作。在 C# 中,程序员通常不需要手动编写析构函数,因为 .NET 的垃圾回收机制会自动处理内存管理和资源清理。

在需要手动管理非托管资源(例如文件、数据库连接、网络资源等)的情况下,可以使用析构函数来确保这些资源在对象被回收时得到释放。但是,现在推荐使用更现代的手段如实现 IDisposable 接口,使用 using 语句块,或使用 Dispose 方法来进行资源清理。这些方法提供了更明确和可控的资源管理方式。

如果你还是想了解如何编写析构函数,下面是一个示例:

using System;

class MyClass
{
    // 析构函数
    ~MyClass()
    {
        // 执行资源清理的代码
        // 例如关闭文件、释放非托管资源等
        Console.WriteLine("Destructor is called. Cleaning up resources.");
    }
}

class Program
{
    static void Main()
    {
        // 创建一个对象
        MyClass obj = new MyClass();

        // 程序运行到此结束时,对象可能还未被回收,所以析构函数可能会在程序结束之后被调用。
    }
}

值得注意的是,析构函数的调用时间是由垃圾回收器决定的,所以无法确切控制它何时被调用。这也是为什么推荐使用 IDisposable 接口和 Dispose 方法,以便在资源不再需要时立即进行释放和清理。

3. 回到案例

回到我们一开始的案例中,我们在ParentClass类中编写了构造函数ParentClass()和析构函数~ParentClass(),但是在最后的输出中,我们看到只有构造函数被执行,但是析构函数并没有被执行。

~ParentClass()
{
    Console.WriteLine("ParentClass析构函数");
}

到此出,问题来了,不是说析构函数程序员通常不需要手动编写析构函数,因为 .NET 的垃圾回收机制会自动处理内存管理和资源清理。,但是为什么析构函数并没有被执行呢?这是以为只有在触发垃圾回收时,才会调用析构函数,基于此我们来改一下Main函数,使其主动调用一下GC。

3.1. 主动GC

  • 代码
internal class Program
{
    static void Main(string[] args)
    {
        ReadInfo();
        Console.WriteLine("调用GC............");
        GC.Collect();
        Console.ReadKey();
    }

    private static void ReadInfo()
    {
        Console.WriteLine("ReadInfo开始调用............");
        ParentClass parentClass = new ParentClass();
        parentClass.DoSomeThing();
        Console.WriteLine("ReadInfo结束调用............");
    }
}

  • 代码输出
ReadInfo开始调用............
ParentClass构造函数
ParentClass做点什么DoSomeThing()
ReadInfo结束调用............
调用GC............
ParentClass析构函数

这个时候我们发现GC被调用,只是这个时候GC是主动调用的

3.3. 写一个GC,由CLR自动调用的案例

static void Main(string[] args)
{
    ReadInfo();
    //Console.WriteLine("调用GC............");
    //GC.Collect();
    List list = new List();
    for (int i = 0; i < 100000000; i++)
    {
        list.Add($"内存调用{ i }");
    }
    //ReadInfo();
    Console.ReadKey();
}

private static void ReadInfo()
{
    Console.WriteLine("ReadInfo开始调用............");
    ParentClass parentClass = new ParentClass();
    parentClass.DoSomeThing();
    //List list = new List();
    //for (int i = 0; i < 100000000; i++)
    //{
    //    list.Add($"内存调用{ i }");
    //}
    Console.WriteLine("ReadInfo结束调用............");
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FTdSsa1g-1690855974476)(GC发生调用.png)]

以下这个则不会触发调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckCtIIR0-1690855974478)(GC没有调用.png)]

static void Main(string[] args)
{
    ReadInfo();
    //Console.WriteLine("调用GC............");
    //GC.Collect();
    //ReadInfo();
    Console.ReadKey();
}

private static void ReadInfo()
{
    Console.WriteLine("ReadInfo开始调用............");
    ParentClass parentClass = new ParentClass();
    parentClass.DoSomeThing();
    parentClass = null;
    
    List list = new List();
    for (int i = 0; i < 100000000; i++)
    {
        list.Add($"内存调用{ i }");
    }
    Console.WriteLine("ReadInfo结束调用............");
}

3.3. 同一个变量,我们new两次,是否触发析构函数

internal class Program
{
    static void Main(string[] args)
    {
        ReadInfo();
        
        Console.ReadKey();
    }

    private static void ReadInfo()
    {
        Console.WriteLine("ReadInfo开始调用............");
        ParentClass parentClass = new ParentClass();
        parentClass.DoSomeThing();
        
        parentClass = new ParentClass();
        parentClass.DoSomeThing();
        Console.WriteLine("ReadInfo结束调用............");
    }
}
ReadInfo开始调用............
ParentClass构造函数
ParentClass做点什么DoSomeThing()
ParentClass构造函数
ParentClass做点什么DoSomeThing()
ReadInfo结束调用............

4. 稍微提升一点难度

4.1. 如果现在有一个子类ChildClass继承了ParentClass

代码如下:

public class ParentClass
{
    public ParentClass()
    {
        Console.WriteLine("ParentClass构造函数");
    }

    public void DoSomeThing()
    {
        Console.WriteLine("ParentClass做点什么DoSomeThing()");
    }

    ~ParentClass()
    {
        Console.WriteLine("ParentClass析构函数");
    }
}

public class ChildClass: ParentClass
{
    public ChildClass()
    {
        Console.WriteLine("ChildClass构造函数");
    }

    ~ChildClass()
    {
        Console.WriteLine("ChildClass析构函数");
    }
}
internal class Program
{
    static void Main(string[] args)
    {
        ReadInfo();
        Console.ReadKey();
    }

    private static void ReadInfo()
    {
        Console.WriteLine("ReadInfo开始调用............");
        ChildClass childClass=new ChildClass();
        childClass.DoSomeThing();
        Console.WriteLine("ReadInfo结束调用............");
    }
}
  1. 子类和父类的构造函数的调用逻辑是?
  • A: 先调用父类,再调用子类
  • B: 只调用子类
  • C: 只调用父类
  • D: 先调用子类,再调用父类

揭晓答案,答案是A:先调用父类,再调用子类

ReadInfo开始调用............
ParentClass构造函数
ChildClass构造函数
ParentClass做点什么DoSomeThing()
ReadInfo结束调用............
  1. 同理可得,当触发GC.Collect();时,会先调用子类再调用父类。
static void Main(string[] args)
{
    ReadInfo();
    Console.WriteLine("调用GC............");
    GC.Collect();
    Console.ReadKey();
}
ReadInfo开始调用............
ParentClass构造函数
ChildClass构造函数
ParentClass做点什么DoSomeThing()
ReadInfo结束调用............
调用GC............
ChildClass析构函数
ParentClass析构函数
  1. 为了进一步验证,我们在ParentClass父类上再增加一层类。
  • 完整代码如下:
public class BaseClass
{
    public BaseClass()
    {
        Console.WriteLine("BaseClass构造函数");
    }

    public void DoSomeThing()
    {
        Console.WriteLine("BaseClass做点什么DoSomeThing()");
    }

    ~BaseClass()
    {
        Console.WriteLine("BaseClass析构函数");
    }
}

public class ParentClass: BaseClass
{
    public ParentClass()
    {
        Console.WriteLine("ParentClass构造函数");
    }

    public new void DoSomeThing()
    {
        Console.WriteLine("ParentClass做点什么DoSomeThing()");
    }
    
    ~ParentClass()
    {
        Console.WriteLine("ParentClass析构函数");
    }
}

public class ChildClass: ParentClass
{
    public ChildClass()
    {
        Console.WriteLine("ChildClass构造函数");
    }

    ~ChildClass()
    {
        Console.WriteLine("ChildClass析构函数");
    }
}





internal class Program
{
    static void Main(string[] args)
    {
        ReadInfo();
        Console.WriteLine("调用GC............");
        GC.Collect();
        Console.ReadKey();
    }
    private static void ReadInfo()
    {
        Console.WriteLine("ReadInfo开始调用............");
        ChildClass childClass=new ChildClass();
        childClass.DoSomeThing();
        Console.WriteLine("ReadInfo结束调用............");
    }
}
  • 得到的输出如下:
ReadInfo开始调用............
BaseClass构造函数
ParentClass构造函数
ChildClass构造函数
ParentClass做点什么DoSomeThing()
ReadInfo结束调用............
调用GC............
ChildClass析构函数
ParentClass析构函数
BaseClass析构函数

4.2. 一般在使用时,不推荐使用析构函数,而使用IDispose

  • 完整代码如下:
internal class Program
{
    static void Main(string[] args)
    {
        UsingReadInfo();
        Console.ReadKey();
    }

    private static void UsingReadInfo()
    {
        Console.WriteLine("UsingReadInfo开始调用............");
        using (ChildClass childClass = new ChildClass())
        {
            childClass.DoSomeThing();
        }
        
        Console.WriteLine("UsingReadInfo结束调用............");
    }

    private static void ReadInfo()
    {
        Console.WriteLine("ReadInfo开始调用............");
        ChildClass childClass=new ChildClass();
        childClass.DoSomeThing();
        Console.WriteLine("ReadInfo结束调用............");
    }
}

public class BaseClass:IDisposable
{
    public BaseClass()
    {
        Console.WriteLine("BaseClass构造函数");
    }

    public void DoSomeThing()
    {
        Console.WriteLine("BaseClass做点什么DoSomeThing()");
    }

    ~BaseClass()
    {
        Console.WriteLine("BaseClass析构函数");
    }

    public void Dispose()
    {
        Console.WriteLine("BaseClass调用Dispose");
    }
}

public class ParentClass: BaseClass, IDisposable
{
    public ParentClass()
    {
        Console.WriteLine("ParentClass构造函数");
    }

    public new void DoSomeThing()
    {
        Console.WriteLine("ParentClass做点什么DoSomeThing()");
    }

    ~ParentClass()
    {
        Console.WriteLine("ParentClass析构函数");
    }

    public void Dispose()
    {
        Console.WriteLine("ParentClass调用Dispose");
    }
}

public class ChildClass: ParentClass, IDisposable
{
    public ChildClass()
    {
        Console.WriteLine("ChildClass构造函数");
    }

    ~ChildClass()
    {
        Console.WriteLine("ChildClass析构函数");
    }

    public void Dispose()
    {
        Console.WriteLine("ChildClass调用Dispose");
    }
}
  • 输出结果
UsingReadInfo开始调用............
BaseClass构造函数
ParentClass构造函数
ChildClass构造函数
ParentClass做点什么DoSomeThing()
ChildClass调用Dispose
UsingReadInfo结束调用............
  • 如果将整个类中的Dispose改为虚函数则可以:
public class BaseClass:IDisposable
{
    public virtual void Dispose()
    {
        Console.WriteLine("BaseClass调用Dispose");
    }
}

public class ParentClass: BaseClass, IDisposable
{
    public override void Dispose()
    {
        base.Dispose();
        Console.WriteLine("ParentClass调用Dispose");
    }
}

public class ChildClass: ParentClass, IDisposable
{
    public override void Dispose()
    {
        base.Dispose();
        Console.WriteLine("ChildClass调用Dispose");
    }
}
UsingReadInfo开始调用............
BaseClass构造函数
ParentClass构造函数
ChildClass构造函数
ParentClass做点什么DoSomeThing()
BaseClass调用Dispose
ParentClass调用Dispose
ChildClass调用Dispose
UsingReadInfo结束调用............

5. 结语

好久没有一气呵成的写C#代码了 好怀恋!!!

你可能感兴趣的:(#,C#,c#)