楼主: m5097054
43 0

[经济学教育] 写给刚刚毕业非名校而且正在迷惑中的工科大学生们之二——全真教与梅超风 谈程序员的修炼之路 [推广有奖]

  • 0关注
  • 0粉丝

等待验证会员

学前班

40%

还不是VIP/贵宾

-

威望
0
论坛币
0 个
通用积分
0
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
20 点
帖子
1
精华
0
在线时间
0 小时
注册时间
2018-11-8
最后登录
2018-11-8

楼主
m5097054 发表于 2025-11-19 22:26:32 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

让我们先从一个简单的例子开始(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是一种“使用完毕即清理”的协议,便于与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模式与析构函数的组合使用旨在确保资源无论是在显式调用Dispose方法时,还是在对象被垃圾回收器(GC)回收时,都能得到正确的释放。这种模式通常称为“Dispose模式”,是处理托管和非托管资源的一种最佳实践。

示例说明

设想一个对象,其中既包含非托管资源也包含托管资源。以下是一个示例代码:

class SampleResource: IDisposable
{
    private ManagedResource _mr;  // 托管资源
    private UnmanagedResource _ur; // 非托管资源
    public void Dispose()   // 资源释放
    {
        _mr.Dispose(); // 释放托管资源
        _ur.Dispose(); // 释放非托管资源
    }
}

防止Dispose方法的重复调用

通常情况下,我们的代码逻辑是合理的。然而,考虑到某些资源(如托管资源和非托管资源)可能不是由我们自己编写的,因此重复调用Dispose可能会导致问题。为了解决这个问题,可以在SampleResource类中添加一个标志位,以避免资源的重复释放。更新后的代码如下:

class SampleResource: IDisposable
{
    private ManagedResource _mr;
    private UnmanagedResource _ur;
    private bool isDisposed = false; // 添加: 标志变量
    public void Dispose()
    {
        if (!isDisposed) // 添加: 检查标志变量,避免重复调用
        {
            _mr.Dispose();
            _ur.Dispose();
            isDisposed = true;
        }
    }
}

避免遗漏Dispose方法的调用

对于包含非托管资源的对象,如果忽略了Dispose()的调用,轻则可能导致内存泄漏,重则可能引发严重问题。为了确保即使在忘记调用Dispose()的情况下也能释放资源,可以通过添加析构函数来实现。这样,当对象被GC回收时,资源将自动释放。以下是更新后的示例代码:

class SampleResource: IDisposable
{
    private ManagedResource _mr;
    private UnmanagedResource _ur;
    private bool isDisposed = false;
    public void Dispose()
    {
        ReleaseResources(); // 执行资源释放
        GC.SuppressFinalize(this); // 如果手动调用了Dispose(),告知终结器不再执行析构函数
    }
    protected virtual void ReleaseResources()  // 从Dispose方法中分离出的资源释放逻辑
    {
        if (!isDisposed)
        {
            _mr.Dispose();
            _ur.Dispose();
            isDisposed = true;
        }
    }
    ~SampleResource() // 新增: 析构函数,当忘记调用Dispose()时由终结器执行
    {
        ReleaseResources();
    }
}

托管资源的提前回收

即使对象忘记了调用Dispose(),触发析构函数也可以执行资源的释放。然而,由于终结器的执行顺序不确定,当SampleResource对象被终结器触发析构函数时,其他对象(如_mr)也可能已经触发了析构函数。这意味着当SampleResource执行Dispose时,可能存在_mr的Dispose()方法被调用两次的情况,这可能会导致意外的结果。

存在缺陷的托管资源类定义

下面是一个没有处理重复释放情况的托管资源类示例:

class FaultyManagedResource: IDisposable
{
    private MemoryStream _data = new MemoryStream(new byte[100_000_000]); // 模拟托管资源
    private bool _isFinalized = false;
    int _id;
    public FaultyManagedResource(int id)  // 记录当前对象ID
    {
        _id = id;
    }
    ~FaultyManagedResource()
    {
        _isFinalized = true;  // 由析构函数释放
        Console.WriteLine($"{_id}: FaultyManagedResource 已终结.");
    }
    public void Dispose()
    {
        if (_isFinalized)
        {
            // 处理重复释放的问题
        }
    }
}

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

接下来,我们将定义一个实现了IDisposable接口的SampleObject类。这里采用标准的释放模式(Dispose Pattern),但会故意将托管资源的释放置于disposing判断之外。


class SampleObject : IDisposable
{
    private ManagedData _managedData;
    private int _id;
    
    public SampleObject(int id)
    {
        _id = id;
        _managedData = new ManagedData(id);
    }
    
    private bool _disposedValue;

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // 这里通常应处理托管资源的释放
            }
            try
            {
                _managedData.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),并等待其完成资源的释放过程。


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 - Cannot access a disposed object.
对象名称: '1:无法访问已终结的ManagedData.'.
2:ManagedData 正常释放.
2:ManagedData 已终结.
3:ManagedData 正常释放.
3:ManagedData 已终结.
0:异常: ObjectDisposedException - Cannot access a disposed object.
对象名称: '0:无法访问已终结的ManagedData.'.
    
二维码

扫码加我 拉你入群

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

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

关键词:修炼之路 全真教 刚毕业 非名校 程序员

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
jg-xs1
拉您进交流群
GMT+8, 2025-12-27 06:38