了解依賴注入生命周期:單例、範圍和瞬態的詳細示例

開發者使用依賴注入時可能意識到服務實例的生命週期,但很多人並未完全掌握它是如何工作的。你可以在网上找到許多文章來解釋這些概念,但它們經常只是重複你可能已經知道的定義。讓我用手繪圖例來簡化解釋。

當實現依賴注入時,開發者有三個選擇決定實例的生命週期:

  1. 單例
  2. 範圍
  3. 暫存

雖然大部分開發者認識這些詞彙,但有不少人在為服務生命週期選擇哪個選項上有困難。

定義

讓我從定義開始:

  • 單例生命週期的服務實例是從服務容器中一次創建一個應用程序。一個實例將服务于所有隨後的請求。單例服務在應用程序結束時 (即,在應用程序重新啟動時) 被消毀。
  • 暫存生命週期的服務實例是從服務容器中每個請求創建的。暂存服務在請求結束時被消毀。
  • 範圍生命週期的服務實例是每次客戶請求一次從服務容器中創建。暂存服務在請求結束時被消毀。

使用時機

  • 單例 – 當你希望在應用程式的整個生命週期內使用服務的單一實例時
  • 短暫的 – 當你希望在每个客戶端請求內使用服務的單一實例時
  • 範圍 – 當你希望為每個請求使用服務的單一實例時

什麼是客戶端請求?用非常簡單的話來說,你可以將其視為一個API/REST呼叫,用戶通過按鈕點擊將其傳送到你的應用程式以獲得響應。

別擔心,讓我們通過一個例子來理解。

例子

首先,讓我們創建接口/服務和類別:

C#

 

	// 我們在下面宣告了3個服務
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

現在讓我們為每個服務接口/服務寫实的現行。我們將通過嘗試更新callMeSingletoncallMeTransientcallMeScoped變數來理解這個概念。

  • 單例類別實現:
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// 其他實現
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// 其他實現
}

  • 短暫類別實現:
C#

 

class TransientImplementation: ITransient 
{
	var callMeTransient = ""
	
	// 其他實現
	public SetTransient(string value)
	{
		callMeTransient = value;
	}
	// 其他實現
}

  • 範圍類別實現:
C#

 

class ScopedImplementation: IScoped 
{
	var callMeScoped = ""
			
	//其他實現
	public SetScoped(string value)
	{
		callMeScoped = value;
	}
	//其他實現		
}

讓我們用DI(依賴注入)來註冊(ConfigureServices)以決定每個服務實例的生命週期:

C#

 

services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();

我們將從三個不同的類別(ClassAClassBClassC)呼叫這些服務以了解每個服務的生命週期:

  • ClassA
C#

 

public class ClassA
{
	private ISingleton _singleton;
	// constructor to instantiate 3 different services we creates
	public ClassA(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassA()
	{
		_singleton.SetSingleton("I am from ClassA");
	} 
	
  	public void UpdateTransientFromClassA()
    {
		_transient.SetTransient("I am from ClassA");
	} 
	
  	public void UpdateScopedFromClassA()
	{
		_scoped.SetScoped("I am from ClassA");
	}

	// other implementation 
}

  • ClassB
C#

 

public class ClassB
{
	private ISingleton _singleton;
	// constructor to instantiate 3 different services we creates
	public ClassB(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassB()
	{
		_singleton.SetSingleton("I am from ClassB");
	} 
	
  	public void UpdateTransientFromClassB()
    {
		_transient.SetTransient("I am from ClassB");
	} 
	
  	public void UpdateScopedFromClassB()
	{
		_scoped.SetScoped("I am from ClassB");
	}

	// other implementation 
}

  • ClassC
C#

 

public class ClassC
{
	private ISingleton _singleton;
	// constructor to instantiate 3 different services we creates
	public ClassC(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassC()
	{
		_singleton.SetSingleton("I am from ClassC");
	} 
	
  	public void UpdateTransientFromClassC()
    {
		_transient.SetTransient("I am from ClassC");
	} 
	
  	public void UpdateScopedFromClassC()
	{
		_scoped.SetScoped("I am from ClassC");
	}

	// other implementation 
}

析論

讓我們分析上面實作中每個生命週期的結果和行為:

Singleton

所有類別(ClassAClassBClassC)在應用程序的生命週期內都会使用 SingletonImplementation 類的同一單一實例。這意味著 SingletonImplementation 類的屬性、字段和操作將被所有呼叫類使用的實例共享。對屬性或字段的任何更新將覆蓋先前的更改。

例如,在上述代碼中,`ClassA`、`ClassB` 和 `ClassC` 都使用了 `SingletonImplementation` 服務作為單例實例,並調用 `SetSingleton` 來更新 `callMeSingleton` 變量。在這種情況下,所有嘗試访问此屬性的請求將共享 `callMeSingleton` 變量的單一值。無論哪個類最後訪問它來更新,都將覆蓋 `callMeSingleton` 的值。`

  • ClassA` – 對於 `TransientImplementation` 服務,它將與其他類共享相同的實例。`
  • ClassB` – 對於 `TransientImplementation` 服務,它將與其他類共享相同的實例。`
  • ClassC` – 對於 `TransientImplementation` 服務,它將與其他類共享相同的實例。`

ClassA`、`ClassB` 和 `ClassC` 正在更新 `SingletonImplementation` 類的同一實例,這將覆蓋 `callMeSingleton` 的值。因此,在設定或更新單例服務實現中的屬性時要小心。`

` 單例服務在應用程序結束時被處置(即在應用程序重新啟動時)。`

Transient`

所有類別 (`ClassA`、`ClassB` 和 `ClassC`) 將使用它們各自的 `TransientImplementation` 類別實例。這意味著如果一個類別呼叫 `TransientImplementation` 類別的屬性、字段或操作,它只会更新或覆寫其個別實例值。任何對屬性或字段的更新都不在其他 `TransientImplementation` 實例之間共享。

讓我們來了解:

