全部版块 我的主页
论坛 数据科学与人工智能 IT基础
77 0
2025-11-17

让我们先看一个简单的例子(Net 8)。创建一个实现了Dispose方法的基本对象Defer。接着,在控制台中我们执行以下代码。

// 定义Defer类型
ref struct Defer(Action action) { public void Dispose() => action?.Invoke();}
// 主入口
static void Main(string[] args)
{
    using var df = new Defer(() => Console.WriteLine("Run"));
    Console.WriteLine("Hello, World!");
}
// 控制台输出:
// Hello, World!
// Run
    

可以看出,“Hello, World”和“Run”的显示次序颠倒了。这个Defer结构体能够大致模仿Golang中Defer关键字带来的延迟执行效果。using本质上是一种语法简化,帮助我们更准确地掌握Dispose()方法的调用时机。

对于ref struct,上述代码相当于:

{
    Defer df = new Defer(() => Console.WriteLine("Run"));
    try
    {
        Console.WriteLine("Hello, World!");
    }
    finally
    {
        df.Dispose();
    }
}
    

在此处,try块内需要保护的代码是在df对象生命周期内的代码。

对于异步DisposeAsync(),using等价于:

{
    ResourceType resource = ?expression?;
    try
    {
        ?statement?;
    }
    finally
    {
        IAsyncDisposable d = (IAsyncDisposable)resource;
        if (d != null)
        {
            await d.DisposeAsync();
        }
    }
}
    

为什么设计Dispose?

C#利用垃圾回收机制自动处理内存管理,这意味着开发者无需手动干预内存的分配和释放,有效减少了内存泄漏和无效指针的问题。不过,垃圾回收器仅处理托管内存的回收,对于非托管资源则无能为力。此外,垃圾回收器的执行时间是不可预测的,可能会在资源不再需要很长时间后才启动。因此,需要一种机制来即时释放非托管资源,这也是引入Dispose的原因之一。

在C#编程中,我们频繁使用多种资源,例如文件、数据库连接等。使用这些资源后应立即释放,以避免占用系统资源,影响程序效率。Dispose方法的作用正是释放这些资源。当不再需要某对象时,主动或被动调用Dispose方法,可以确保资源被及时归还给系统,防止资源泄露。

简而言之,Dispose是一种约定俗成的“用完即清理”机制,可以便捷地与using关键字结合使用。接下来,我们再看几个实例。

案例1 通过using在特定代码执行完毕后触发Dispose:

// 主入口
using (Defer df1 = new(() => Console.WriteLine("Run")))
Console.WriteLine("Hello, World!1");  // 或者 使用 { ... } 包围代码
Console.WriteLine("Hello, World!2");
// 控制台输出:
// Hello, World!1
// Run
// Hello, World!2
    

案例2 通过using多层触发,最终按照变量声明的相反顺序执行(类似于栈的弹出顺序):

// 主入口
using Defer df1 = new(() => Console.WriteLine("Run1")),
df2 = new(() => Console.WriteLine("Run2")),
df3 = new(() => Console.WriteLine("Run3"));
Console.WriteLine("Hello, World!");
// 控制台输出:
// Hello, World!
// Run3
// Run2
// Run1
    

案例3 异步IAsyncDisposable,调用await using:

public class A_Async:IAsyncDisposable {async ValueTask IAsyncDisposable.DisposeAsync() => await Task.CompletedTask;}
static async void Main(string[] args)
{
    await using A_Async a = new();
}
    

为什么使用释放模式(Dispose Pattern)?

在C#实现接口时,Visual Studio常建议通过释放模式来完成。那么,释放模式究竟是什么?

释放机制是Dispose模式与析构函数(finalizer)的联合应用,旨在保证资源得以恰当释放,无论通过显式调用Dispose方法,还是在对象被垃圾回收器(GC)回收时触发析构函数。该模式称作“Dispose模式”,是处理托管和非托管资源的一种最优实践。

无标题

例如,我们有一个包含一些非托管资源和一些托管资源的对象。示例代码如下:

class SampleObject:IDisposable
{
private ManagedObject _mo;  // 托管
private UnmanagedObject _umo; // 非托管
public void Dispose()   // 资源释放
{
_mo.Dispose(); // 释放托管
_umo.Dispose(); // 释放非托管
}
}

3.1 防止重复调用Dispose()

通常情况下我们的代码没有问题。但如果ManagedObject和UnmanagedObject并非我们编写,因此需考虑重复调用Dispose可能导致的问题。为此,我们在SampleObject内部添加一个标识符以防止重复释放,此时代码变为:

class SampleObject:IDisposable
{
private ManagedObject _mo;
private UnmanagedObject _umo;
private bool disposedValue = false; // 添加: 标识符变量
public void Dispose()
{
if (!disposedValue) // 添加: 检查标识符值,避免重复调用
{
_mo.Dispose();
_umo.Dispose();
disposedValue = true;
}
}
}

