C#依賴注入生命周期詳解:Scoped、Singleton、Transient全面對比
依賴注入(DI)是現代C#開發的核心部分,掌握三種生命周期模式對于構建高效、可維護的應用至關重要。本文通過詳細的Console示例,幫助你徹底理解Scoped、Singleton和Transient的區別。
依賴注入生命周期簡介
在C# .NET Core/.NET 5+應用程序中,依賴注入框架提供了三種主要的服務生命周期:
- Transient(瞬時):每次請求時創建新實例
- Scoped(作用域):在同一作用域內共享同一實例
- Singleton(單例):整個應用程序共享同一實例
選擇正確的生命周期對于應用程序性能和內存管理至關重要。接下來,我們將通過代碼示例詳細解析三者的區別。
項目環境準備
安裝必要的NuGet包:
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting定義服務接口和實現
我們創建一個簡單的服務接口和實現,用于演示不同生命周期:
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} 執行操作");
}
}
}體驗三種生命周期模式
下面是完整的控制臺應用程序,演示了三種不同生命周期的行為:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DILifecycleDemo
{
class Program
{
static void Main(string[] args)
{
// 創建Host生成器
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
// 注冊三種不同生命周期的服務
services.AddTransient<IExampleService, ExampleService>();
// 為了區分不同生命周期的服務,使用不同的接口
services.AddScoped<IScopedService, ExampleService>();
services.AddSingleton<ISingletonService, ExampleService>();
})
.Build();
Console.WriteLine("=== 依賴注入生命周期測試 ===");
// 測試Transient生命周期
TestTransientLifetime(host.Services);
// 測試Scoped生命周期
TestScopedLifetime(host.Services);
// 測試Singleton生命周期
TestSingletonLifetime(host.Services);
Console.WriteLine("\n按任意鍵退出...");
Console.ReadKey();
}
// 測試Transient生命周期的方法
static void TestTransientLifetime(IServiceProvider serviceProvider)
{
Console.WriteLine("\n=== Transient生命周期測試 ===");
Console.WriteLine("特點:每次請求都創建新實例");
// 第一次獲取Transient服務
Console.WriteLine("\n第一次請求Transient服務:");
var transient1 = serviceProvider.GetService<IExampleService>();
transient1.DoSomething();
// 第二次獲取Transient服務
Console.WriteLine("\n第二次請求Transient服務:");
var transient2 = serviceProvider.GetService<IExampleService>();
transient2.DoSomething();
// 比較實例ID以驗證是否創建了不同的實例
Console.WriteLine($"\n兩個實例是否相同: {transient1.Id == transient2.Id}");
Console.WriteLine($"實例1 ID: {transient1.Id}");
Console.WriteLine($"實例2 ID: {transient2.Id}");
}
// 接口定義 - 為區分不同生命周期的服務
public interface IScopedService : IExampleService { }
public interface ISingletonService : IExampleService { }
// 測試Scoped生命周期的方法
static void TestScopedLifetime(IServiceProvider rootProvider)
{
Console.WriteLine("\n=== Scoped生命周期測試 ===");
Console.WriteLine("特點:在同一作用域內共享實例,不同作用域使用不同實例");
// 創建第一個作用域
Console.WriteLine("\n創建第一個作用域:");
using (var scope1 = rootProvider.CreateScope())
{
// 在同一作用域內獲取兩次服務
Console.WriteLine("在第一個作用域內第一次請求:");
var scoped1 = scope1.ServiceProvider.GetService<IScopedService>();
scoped1.DoSomething();
Console.WriteLine("\n在第一個作用域內第二次請求:");
var scoped2 = scope1.ServiceProvider.GetService<IScopedService>();
scoped2.DoSomething();
// 比較同一作用域內的實例
Console.WriteLine($"\n同一作用域內兩個實例是否相同: {scoped1.Id == scoped2.Id}");
}
// 創建第二個作用域
Console.WriteLine("\n創建第二個作用域:");
using (var scope2 = rootProvider.CreateScope())
{
var scoped3 = scope2.ServiceProvider.GetService<IScopedService>();
scoped3.DoSomething();
// 注意:第二個作用域會創建新的實例
Console.WriteLine("\n注意新的作用域創建了新的實例(與第一個作用域不同)");
}
}
// 測試Singleton生命周期的方法
static void TestSingletonLifetime(IServiceProvider serviceProvider)
{
Console.WriteLine("\n=== Singleton生命周期測試 ===");
Console.WriteLine("特點:整個應用程序只創建一個實例");
// 第一次獲取Singleton服務
Console.WriteLine("\n第一次請求Singleton服務:");
var singleton1 = serviceProvider.GetService<ISingletonService>();
singleton1.DoSomething();
// 第二次獲取Singleton服務
Console.WriteLine("\n第二次請求Singleton服務:");
var singleton2 = serviceProvider.GetService<ISingletonService>();
singleton2.DoSomething();
// 比較實例ID以驗證是否使用了相同的實例
Console.WriteLine($"\n兩個實例是否相同: {singleton1.Id == singleton2.Id}");
Console.WriteLine($"實例1 ID: {singleton1.Id}");
Console.WriteLine($"實例2 ID: {singleton2.Id}");
// 在不同作用域中獲取Singleton服務
Console.WriteLine("\n在新的作用域中請求Singleton服務:");
using (var scope = serviceProvider.CreateScope())
{
var singleton3 = scope.ServiceProvider.GetService<ISingletonService>();
singleton3.DoSomething();
// 比較與之前實例
Console.WriteLine($"\n新作用域實例與之前實例是否相同: {singleton1.Id == singleton3.Id}");
Console.WriteLine("注意:Singleton在不同作用域中依然是同一個實例");
}
}
}
// ExampleService實現了所有接口
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(瞬時服務)
- ? 輕量級、無狀態的服務
- ? 不共享狀態的服務
- ? 每次使用需要全新狀態的服務
- ? 避免用于開銷大的服務(如數據庫連接)
// 注冊Transient服務示例
services.AddTransient<IEmailFormatter, EmailFormatter>();何時使用Scoped(作用域服務)
- ? Web應用中的請求級服務
- ? Entity Framework DbContext
- ? 需要在請求或操作期間保持狀態的服務
- ? 避免在單例服務中注入作用域服務
// 注冊Scoped服務示例
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<DbContext, ApplicationDbContext>();何時使用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:應用程序全局共享一個實例,適用于配置、緩存等全局服務
通過本文的詳細示例,您應該能夠清晰地理解這三種生命周期的區別,并在實際項目中做出正確的選擇。實際應用中,通常會混合使用這三種生命周期以獲得最佳性能和資源利用率。





















