C#依赖注入

一、什么是依赖注入?

依赖指的是:当一个类(A)需要另一个类(B)的功能才能完成工作时,A 就 "依赖" 于 B。
例如:OrderService需要用Logger记录日志,那么OrderService依赖于Logger

依赖注入的核心思想是:将依赖的创建和管理交给外部容器,而不是在类内部自行创建。
简单说就是 "谁需要依赖,就由外部把依赖传给它",而非自己创建。
 

二、为什么需要依赖注入?

先看一个 "没有依赖注入" 的反例:
// 日志类
public class Logger
{
    public void Log(string message) => Console.WriteLine($"日志:{message}");
}

// 订单服务(依赖Logger)
public class OrderService
{
    // 自己创建依赖(问题所在)
    private readonly Logger _logger = new Logger();
    
    public void CreateOrder()
    {
        _logger.Log("订单创建成功");
        // 其他业务逻辑...
    }
}
View Code

这个例子中OrderService直接依赖Logger的具体实现,如果未来要替换为FileLogger,必须修改OrderService,这就违反了开闭原则,对扩展开放,对修改关闭

如果使用依赖注入,应该要生命一个抽象接口,让服务类去依赖接口,以后要添加别的类型的日志的适合直接添加就行,然后在逻辑代码中实例化接口,传入给服务类的构造函数。

// 抽象接口(依赖抽象)
public interface ILogger
{
    void Log(string message);
}

// 具体实现
public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine($"日志:{message}");
}

// 订单服务(依赖通过外部注入)
public class OrderService
{
    private readonly ILogger _logger;
    
    // 构造函数注入(推荐方式)
    public OrderService(ILogger logger)
    {
        _logger = logger; // 依赖由外部传入,而非自己创建
    }
    
    public void CreateOrder()
    {
        _logger.Log("订单创建成功");
    }
}
View Code

三、依赖注入的 3 种方式

在 C# 中,依赖注入通常通过以下 3 种方式实现:

1. 构造函数注入(最推荐)

通过类的构造函数接收依赖,是.NET 中最常用的方式。适用于 "必须的依赖"(类没有它就无法工作)。
 
public class UserService
{
    private readonly ILogger _logger;
    
    // 构造函数注入:创建UserService时必须提供ILogger
    public UserService(ILogger logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
}
View Code

2. 属性注入

通过公共属性接收依赖,适用于 "可选的依赖"(类没有它也能工作)。
ASP.NET Core 中,通常配合[Inject]特性使用(如在视图中)
public class UserService
{
    // 属性注入:依赖是可选的
    public ILogger Logger { get; set; }
    
    public void DoWork()
    {
        Logger?.Log("执行工作"); // 注意判空
    }
}
View Code

3. 方法注入

通过方法参数接收依赖,适用于仅在特定方法中需要的依赖。
public class UserService
{
    public void DoWork(ILogger logger)
    {
        logger.Log("执行工作"); // 仅该方法需要依赖
    }
}
View Code

 

四、微软自带的Microsoft.Extensions.DependencyInjection

1.核心原理是通过服务容器管理服务的注册、生命周期和依赖解析,其设计遵循 “依赖倒置” 和 “控制反转” 原则,核心流程可分为服务注册、容器构建和服务解

using Microsoft.Extensions.DependencyInjection;

// 定义服务接口和实现
public interface IUserService { }
public class UserService : IUserService { }

// 注册服务其实就是一个LIst<ServiceDescriptor>集合
var services = new ServiceCollection();
// 瞬时生命周期,new一个新的元素,参数:抽象类型为接口,具体实现类型,Transient是生命周期
services.AddTransient<IUserService, UserService>();//比如小工具,计算等,小东西,可忽略性能的
// 作用域生命周期
services.AddScoped<IUserService, UserService>();//比如创建的多个流程
// 单例生命周期
services.AddSingleton<IUserService, UserService>();//比如硬件需要长连接,大型框架就是硬件工厂模式全局唯一

// 3. 构建服务提供器(DI容器)
_serviceProvider = services.BuildServiceProvider();

2.容器构建(BuildServiceProvider

当服务注册完成后,调用 services.BuildServiceProvider() 会创建 IServiceProvider 实例(默认实现为 ServiceProvider)。这一步是 “从注册到可用容器” 的关键转换,核心工作包括:

验证服务注册的合法性

为每个服务创建 “解析器”

初始化根容器与作用域管理

3.服务解析(GetService/GetRequiredService

当调用 serviceProvider.GetRequiredService<T>() 时,ServiceProvider 会按以下流程解析服务实例:

查找服务元数据

根据生命周期创建实例

 自动注入依赖(构造函数注入)

 处理 IDisposable 服务

4.最后总结原理就类似 :

1)注册阶段:new一个字典集合,注册:相当于添加元素,添加接口和实现类指定生命周期方式

2)构建阶段:BuildServiceProvider() 验证注册合法性,生成解析器,初始化根容器。

3)解析阶段:

据服务类型查找 ServiceDescriptor。按生命周期(Transient/Scoped/Singleton)创建实例,递归注入依赖。缓存实例(Scoped 缓存于作用域,Singleton 缓存于根容器)。

4)清理阶段:作用域或根容器释放时,自动清理 IDisposable 服务。

四、依赖注入在WPF中的应用

1.第一步删除App.xaml文件中删除StartupUri="MainWindow.xaml"

2.定义接口

using System;
namespace 依赖注入包典型应用
{
    /// <summary>
    /// 定义服务接口
    /// </summary>
    public interface IDataService
    {
        string GetData();
        string RefreshData();
    }
    /// <summary>
    ///  Log服务接口   
    /// </summary>
    public interface ILoggerService
    {
        void Log(string message);
        void LogError(string errorMessage);
    }
    /// <summary>
    /// 设定服务接口
    /// </summary>
    public interface ISettingsService
    {
        string GetSetting(string key);
        void SetSetting(string key, string value);
    }
}
View Code

