理解依赖注入生命周期:单例、范围和瞬态的详细示例

开发者在使用依赖注入时可能知道服务实例的生命周期,但很多人并不完全理解它是如何工作的。网络上有很多文章解释这些概念,但它们通常只是重复你可能已经知道的定义。让我用一个详细的例子来简化解释。

在实现依赖注入时,开发者有三种选择来决定实例的生命周期:

  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>();

让我们从3个不同的类(ClassAClassBClassC)中使用/调用这些服务来了解每个服务的生活周期:

  • ClassA
C#

 

public class ClassA
{
	private ISingleton _singleton;
	// 构造函数以实例化3个不同的服务
	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");
	}

	// 其他实现 
}

  • ClassB
C#

 

public class ClassB
{
	private ISingleton _singleton;
	// 构造函数以实例化3个不同的服务
	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");
	}

	// 其他实现 
}

  • ClassC
C#

 

public class ClassC
{
	private ISingleton _singleton;
	// 构造函数以实例化3个不同的服务
	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");
	}

	// 其他实现 
}

分析

让我们分析上述实现中每个生命周期的结果和行为:

单例

所有类(ClassAClassBClassC)在整个应用程序的生命周期中都将使用SingletonImplementation类的同一个实例。这意味着SingletonImplementation类的属性、字段和操作将被所有调用类使用的实例共享。对属性或字段的任何更新都将覆盖先前的更改。

例如,在上面的代码中,ClassAClassBClassC 都使用了 SingletonImplementation 服务作为单例实例,并调用 SetSingleton 来更新 callMeSingleton 变量。在这种情况下,所有尝试访问这个属性的请求都将共享 callMeSingleton 变量的同一个值。最后访问它的类将覆盖 callMeSingleton 的值。

  • ClassA – 它将与其他类共享相同实例的服务 TransientImplementation
  • ClassB – 它将与其他类共享相同实例的服务 TransientImplementation
  • ClassC – 它将与其他类共享相同实例的服务 TransientImplementation

ClassAClassBClassC 都在更新 SingletonImplementation 类的同一个实例,这将覆盖 callMeSingleton 的值。因此,在设置或更新单例服务实现中的属性时要小心。

单例服务在应用程序结束时被销毁(即在应用程序重启时)。

Transient

所有类(ClassAClassBClassC)都将使用TransientImplementation类的各自实例。这意味着,如果一个类调用TransientImplementation类的属性、字段或操作,它只会更新或覆盖其各自的实例值。对属性或字段的任何更新都不会在其他TransientImplementation实例之间共享。

来理解一下:

  • ClassA – 它将拥有TransientImplementation服务的一个实例。
  • ClassB – 它将拥有TransientImplementation服务的一个实例。
  • ClassC – 它将拥有TransientImplementation服务的一个实例。

假设您有一个ClassD类,它正在从ClassAClassBClassC 实例调用瞬态服务。在这种情况下,每个类实例都将被视为不同的/独立的实例,每个类都将有自己的callMeTransient值。阅读下面的内联注释了解ClassD

C#

 

public ClassD
{
	// 其他实现
		
    // 下面的代码将仅更新ClassA实例中callMeTransient的值为"I am from ClassA"
    // 并且它不会被Class B或B类的后续调用更改
	ClassA.UpdateTransientFromClassA(); 		
       
    // 下面的代码将仅更新ClassB实例中callMeTransient的值为"I am from ClassB"
    // 并且它不会覆盖ClassA实例的值,也不会被Class C的后续调用更改
	ClassB.UpdateTransientFromClassB(); 
    
    // 下面的代码将仅更新ClassC实例中callMeTransient的值为"I am from ClassC"
    // 并且它不会覆盖ClassA和ClassB实例的值,也不会被任何其他类的后续调用更改
    ClassC.UpdateTransientFromClassC(); 

	// 其他实现
}

每次请求结束时,Transient服务都会被销毁。当您希望在请求内实现无状态行为时,请使用Transient。

作用域

所有类(ClassAClassBClassC)将为每个请求使用ScopedImplementation类的单实例。这意味着在ScopedImplementation类上对属性/字段/操作的调用将在请求的作用域内单实例上进行。任何属性的更新/字段都将与其他类共享。  

让我们理解:

  • ClassA – 它将拥有TransientImplementation服务的一个实例。
  • ClassB – 它将拥有与ClassA相同的TransientImplementation服务实例。
  • ClassC – 它将拥有与ClassAClassB相同的TransientImplementation服务实例。

假设你有一个ClassD,它从ClassAClassBClassC实例中调用作用域服务。在这种情况下,每个类都将有ScopedImplementation类的单个实例。下面是关于ClassD的内部注释。

C#

 

public class ClassD
{
  // 其他实现
 
  // 下面的代码将更新callMeScoped的值,对于ClassA的实例,将其更新为"I am from ClassA"
  // 但由于它是作用域生命周期,所以它持有单个实例的ScopedImplementation
  // 然后它可以被ClassB或ClassC下一次调用所覆盖
	ClassA.UpdateScopedFromClassA();  
  
  // 下面的代码将更新single instance ScopedImplementation的callMeScoped值,变为"I am from ClassB" 
  // 并且它也会覆盖掉classA实例的callMeScoped值。 
	ClassB.UpdateScopedFromClassB();
  
  // 现在,如果Class A对ScopedImplementation执行任何操作,
  // 它将使用由classB覆盖的最新属性/字段值。
	
  // 下面的代码将更新callMeScoped的值,变为"I am from ClassC"
  // 并且它也会覆盖掉classA和ClassB实例的callMeScoped值。
	ClassC.UpdateScopedFromClassC(); 
  // 现在,如果Class B或Class A对ScopedImplementation执行任何操作,它将使用由classC覆盖的最新属性/字段值。

    // 其他实现
}

作用域服务在每个请求结束时被销毁。当你希望在单个请求之间保持无状态行为时,请使用作用域。

趣味时间

服务的生命周期可以被其父服务覆盖,其中它被初始化。困惑吗?让我解释:

让我们以上面的类为例,初始化从SingletonImplementation(这是一个单例)中获取的Transient和Scoped服务。这将初始化ITransientIScoped服务,并将这些服务的生活周期重写为单例生命周期作为父服务。在这种情况下,您的应用程序将不具有任何Transient或Scoped服务(假设您只有我们在示例中使用的这三个服务)。

阅读下面代码中的行:

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 = ""
		
	// 其他实现
}

总结

我希望上面的文章在理解这个主题方面对你有所帮助。我建议您根据上面设定的上下文自己尝试一下,这样您就永远不会再感到困惑了。Singleton是最容易理解的,因为一旦您创建了它的实例,它将在整个应用程序的生命周期内被共享。在Singleton的同一线上,Scoped实例模仿相同的行为,但仅限于应用程序请求的生命周期内。Transient是完全无状态的,对于每个请求,每个类实例都将持有其自己的服务实例。

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