找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4458

积分

0

好友

614

主题
发表于 1 小时前 | 查看: 2| 回复: 0

在C#开发中,依赖注入(DI)早已不是一项可选技术,而是整个.NET生态的核心基石。无论是Core框架自带的DI容器,还是诸如Autofac、Unity这样的第三方实现,它们的本质都是帮助我们管理对象依赖、实现项目解耦的「对象工厂」。

然而,不少开发者只熟悉框架封装好的用法,对其底层原理却一知半解。一旦遇到生命周期错乱或服务解析失败的问题,往往无从下手。今天,我们不依赖任何第三方库,从零开始手写一个轻量级的DI容器,再将其落地到实战项目中,旨在彻底吃透依赖注入的核心思想。

一、依赖注入的本质:为什么需要它?

1. 核心概念速览

  • 依赖:当A类需要调用B类的方法来完成自身功能时,A就依赖于B。例如,控制器依赖服务层,服务层又依赖数据访问层。
  • 注入:指不通过手动 new 来创建依赖对象,而是由外部容器自动传入(通常通过构造函数、属性或方法)。
  • DI容器:一个统一管理所有服务的创建、生命周期和依赖解析的组件,你可以将其理解为一个智能的「对象管家」。

2. 传统方式的痛点:强耦合

让我们先看一个反面例子。在传统开发模式中,我们习惯于手动 new 对象,这导致类与类之间高度耦合:

// 仓储层:操作数据库
public class UserRepository
{
    public void AddUser() => Console.WriteLine("新增用户");
}

// 服务层:依赖仓储层
public class UserService
{
    // 硬编码new对象!强耦合:修改UserRepository就必须修改UserService
    private readonly UserRepository _userRepo = new UserRepository();
    public void AddUser() => _userRepo.AddUser();
}

// 调用层
var service = new UserService();
service.AddUser();

这种方式存在几个明显问题:

  1. 强耦合:如果 UserRepository 的类名或构造函数发生改变,UserService 也必须同步修改,牵一发而动全身。
  2. 难以测试:在单元测试中,我们无法轻松地将真实的 UserRepository 替换为模拟对象(Mock)。
  3. 管理混乱:对象的生命周期完全由开发者手动控制,容易导致内存泄漏或资源未释放。

3. 引入依赖注入的优势

  • 解耦:依赖抽象(接口),而非具体实现,这是面向接口编程的核心实践。
  • 自动管理:容器负责对象的创建与销毁,开发者无需关心 newdispose 的细节。
  • 易于测试:可以轻松为接口注入不同的实现(如Mock对象)进行单元测试。
  • 统一规范:使项目结构更加清晰,更符合面向对象设计的开闭原则。

二、DI的前提:面向接口编程

依赖注入的核心思想是依赖抽象,而非具体实现。我们首先需要将上面的强耦合代码改造为面向接口的形式:

// 1. 定义抽象仓储接口
public interface IUserRepository
{
    void AddUser();
}

// 2. 提供具体实现类
public class UserRepository : IUserRepository
{
    public void AddUser() => Console.WriteLine("数据库新增用户");
}

// 3. 定义抽象服务接口
public interface IUserService
{
    void AddUser();
}

// 4. 服务实现:构造函数依赖IUserRepository(抽象接口)
public class UserService : IUserService
{
    private readonly IUserRepository _userRepo;
    // 构造函数注入:依赖由外部传入,内部不自己new
    public UserService(IUserRepository userRepo)
    {
        _userRepo = userRepo;
    }
    public void AddUser() => _userRepo.AddUser();
}

现在,UserService 只认识 IUserRepository 这个接口,完全不知道也不关心背后具体是哪个实现类。这就是解耦的第一步。

三、从零手写C# DI容器:理解核心原理

一个最精简的DI容器,只需要完成三件核心任务:

  1. 注册服务:绑定「抽象接口」与「具体实现类」,并声明其生命周期。
  2. 解析服务:根据请求的抽象类型,自动创建对象,并递归地解析其所有依赖。
  3. 管理生命周期:控制服务实例是单例、瞬时还是作用域内唯一。

