C#的订阅模式

编程语言 2026-07-09
  1. 这段代码会导致内存泄漏吗? 还是垃圾回收器(GC)会清理这两个对象,因为它们形成了一个“隔离岛”,并且不再被应用程序根引用?

  2. 在这里的最佳实践是什么? 如果 EventPublisher 的生命周期比 EventSubscriber 长(例如一个单例),我应该如何重构 EventSubscriber 以防止内存泄漏?

  3. 应该让 EventSubscriber 实现 IDisposable 以取消订阅(publisher.ProcessCompleted -= HandleProcessCompleted),还是有更现代的做法,如弱事件模式?

public class EventPublisher
{
    public event EventHandler? ProcessCompleted;

    public async Task DoProcess()
    {
        await Task.Delay(100);
        OnProcessCompleted(EventArgs.Empty);
    }

    protected virtual void OnProcessCompleted(EventArgs e)
    {
        ProcessCompleted?.Invoke(this, e);
    }
}

public class EventSubscriber : IDisposable
{
    private readonly string _name;
    private readonly EventPublisher _publisher;

    public EventSubscriber(string name, EventPublisher publisher)
    {
        _name = name;
        _publisher = publisher;
        publisher.ProcessCompleted +=  HandleProcessCompleted!;
    }

    private void HandleProcessCompleted(object sender, EventArgs e)
    {
        Console.WriteLine($"{_name} received event.");
    }

    public void Dispose()
    {
        _publisher.ProcessCompleted -= HandleProcessCompleted!;
    }
}

class Program
{
    static async Task Main(string[] args)
    {
        var tasks = Enumerable.Range(1, 10000).Select(async i =>
        {
            var publisher = new EventPublisher();
            using var subscriber = new EventSubscriber($"Subscriber_{i}", publisher);
            await publisher.DoProcess();
        });

        await Task.WhenAll(tasks);

        Console.WriteLine("Completed. Press any key to exit.");
        Console.ReadKey();
    }
}

namespace Dotnet9ConsoleExceptionHandling { public class Program { public static void Main(string[] args) { Console.WriteLine("Enter numbers separated by comma (e.g., 2,4,5):"); string? input = Console.ReadLine();

        // بررسی خالی بودن ورودی
        if (string.IsNullOrWhiteSpace(input))
        {
            Console.WriteLine("Input cannot be empty.");
            return;
        }

        string[] tokens = input.Split(',');
        List<int> numbers = new List<int>();

        foreach (var token in tokens)
        {
            // تغییر اول: استفاده از int.TryParse به جای try-catch
            // این روش خطایی پرتاب نمی‌کند و پرفورمنس بالاتری دارد
            if (int.TryParse(token.Trim(), out int number))
            {
                // تغییر دوم: بررسی عدد صفر بدون متوقف کردن کل برنامه
                if (number == 0)
                {
                    Console.WriteLine("Warning: Zero is not allowed (division by zero). Skipping '0'.");
                    continue; // رد شدن از این عدد و ادامه حلقه
                }

                numbers.Add(number);
            }
            else
            {
                // اگر کاربر حرف یا متن وارد کرد، فقط اخطار می‌دهیم و از آن رد می‌شویم
                Console.WriteLine($"Warning: Invalid input '{token}'. Skipping non-numeric value.");
            }
        }

        // اگر هیچ عدد معتبری وارد نشده بود
        if (numbers.Count == 0)
        {
            Console.WriteLine("No valid numbers were provided for calculation.");
            return;
        }

        // دریافت نتیجه با نوع double
        double result = CalculateSumOfReciprocals(numbers);

        // نمایش خروجی با دو رقم اعشار برای زیبایی بیشتر
        Console.WriteLine($"Sum of reciprocals: {result:F2}");
    }

    // تغییر سوم: تغییر نوع خروجی از int به double
    public static double CalculateSumOfReciprocals(List<int> numbers)
    {
        double sum = 0;
        foreach (var n in numbers)
        {
            // تغییر چهارم: استفاده از 1.0 به جای 1 
            // این کار باعث می‌شود کامپایلر محاسبه را به صورت اعشاری (Floating-point) انجام دهد
            sum += 1.0 / n; 
        }
        return sum;
    }
}

}

class Program { static List results = new List();

static async Task Main(string[] args)
{
    var numbers = Enumerable.Range(1, 100).ToList();
    var tasks = new List<Task>();

    foreach (var number in numbers)
    {
        tasks.Add(ProcessNumberAsync(number));
    }

    await Task.WhenAll(tasks);
    Console.WriteLine($"Sum: {results.Sum()}");
}

static async Task ProcessNumberAsync(int number)
{
    await Task.Delay(10);
    results.Add(number * 2);
}

}

可在Stack Overflow上复现的标准版本

这就是你的代码,只是缩进改为四个空格:

class LargeObject
{
// تخصیص ۱۰ مگابایت حافظه
public byte[] Data = new byte[1024 * 1024 * 10];
}

// تغییر اول: پیاده‌سازی IDisposable برای مدیریت دقیق چرخه حیات
class MemorySafeManager : IDisposable
{
// تغییر دوم: حذف کلمه کلیدی static
// دلیل: با این کار، لیست فقط تا زمانی زنده است که کلاس Manager زنده باشد
private List<LargeObject> _objects = new List<LargeObject>();

public Task AddObjects(int count, LargeObject obj)
{
for (int i = 0; i < count; i++)
{
_objects.Add(obj);
}

return Task.CompletedTask;
}

// تغییر سوم: پیاده‌سازی متد Dispose برای آزادسازی صریح حافظه
public void Dispose()
{
// پاک کردن تمام رفرنس‌های داخل لیست
_objects.Clear();

// در پروژه‌های بزرگ، قرار دادن لیست روی null به GC کمک بیشتری می‌کند
// _objects = null; 
}
}

class Program
{
static async Task Main(string[] args)
{
// تغییر چهارم: استفاده از بلوک using 
// این کار تضمین می‌کند که به محض خروج از این بلوک، متد Dispose صدا زده شود
using var manager = new MemorySafeManager();

var obj = new LargeObject();

var tasks = Enumerable.Range(0, 20).Select(async i =>
{
await manager.AddObjects(5, obj);
Console.WriteLine($"Iteration {i + 1}: Objects added.");
await Task.Delay(500);
});

await Task.WhenAll(tasks);

Console.WriteLine("Process finished and memory is safe. Press any key to exit...");
Console.ReadKey();
}
}

解决方案

不,这里没有内存泄漏。每个发布者和订阅者都可以被垃圾回收,因为没有其他引用它们(甚至连静态变量也没有引用)。

EventSubscriber 实现 IDisposable 对我来说似乎是合理的。它 确实 要求调用方确保他们在使用完毕后对其进行处置,否则可能导致内存泄漏,但也就这些。使用弱事件模式使其更易用、无需担心订阅的处置,似乎有点过于复杂——如果你最终遇到难以管理生命周期的情况,我可能会考虑更改设计,而不是尝试使用一种不要求你正确理解生命周期的模式。(起初,在许多情况下我会尽量避免使用单例。这样的做法在某些场景下确实更简单,但也更难测试,且会带来此类生命周期模糊。)

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章