C#依賴注入(DI)簡析--我的理解


如題。

為什么要依賴注入,簡言之為了解耦。

對一些概念做一些拆解,網上的說法一鍋粥,容易糊塗。

依賴:

一個人類,每個人出來就應該有100塊錢。直覺上,會這么寫(別去糾結錢類是啥):

internal class 人
    {
        錢 _錢;
        public 人()
        {
            _錢 = new 錢("一百塊");
        }
    }

這段邏輯里,人類對錢類產生了依賴,即:沒有錢就不是人。

這么寫沒問題,但先進的編程理念告訴我們:類應當盡量封閉,不與外界相關。即:人類應該盡量關心自己的事,少去“掙錢”(即new 錢),這是不務正業。

那么沒錢又不行,不掙怎么辦呢?答案:直接拿。

注入:

看下面的代碼

internal class 人
    {
        錢 _錢;
        public 人(錢 一筆錢)
        {
            _錢 = 一筆錢;
        }
    }

在這里,人類不new錢,即不掙錢,也就不會不務正業。需要的錢,在構造方法里,從外界獲得。這個動作,就是注入(外界給人注入錢)。構造方法的參數,就是注入點。

獲得錢的方式從類內到了類外,這就是控制反轉(IoC)。

注入的好處:人類更單純了,給錢辦事。原先需要掙的錢,如果從人民幣變成美刀,需要修改類代碼。現在,看老板怎么給,給啥都行,我不操心。

容器:

通過注冊的方式,可以列張表,說明什么類對象用什么實現。

例如:有個接口叫“報酬”,錢類、物品類、以身相許類都實現這個接口。

我想以后只要提到“報酬”就表示“以身相許”。那么表里就應該是:

類型 服務
報酬 以身相許

這個表就是容器,它管理一系列對象的關系,甚至可以創建這些對象。我們可以這樣用:

報酬 a=容器.給個對象<報酬>();

這時候容器會創建一個“以身相許”的類對象賦值給a。

 

假設人類變成這樣:

internal class 人
    {
        報酬 _報酬;
        public 人(報酬 一筆報酬)
        {
            _報酬 = 一筆報酬;
        }
    }

而表格里增加一行:

類型 服務
報酬 以身相許

這時候想要獲得一個人類的對象,可以寫成:

var a=容器.給個對象<人>();

容器在構造人的時候發現還需要報酬,會根據表,自動傳遞一個“以身相許”的對象過去。

以后你想修改程序,把“報酬”變成“物品”,只需要修改這張表(容器)就行了。所有注冊了的,使用了“報酬”做構造參數的地方就全都改過來了。很省事。

 小結:

依賴是客觀存在的。注入可以讓類更封閉。容器可以對注入進行管理。

也許會出現容器管不了/管不到的注入,不必強求,離了筷子一樣吃東西。


 控制台下做了些驗證性的例子,想用最簡單的代碼說清楚它。

概述:

接口Person,派生類Chinese和American。一個應用類Test。自己寫了個做服務的類“MyService”,利用“ServiceCollection”類的對象注冊(也就是描述接口和類的對應關系),並通過它的“BuildServiceProvider()”方法來生成“IServiceProvider”對象"provider"(即容器)。后續創建實例都是通過“provider”來完成。

完成以上操作后,通過容器初始化Test對象時,參數里的幾個類,容器會自動生成。如需更改實現,修改注冊內容即可。

前提:通過nuget下載“Microsoft.Extensions.DependencyInjection”包。

Person.cs:

internal interface Person:IDisposable
    {
        void showMe();
    }

Chinese.cs:

internal class Chinese:Person
    {
        int c = 0;
        public string Name { get; set; }
        public Chinese()
        {
            Name = "無名氏";
        }
        public Chinese(string name)
        {
            Name = name;
        }
        public void Dispose()
        {
            Console.WriteLine("中國人對象銷毀");
        }

        public void showMe()
        {
            Console.WriteLine($"{Name}:你好,{c++}");
        }
    }

兩個構造函數是為了演示有參/無參注冊和使用DI。

American.cs:

internal class American:Person
    {
        int c = 0;
        public void Dispose()
        {
            Console.WriteLine("American is disposed.");
        }
        public void showMe()
        {
            Console.WriteLine($"hello!{c++}");
        }
    }

Test.cs:

internal class Test
    {
        Person person;
        Chinese chinese;
        American american;
        public Test(Person p,Chinese c,American a)
        {
            person = p; 
            chinese = c;
            american = a;
        }
        public void showTest()
        {
            person.showMe();
            chinese.showMe();
            american.showMe();
        }
    }

MyService.cs:

 1 internal class MyService
 2     {
 3         static IServiceProvider provider=getMyService();
 4         public static IServiceProvider getMyService()
 5         {
 6             var services = new ServiceCollection();
 7             services.AddSingleton<Test>();
 8             //services.AddSingleton<Person,Chinese>();
 9             services.AddSingleton<Chinese>();
10             services.AddSingleton(new American());
11             services.AddTransient<Person>(sp=>new Chinese("張三"));
12             return services.BuildServiceProvider();
13         }
14         public static T? getInstance<T>()
15         {
16             return provider.GetService<T>();
17         }
18 
19         public void Dispose()
20         {
21             Console.WriteLine("MyService is disposed.");
22         }
23     }

可以用第8行取代第11行查看效果。第16行,如果使用GetServices<T>()可以返回一個IEnumerable類型的集合。tolist的話,可以提取重復注冊的幾個類對象。

一個泛型表示注冊這個類,兩個表示后面的類實現前面的接口。

對於三個作用域,AddTransient每次都是新對象;AddSingleton從頭到尾都是一個對象;AddScoped一般用於web請求,在當前請求有效。