1. 定义生命周期枚举

参照.NET Core的标准,我们定义三种常见的生命周期:

/// <summary>
/// 服务生命周期
/// </summary>
public enum ServiceLifetime
{
    Singleton, // 单例:全局唯一实例
    Transient, // 瞬时:每次获取都创建新实例
    Scoped     // 作用域:每个请求/作用域内一个实例(Web常用)
}

2. 定义服务描述实体

这个类用于存储注册服务时的所有元数据信息:

/// <summary>
/// 服务描述:存储接口与实现的映射关系
/// </summary>
public class ServiceDescriptor
{
    public Type ServiceType { get; } // 抽象类型(接口)
    public Type ImplementationType { get; } // 具体实现类型
    public ServiceLifetime Lifetime { get; } // 生命周期
    public object? ImplementationInstance { get; set; } // 单例实例缓存

    // 构造函数
    public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime)
    {
        ServiceType = serviceType;
        ImplementationType = implementationType;
        Lifetime = lifetime;
    }
}

3. 实现DI容器核心类

这是整个容器的核心,包含服务注册和递归解析依赖的逻辑:

/// <summary>
/// 手写轻量级DI容器
/// </summary>
public class MiniContainer
{
    // 存储所有注册的服务描述
    private readonly List<ServiceDescriptor> _services = new();

    #region 1. 服务注册方法
    // 注册单例服务
    public void AddSingleton<TService, TImplementation>() where TImplementation : TService
    {
        _services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), ServiceLifetime.Singleton));
    }
    // 注册瞬时服务
    public void AddTransient<TService, TImplementation>() where TImplementation : TService
    {
        _services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), ServiceLifetime.Transient));
    }
    #endregion

    #region 2. 服务解析核心方法
    /// <summary>
    /// 根据接口类型获取服务实例
    /// </summary>
    public TService GetService<TService>()
    {
        return (TService)GetService(typeof(TService));
    }

    /// <summary>
    /// 递归解析服务:自动创建对象+注入依赖
    /// </summary>
    private object GetService(Type serviceType)
    {
        // 1. 找到注册的服务描述
        var descriptor = _services.FirstOrDefault(s => s.ServiceType == serviceType);
        if (descriptor == null)
            throw new Exception($"未注册服务:{serviceType.Name}");

        // 2. 如果是单例且已创建,直接返回缓存实例
        if (descriptor.Lifetime == ServiceLifetime.Singleton && descriptor.ImplementationInstance != null)
            return descriptor.ImplementationInstance;

        // 3. 找到实现类的第一个构造函数
        var constructor = descriptor.ImplementationType.GetConstructors().First();
        // 4. 递归解析构造函数的所有参数依赖
        var parameters = constructor.GetParameters();
        var parameterInstances = new object[parameters.Length];
        for (int i = 0; i < parameters.Length; i++)
        {
            // 递归调用:自动创建参数对象
            parameterInstances[i] = GetService(parameters[i].ParameterType);
        }

        // 5. 利用反射创建服务实例
        var instance = Activator.CreateInstance(descriptor.ImplementationType, parameterInstances);

        // 6. 如果是单例,则缓存实例
        if (descriptor.Lifetime == ServiceLifetime.Singleton)
            descriptor.ImplementationInstance = instance;

        return instance;
    }
    #endregion
}

4. 测试我们手写的容器

现在,使用我们刚实现的 MiniContainer 来运行之前的示例:

class Program
{
    static void Main(string[] args)
    {
        // 1. 创建DI容器
        var container = new MiniContainer();
        // 2. 注册服务:接口→实现+生命周期
        container.AddSingleton<IUserRepository, UserRepository>();
        container.AddTransient<IUserService, UserService>();
        // 3. 解析服务:容器自动创建对象+注入依赖
        var userService = container.GetService<IUserService>();
        userService.AddUser(); // 输出:数据库新增用户

        // 验证:瞬时服务每次创建新对象
        var userService2 = container.GetService<IUserService>();
        Console.WriteLine(object.ReferenceEquals(userService, userService2)); // 输出:False
    }
}