  • ClassA – 它將有自己的 `TransientImplementation` 服務實例。
  • ClassB – 它將有自己的 `TransientImplementation` 服務實例。
  • ClassC – 它將有自己的 `TransientImplementation` 服務實例。

假設您有一個 `ClassD`,它正在從 `ClassA`、`ClassB` 和 `ClassC` 實例呼叫瞬態服務。在這種情況下,每個類別實例將被當作不同的/單獨的實例處理,每個類別將有自己的 `callMeTransient` 值。閱讀下面的內嵌註釋以了解 `ClassD`:

C#

 

public ClassD
{
	// 其他實作
		
    // 以下程式碼會將 callMeTransient 的值更新為 "I am from ClassA" ,僅僅對 ClassA 的實例產生影響。
    // 而且它不會被 Class B 或 B 類的任何後續呼叫所更改。
	ClassA.UpdateTransientFromClassA(); 		
       
    // 以下程式碼會將 callMeTransient 的值更新為 "I am from ClassB" ,僅僅對 ClassB 的實例產生影響。
    // 它不會覆寫 ClassA 實例的值,也不会被 Class C 的後續呼叫所更改。
	ClassB.UpdateTransientFromClassB(); 
    
    // 以下程式碼會將 callMeTransient 的值更新為 "I am from ClassC" ,僅僅對 ClassC 的實例產生影響。
    // 它不會覆寫 ClassA 和 ClassB 實例的值,也不会被其他類的後續呼叫所更改。
    ClassC.UpdateTransientFromClassC(); 

	// 其他實作
}

Transient 服務在每個請求結束時被釋放。當您希望在請求內達到無狀態行為時,請使用 Transient。

Scoped

所有類(ClassAClassBClassC)對於每個請求都会使用 ScopedImplementation 類的單一實例。這意味著在 ScopedImplementation 類上對屬性/字段/操作的調用將發生在請求範圍內的一個實例上。屬性/字段的任何更新都將影響其他類。  

讓我們來了解:

  • ClassA – 它將具有其 TransientImplementation 服務的實例。
  • ClassB – 它將具有與ClassA相同的TransientImplementation服務實例。
  • ClassC – 它將具有與ClassAClassB相同的TransientImplementation服務實例。

假設您有一個ClassD,它從ClassAClassBClassC實例中調用範圍內的服務。在這種情況下,每個類都將具有ScopedImplementation類的单個實例。以下是對ClassD的內聯注釋進行閱讀。

C#

 

public class ClassD
{
  // 其他實作
 
  // 下列代碼將將callMeScoped的值更新為"我來自ClassA"對於ClassA實例
  // 但由於它是一個範圍生命週期,所以它持有單個實例ScopedImplementation
  // 然後它可以被ClassB或ClassC下一次呼叫覆寫
	ClassA.UpdateScopedFromClassA();  
  
  // 下列代碼將將callMeScoped的值更新為對於單個實例ScopedImplementation"我來自ClassB" 
  // 它將也覆寫對於classA實例的callMeScoped值。 
	ClassB.UpdateScopedFromClassB();
  
  // 現在如果Class A將對ScopedImplementation執行任何操作,
  // 它將使用最新覆寫classB的屬性/字段值。
	
  // 下列代碼將將callMeScoped的值更新為"我來自ClassC"
  // 它將也覆寫對於classA和ClassB實例的callMeScoped值。
	ClassC.UpdateScopedFromClassC(); 
  // 現在如果Class B或Class A將對ScopedImplementation執行任何操作,它將使用最新覆寫classC的屬性/字段值。

    // 其他實作
}

範圍服務在每個請求結束時被釋放。當你希望在單個請求之間的無狀態行為時使用Scoped。

小知識時間

服務的生命週期可以被父服務覆寫,在它被初始化時。混淆了嗎?讓我解釋:

讓我們取材於上方示例中的同一堂課,並從 SingletonImplementation (這是一個單例)初始化 Transient 和 Scoped 服務。這將初始化 ITransientIScoped 服務,並將這些服務的生命週期覆寫為單例生命週期作為父服務。在這種情況下,您的應用程序將不會有任何 Transient 或 Scoped 服務(假設您只使用我們在示例中使用的這3種服務)。

閱讀下面的代碼行:

C#

 

public class SingletonImplementation: ISingleton
{
	// 構造函數以添加初始化服務。
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // 現在 _transient 將表现为單例服務,無論它是如何作為 Transient 註冊的
		_scoped = scoped;
        // 現在 scoped 將表现为單例服務,無論它是否作為 Scoped 註冊
	}
    var callMeSingleton = ""
		
	// 其他實現
}

總結

我希望上述文章對理解該主題有幫助。我建議您在上述上下文中自己試一試,您將永遠不再混亂。單例是最容易理解的,因為一旦您創建了其實例,它將會在整個應用程序的 應用程序生命週期 內共享。與單例相似,Scoped 實例模擬相同行為,但僅限於應用程序內的一個請求生命週期內。Transient 完全無狀態,對於每個請求和每個類實例將保留其自己的服務實例。

Source:
https://dzone.com/articles/understanding-the-dependency-injection-lifecycle