C#的订阅模式
-
这段代码会导致内存泄漏吗? 还是垃圾回收器(GC)会清理这两个对象,因为它们形成了一个“隔离岛”,并且不再被应用程序根引用?
-
在这里的最佳实践是什么? 如果
EventPublisher的生命周期比EventSubscriber长(例如一个单例),我应该如何重构EventSubscriber以防止内存泄漏? -
应该让
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 对我来说似乎是合理的。它 确实 要求调用方确保他们在使用完毕后对其进行处置,否则可能导致内存泄漏,但也就这些。使用弱事件模式使其更易用、无需担心订阅的处置,似乎有点过于复杂——如果你最终遇到难以管理生命周期的情况,我可能会考虑更改设计,而不是尝试使用一种不要求你正确理解生命周期的模式。(起初,在许多情况下我会尽量避免使用单例。这样的做法在某些场景下确实更简单,但也更难测试,且会带来此类生命周期模糊。)