运行结果:

数据库新增用户
False

✅ 成功!我们没有在任何地方手动 new UserRepository()new UserService(),容器自动完成了依赖的解析和注入,完美实现了代码解耦。

四、回归实战:.NET 原生DI容器应用

理解原理后,在实际开发中,我们直接使用功能更完善、经过充分测试的微软官方DI容器(ASP.NET Core内置,也可用于控制台程序)。

1. 三种生命周期及使用场景

注册方法 生命周期 典型使用场景
AddSingleton 单例 工具类、配置类、全局共享的缓存服务
AddScoped 作用域 Web请求上下文、数据库上下文(DbContext)
AddTransient 瞬时 轻量级、无状态的服务

2. 在控制台项目中使用

首先,通过NuGet安装必要的包:

Install-Package Microsoft.Extensions.DependencyInjection

然后,编写使用代码:

using Microsoft.Extensions.DependencyInjection;

class Program
{
    static void Main(string[] args)
    {
        // 1. 创建服务集合(即DI容器配置)
        var services = new ServiceCollection();
        // 2. 注册服务
        services.AddSingleton<IUserRepository, UserRepository>();
        services.AddTransient<IUserService, UserService>();
        // 3. 构建服务提供程序(即容器本体)
        var serviceProvider = services.BuildServiceProvider();
        // 4. 解析并使用服务
        var userService = serviceProvider.GetRequiredService<IUserService>();
        userService.AddUser();
    }
}

3. 在ASP.NET Core中使用(最常用场景)

Program.cs 中,注册服务变得异常简单:

var builder = WebApplication.CreateBuilder(args);

// 注册服务:一行代码完成解耦配置
builder.Services.AddSingleton<IUserRepository, UserRepository>();
builder.Services.AddTransient<IUserService, UserService>();

var app = builder.Build();
app.Run();

在控制器中,框架会自动通过构造函数完成依赖注入:

public class UserController : Controller
{
    private readonly IUserService _userService;
    // 框架自动注入已注册的IUserService实现,无需手动new
    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    public IActionResult Add()
    {
        _userService.AddUser();
        return Ok("新增成功");
    }
}

五、核心总结与避坑指南

核心精髓

  1. 依赖抽象,而非具体:用接口定义行为,用实现类完成功能,这是解耦的基石。
  2. 容器管理,解放双手:遵循“注册-解析”两步走模式,对象的创建和依赖注入由容器自动完成。
  3. 生命周期是关键:深刻理解单例(Singleton)、作用域(Scoped)和瞬时(Transient)的区别与应用场景,是正确使用DI的保证。

开发避坑指南

  1. 避免生命周期错乱:切勿在单例服务中注入瞬时或作用域服务,这可能导致后者被意外提升为单例,引发内存泄漏或状态混乱。
  2. 首选构造函数注入:这是最明确、最安全的注入方式。属性注入和方法注入应仅作为补充手段在特定场景下使用。
  3. 先注册,后解析:所有服务必须在 BuildServiceProvider() 之前完成注册,否则解析时会抛出 InvalidOperationException
  4. DbContext使用Scoped:在ASP.NET Core中,Entity Framework Core的 DbContext 应使用 AddScoped 注册,以确保在一个Web请求内使用同一个连接实例。

结语

通过从零手写一个DI容器,我们深入剖析了其“注册映射、递归解析、生命周期管理”三大核心机制。依赖注入并非一种复杂晦涩的设计模式,而是一种能让代码变得更优雅、更易于维护和测试的最佳工程实践。

在C#与.NET开发中,熟练掌握依赖注入是迈向高质量架构设计的第一步。希望本文的讲解与实战,能帮助你彻底告别“硬编码 new 对象”的强耦合时代,写出更加清晰、灵活且健壮的应用。




上一篇:Anthropic训练或现架构突破,Mythos模型性能超预期两倍引业界震动
下一篇:DeepSeek服务中断12小时背后:宕机原因分析与连锁反应
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-31 07:02 , Processed in 0.525059 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表