微軟官方說法如下:


暫時生存期服務是每次從服務容器進行請求時創建的。 這種生存期適合輕量級、 無狀態的服務。 向 AddTransient 注冊暫時性服務。

在處理請求的應用中,在請求結束時會釋放暫時服務。

范圍內
對於 Web 應用,指定了作用域的生存期指明了每個客戶端請求(連接)創建一次服務。 向 AddScoped 注冊范圍內服務。
在處理請求的應用中,在請求結束時會釋放有作用域的服務。
使用 Entity Framework Core 時,默認情況下 AddDbContext 擴展方法使用范圍內生存期來注冊 DbContext 類型。

創建單例生命周期服務的情況如下:
在首次請求它們時進行創建;或者
在向容器直接提供實現實例時由開發人員進行創建。 很少用到此方法。

來自依賴關系注入容器的服務實現的每一個后續請求都使用同一個實例。 如果應用需要單一實例行為,則允許服務容器管理服務的生存期。 不要實現單一實例設計模式,或提供代碼來釋放單一實例。 服務永遠不應由解析容器服務的代碼釋放。 如果類型或工廠注冊為單一實例,則容器自動釋放單一實例。
向 AddSingleton 注冊單一實例服務。 單一實例服務必須是線程安全的,並且通常在無狀態服務中使用。
在處理請求的應用中,當應用關閉並釋放 ServiceProvider 時,會釋放單一實例服務。 由於應用關閉之前不釋放內存,因此請考慮單一實例服務的內存使用。


主程序:

using ConsoleApp1;

Test? t= MyService.getInstance<Test>();
t?.showTest();

也可用下面的主程序調試:

 1 using ConsoleApp1;
 2 
 3 //using (var p = MyService.getInstance<Person>())
 4 //{
 5 //    p.showMe();
 6 //}
 7 //以上寫法給出作用域,可執行/檢驗銷毀(dispose)方法。
 8 
 9 for (int i = 0; i < 3; i++)
10 {
11     var p = MyService.getInstance<Person>();
12     p?.showMe();//"?."先檢查p是否為空,是則不進行后續操作,返回null
13 }

 


在.net8 core mvc中,可以在Program.cs中注冊依賴注入。

以下例子演示了有效期為當前請求(scoped)的注入,和在過濾器(filter)中使用注入的方式。

MyMsg.cs

namespace WebApplication2.MyCode
{
    public class MyMsg
    {
        public string? xm { get; set; } = "xxx";
        public int nl { get; set; } = -1;
        public override string ToString()
        {
            return $"xm:{xm},nl:{nl}";
        }
    }
}

MyFilter.cs

using Microsoft.AspNetCore.Mvc.Filters;

namespace WebApplication2.MyCode
{
    public class MyFilter:ActionFilterAttribute
    {
        public MyMsg myMsg { get; set; }
        public MyFilter(MyMsg _mymsg)
        {
            myMsg = _mymsg;
        }
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var t1 = context.HttpContext.Request.Query["xm"].ToString();
            var t2 = context.HttpContext.Request.Query["nl"].ToString();
            myMsg.xm = t1 == "" ? "NoName" : t1;
            myMsg.nl = t2 == "" ? 0 : int.Parse(t2);
            base.OnActionExecuting(context);
        }
    }
}

 

Program.cs

 1 var builder = WebApplication.CreateBuilder(args);
 2 
 3 // Add services to the container.
 4 builder.Services.AddControllersWithViews();
 5 builder.Services.AddScoped<MyMsg>();
 6 
 7 var app = builder.Build();
 8 
 9 // Configure the HTTP request pipeline.
10 if (!app.Environment.IsDevelopment())
11 {
12     app.UseExceptionHandler("/Home/Error");
13 }
14 app.UseStaticFiles();
15 
16 app.UseRouting();
17 
18 app.UseAuthorization();
19 
20 app.MapControllerRoute(
21     name: "default",
22     pattern: "{controller=Home}/{action=Index}/{id?}");
23 
24 app.Run();

其中除了第5行,都是自動生成的。

控制器HomeController.cs部分代碼:

 1  public class HomeController : Controller
 2  {
 3      private readonly ILogger<HomeController> _logger;
 4 
 5      public MyMsg msg { get; set; }
 6 
 7      public HomeController(ILogger<HomeController> logger,MyMsg _msg)
 8      {
 9          _logger = logger;
10          msg= _msg;
11      }
12      [TypeFilter<MyFilter>]
13      public IActionResult Index()
14      {
15          ViewData["msg"] = msg.ToString();
16          return View();
17      }
18 
19      public IActionResult Privacy()
20      {
21          ViewData["msg"] = msg.ToString();
22          return View();
23      }

23行后面的內容都沒動,略。

5行增加控制器屬性。

7-11行使用注冊內容,為當前請求匹配msg。

12行過濾器,無法直接使用“[MyFilter]”來依賴注入,會報構造函數錯誤。必須用“TypeFilter”或"ServiceFilter"來曲線調用。

  其中,ServiceFilter必須通過在容器中注冊過濾器(此處為MyFilter)來使用,TypeFilter可以不注冊,默認應該是短暫“Transient”(未驗證,此處不影響)。

  詳見官方文檔

群友“那啥...(QQ:821103266)”提出,可以利用“context.HttpContext.RequestServices”,在MyFilter的方法中,直接獲得MyMsg實例,對myMsg初始化。應該可行,未測試。感興趣的話,自行研究。

index.cshtml包含如下代碼:

<p>@ViewData["msg"]Learn about

privacy.cshtml類似

運行結果:

 

不要去糾結WebApplication1還是2,之前編輯的時候死機了。

可以看出,能由依賴注入的請求(scoped)級對象,在過濾器和動作之間傳遞內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM