C#依赖注入生命周期详解:Scoped、Singleton、Transient全面对比
依赖注入(DI)是现代C#开发的核心部分,掌握三种生命周期模式对于构建高效、可维护的应用至关重要。本文通过详细的Console示例,帮助你彻底理解Scoped、Singleton和Transient的区别。
依赖注入生命周期简介
在C# .NET Core/.NET 5+应用程序中,依赖注入框架提供了三种主要的服务生命周期:
- Transient(瞬时):每次请求时创建新实例
- Scoped(作用域):在同一作用域内共享同一实例
- Singleton(单例):整个应用程序共享同一实例
选择正确的生命周期对于应用程序性能和内存管理至关重要。接下来,我们将通过代码示例详细解析三者的区别。
项目环境准备
安装必要的NuGet包:
定义服务接口和实现
我们创建一个简单的服务接口和实现,用于演示不同生命周期:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
usingstatic AppDependencyInjection.Program;
namespace AppDependencyInjection
{
// 服务接口
public interface IExampleService
{
Guid Id { get; } // 用于识别服务实例
void DoSomething();
}
// 服务实现
publicclass ExampleService : IExampleService, IScopedService, ISingletonService
{
public Guid Id { get; }
public ExampleService()
{
// 在构造函数中生成唯一ID,用于标识实例
Id = Guid.NewGuid();
Console.WriteLine($"创建新的服务实例: {Id}");
}
public void DoSomething()
{
Console.WriteLine($"服务实例 {Id} 执行操作");
}
}
}
体验三种生命周期模式
下面是完整的控制台应用程序,演示了三种不同生命周期的行为:
三种生命周期详细对比
|特性|Transient|Scoped|Singleton||-|-|-|-||创建时机|每次请求服务时|每个作用域第一次请求时|首次请求或应用启动时||实例数量|每次请求都创建新实例|每个作用域一个实例|整个应用程序只有一个实例||实例共享|不共享|同一作用域内共享|全局共享||内存占用|较高|中等|最低||适用场景|轻量级、无状态服务|Web请求、数据库上下文|全局配置、缓存服务||线程安全|天然线程安全|需考虑作用域内的并发|必须实现线程安全|
选择正确生命周期的最佳实践
何时使用Transient(瞬时服务)
- ✅ 轻量级、无状态的服务
- ✅ 不共享状态的服务
- ✅ 每次使用需要全新状态的服务
- ❌ 避免用于开销大的服务(如数据库连接)
何时使用Scoped(作用域服务)
- ✅ Web应用中的请求级服务
- ✅ Entity Framework DbContext
- ✅ 需要在请求或操作期间保持状态的服务
- ❌ 避免在单例服务中注入作用域服务
何时使用Singleton(单例服务)
- ✅ 全局配置服务
- ✅ 缓存服务
- ✅ 日志服务
- ✅ 重量级、创建成本高的服务
- ❌ 避免用于包含用户特定数据的服务
- ❌ 必须确保线程安全
// 注册Singleton服务示例 services.AddSingleton<IConfiguration, AppConfiguration>(); services.AddSingleton<ICacheService, RedisCacheService>();
常见陷阱和解决方案
作用域服务被单例服务依赖
问题: 当单例服务依赖于作用域服务时会导致问题,因为单例服务只创建一次,而它依赖的作用域服务会被锁定在创建单例时的那个作用域。
解决方案: 使用IServiceProvider和工厂模式:
public class SingletonService
{
private readonly IServiceProvider _serviceProvider;
public SingletonService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void DoWork()
{
// 在需要时获取作用域服务
using (var scope = _serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetService<IScopedService>();
scopedService.DoSomething();
}
}
}
内存泄漏风险
问题: 单例服务如果持有对临时对象的引用可能导致内存泄漏。
解决方案: 使用弱引用或事件处理模式:
public class CacheService
{
private readonly ConditionalWeakTable<object, object> _cache = new();
public void Add(object key, object value)
{
_cache.Add(key, value);
}
}
线程安全问题
问题: 单例服务被多线程访问导致竞态条件。
解决方案: 使用线程安全的数据结构和同步机制:
public class ThreadSafeSingleton
{
private readonly ConcurrentDictionary<string, object> _concurrentCache = new();
public void AddToCache(string key, object value)
{
_concurrentCache.AddOrUpdate(key, value, (k, v) => value);
}
}
总结
选择合适的依赖注入生命周期对于构建高效、可维护的C#应用至关重要:
- Transient:每次请求创建新实例,适用于轻量级、无状态服务
- Scoped:在作用域内共享实例,适用于请求级服务如DbContext
- Singleton:应用程序全局共享一个实例,适用于配置、缓存等全局服务
通过本文的详细示例,您应该能够清晰地理解这三种生命周期的区别,并在实际项目中做出正确的选择。实际应用中,通常会混合使用这三种生命周期以获得最佳性能和资源利用率。
本文来自博客园,作者:南風未起,转载请注明原文链接:https://www.cnblogs.com/Andy-Blog/p/21434792


浙公网安备 33010602011771号