3.定义基于接口的数据服务

using System.IO;

namespace 依赖注入包典型应用
{
    public class DataService : IDataService
    {
        // 可以在这里注入其他服务,如数据库访问服务等
        private int _refreshCount = 0;

        public string GetData()
        {
            return "Initial data from DataService - " +
                   $"Current time: {DateTime.Now:HH:mm:ss}";
        }

        public string RefreshData()
        {
            _refreshCount++;
            return $"Refreshed data ({_refreshCount}) - " +
                   $"Current time: {DateTime.Now:HH:mm:ss}";
        }
    }
    public class LoggerService : ILoggerService
    {
        public void Log(string message)
        {
            var logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] INFO: {message}{Environment.NewLine}";
            Console.WriteLine(logMessage); // 输出到控制台
            WriteToLogFile(logMessage);    // 写入日志文件
        }

        public void LogError(string errorMessage)
        {
            var logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ERROR: {errorMessage}{Environment.NewLine}";
            Console.WriteLine(logMessage); // 输出到控制台
            WriteToLogFile(logMessage);    // 写入日志文件
        }

        private void WriteToLogFile(string message)
        {
            try
            {
                var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "app.log");
                File.AppendAllText(logPath, message);
            }
            catch
            {
                // 日志写入失败时的处理
            }
        }
    }
    public class SettingsService : ISettingsService
    {
        // 实际应用中可能会从配置文件或注册表读取设置
        private readonly Dictionary<string, string> _settings;

        public SettingsService()
        {
            // 初始化一些默认设置
            _settings = new Dictionary<string, string>
            {
                { "AppName", "WPF DI Example" },
                { "Version", "1.0.0" },
                { "Theme", "Light" }
            };
        }

        public string GetSetting(string key)
        {
            if (_settings.TryGetValue(key, out var value))
            {
                return value;
            }
            return null;
        }

        public void SetSetting(string key, string value)
        {
            if (_settings.ContainsKey(key))
            {
                _settings[key] = value;
            }
            else
            {
                _settings.Add(key, value);
            }

            // 在实际应用中,这里应该将设置保存到持久化存储
        }
    }
}
View Code

 

4.创建,注册,解析

using Microsoft.Extensions.DependencyInjection;
using System.Windows;

namespace 依赖注入包典型应用
{
    public partial class App : Application
    {
        private readonly IServiceProvider _serviceProvider;

        public App()
        {
            // 配置服务集合
            var serviceCollection = new ServiceCollection();
            ConfigureServices(serviceCollection);

            // 容器构建:验证服务注册的合法性,为每个服务创建 “解析器”,初始化根容器与作用域管理
            _serviceProvider = serviceCollection.BuildServiceProvider();
        }

        private void ConfigureServices(IServiceCollection services)
        {
            // 注册窗口
            services.AddSingleton<MainWindow>();

            // 注册服务
            services.AddScoped<IDataService, DataService>();//范围,在作用域内(IServiceScope )缓存实例,同一作用域内复用,比如流程
            services.AddTransient<ILoggerService, LoggerService>();//瞬时,每次解析服务时都会创建新实例,用于轻量级服务。如小工具,计算格式化等
            services.AddSingleton<ISettingsService, SettingsService>();//单例,在整个应用程序的生命周期中只创建一次实例。比如唯一硬件(工厂模式下的相机)长连接模式,配置管理等
        }

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            // 服务解析:查找服务元数据,根据生命周期创建实例,自动注入依赖(构造函数注入)
            var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
            mainWindow.Show();
        }
    }

}
View Code

5.界面代码。界面代码没什么可以看的

<Window x:Class="依赖注入包典型应用.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:依赖注入包典型应用"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <!--后台代码使用-->
        <TextBlock x:Name="DataDisplay" 
                   TextWrapping="Wrap" 
                   Margin="0,0,0,20"/>
        
        <Button x:Name="RefreshButton" 
                Content="Refresh Data" 
                Click="RefreshButton_Click"
                Grid.Row="1"
                HorizontalAlignment="Left"
                Padding="10,5"/>
    </Grid>
</Window>
View Code

6.界面后台代码,使用已注册的服务

using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace 依赖注入包典型应用
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly IDataService _dataService;
        private readonly ILoggerService _loggerService;
        private readonly ISettingsService _settingsService;

        // 构造函数依赖注入
        public MainWindow(IDataService dataService,
                          ILoggerService loggerService,
                          ISettingsService settingsService)
        {
            InitializeComponent();

            _dataService = dataService;
            _loggerService = loggerService;
            _settingsService = settingsService;

            // 使用注入的服务
            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                // 使用设置服务//单例的 实例
                var appName = _settingsService.GetSetting("AppName");
                Title = appName;

                // 使用数据服务//范围的实例
                var data = _dataService.GetData();
                DataDisplay.Text = data;

                // 使用日志服务//瞬时的实例
                _loggerService.Log("Main window loaded successfully");
            }
            catch (Exception ex)
            {
                _loggerService.LogError($"Error loading main window: {ex.Message}");
                MessageBox.Show($"An error occurred: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        private void RefreshButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {// 再次使用数据和日志服务
                var newData = _dataService.RefreshData();
                DataDisplay.Text = newData;
                _loggerService.Log("Data refreshed");
            }
            catch (Exception ex)
            {
                _loggerService.LogError($"Error refreshing data: {ex.Message}");
                MessageBox.Show($"An error occurred: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }
    }
}
View Code

 

posted @ 2025-09-20 14:36  灰色淡季  阅读(97)  评论(0)    收藏  举报