3.2 避免遗漏调用Dispose()

对于含有非托管资源的对象,若遗忘调用Dispose(),轻微的情况会导致内存泄漏,严重时可能酿成灾难。为确保对象能调用Dispose(),我们考虑添加析构函数。希望在程序被GC回收时自动释放资源,示例代码如下:

class SampleObject:IDisposable
{
private ManagedObject _mo;
private UnmanagedObject _umo;
private bool disposedValue = false;
public void Dispose()
{
DisposeFinal(); // 执行资源释放
// 添加: 若手动调用了Dispose(),通知终结器不再执行析构函数
// 即不再重复调用DisposeFinal()方法
GC.SuppressFinalize(this);
}
public void DisposeFinal()  // 重命名,从Dispose方法分离
{
if (!disposedValue)
{
_mo.Dispose();
_umo.Dispose();
disposedValue = true;
}
}
// 添加: 析构函数,在遗忘调用Dispose()时由终结器执行Dispose()
~SampleObject()
{
DisposeFinal();
}
}

3.3 托管资源的提前回收

即使3.2中的对象忘记调用Dispose(),此时触发析构函数,依然可以执行Dispose()。尽管看起来一切都很理想。但这里仍存在潜在的重复调用Dispose()风险。因为终结器的执行顺序不确定,当SampleObject对象被终结器触发析构函数时,其他对象(如_mo)也可能触发了析构函数。这导致在SampleObject执行Dispose时,_mo的Dispose()方法可能被调用两次(一次自身,一次外部调用),从而引发意外结果。

我们可以看一个实例。

3.3.1 定义一个有缺陷的托管资源类

此类未对重复释放进行阻止。

// 我们定义一个有缺陷的托管资源类
class ManagedData:IDisposable
{
// 模拟托管资源,大数组尽量让GC多保留一会儿,增加测试结果多样性
private MemoryStream data= new MemoryStream(new byte[100_000000]);
private bool _finalized = false;
int id;
public ManagedData(int id)  // 记录当前对象id
{
this.id = id;
}
~ManagedData()
{
_finalized = true;  // 由析构函数释放
Console.WriteLine($"{id}:ManagedData 已终结.");
}
public void Dispose()
{
if (_finalized)

throw new ObjectDisposedException($"{id}: 无法接触已处置的ManagedData.");

data.Dispose();

Console.WriteLine($"{id}: ManagedData 成功释放.");

_finalized = true; // 由dispose释放

}

}

3.3.2 定义一个继承IDisposable接口的类

接下来定义一个实现IDisposable接口的SampleObject来使用。这里采用标准的释放模式(Dispose Pattern)编写,但故意将托管资源的释放放在disposing判断之外。

class SampleObject : IDisposable
{
    private ManagedData _mo;
    int id;

    public SampleObject(int id) // 记录当前对象id
    {
        this.id = id;
        _mo = new ManagedData(id);
    }

    private bool disposedValue;

    // 标准的释放模式写法
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue) // 如果已执行dispose,则以下代码跳过
        {
            // 判定来源
            // 若是由手动Dispose()调用的,disposing为true,释放托管资源
            // 若是由终结器在析构函数调用的,disposing为false,此时不应释放托管资源
            if (disposing)
            {
                // 本应在此处编写托管资源
            }
            try
            {
                _mo.Dispose();  // 为了测试,这里将托管资源的释放和操作放在外部
            }
            catch (Exception ex)
            {
                Console.WriteLine($"{id}: 异常: {ex.GetType().Name} - {ex.Message}");
            }
            disposedValue = true;
        }
    }

    ~SampleObject()
    {
        Dispose(disposing: false);
    }

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

3.3.3 我们创建一些对象进行测试

尝试在一个循环中创建这些对象,随后调用GC,等待GC处理

for (int i = 0; i < 5; i++)
{
    new SampleObject(i); // 即刻变为垃圾
}
Console.WriteLine("创建完成,启动GC...");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine($"GC处理完毕");
Console.ReadLine(); // 需要有一个暂停,以便查看最终的打印结果

// 控制台输出

// 创建完成,启动GC...

// 0:ManagedData 已终结.

// 1:ManagedData 已终结.

// 1:异常: ObjectDisposedException - 无法接触已处置的对象.

// 对象名称: '1:无法接触已终结的ManagedData.'

// 2:ManagedData 成功释放.

// 2:ManagedData 已终结.

// 3:ManagedData 成功释放.

// 3:ManagedData 已终结.

// 0:异常: ObjectDisposedException - 无法接触已处置的对象.

// 对象名称: '0:无法接触已终结的ManagedData.'

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

相关推荐
栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群