Lazy延迟初始化

从net 4.0开始,C#开始支持延迟初始化,通过Lazy关键字,可以声明某个对象在第一次使用的时候再初始化,如果一直没有调用,那就不初始化,省去了一部分不必要的开销,提升了效率,同时Lazy是天生线程安全的。

场景

  • 对象创建成本高且程序可能不会使用它。

    例如,假定内存中有具有 Orders 属性的 Customer 对象,该对象包含大量 Order 对象,初始化这些对象需要数据库连接。 如果用户不需要显示 Orders 或在计算中使用该数据,则无需使用系统内存或计算周期来创建它。 通过使用 Lazy<Orders> 来声明 Orders 对象用于迟缓初始化,可以避免在不使用该对象时浪费系统资源。

  • 对象创建成本高,且希望将其创建推迟到其他高成本操作完成后。

    例如,假定程序在启动时加载多个对象实例,但是只需立即加载其中一部分。 可以通过推迟初始化不需要的对象,直到创建所需对象,提升程序的启动性能。

建议使用 LazyLazy 及其相关的类型支持线程安全并提供一致的异常传播策略。

延迟初始化 - .NET Framework | Microsoft Learn

image-20231031093457833

使用

默认初始化

在使用Lazy时,如果没有在构造函数中传入委托,则在首次访问值属性时,将会使用Activator.CreateInstance来创建类型的对象,如果此类型没有无参数的构造函数时将会引发运行时异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;

namespace LazyUsage
{
class LazyDemo
{
static void Main()
{
Lazy<Data> lazyData = new Lazy<Data>();
Console.WriteLine("Main->is lazyData Initialized? value = " + lazyData.IsValueCreated);
lazyData.Value.Print();//此处访问时才会将Data真正的初始化
Console.WriteLine("Main->is lazyData Initialized? value = " + lazyData.IsValueCreated);

Console.ReadKey();
}
}

class Data
{
public Data()
{
Console.WriteLine("Data::.ctor->Initialized");
}

public void Print()
{
Console.WriteLine("Data::Print->println");
}
}
}

执行结果:

1
2
3
4
Main->is lazyData Initialized? value = False
Data::.ctor->Initialized
Data::Print->println
Main->is lazyData Initialized? value = True

委托初始化

指定委托来初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;

namespace LazyUsage
{
class LazyDemo
{
static void Main()
{
//指定委托来初始化Data
Lazy<Data> lazyData = new Lazy<Data>(
() =>
{
Console.WriteLine("Main->lazyData will be Initialized!");
return new Data("Test");
});
Console.WriteLine("Main->is lazyData Initialized? value = " + lazyData.IsValueCreated);
lazyData.Value.Print();
Console.WriteLine("Main->is lazyData Initialized? value = " + lazyData.IsValueCreated);


Console.ReadKey();
}
}

class Data
{
public string Name { get; private set; }

public Data(string name)
{
Name = name;
Console.WriteLine("Data::.ctor->Initialized,name = "+name);
}

public void Print()
{
Console.WriteLine("Data::Print->name = " + Name);
}
}
}

执行结果:

1
2
3
4
5
Main->is lazyData Initialized? value = False
Main->lazyData will be Initialized!
Data::.ctor->Initialized,name = Test
Data::Print->name = Test
Main->is lazyData Initialized? value = True

线程安全初始化

默认情况下,Lazy 对象是线程安全的。也就是说,如果构造函数没有指定线程安全性的类型,该函数创建的 Lazy 对象是线程安全的。

在多线程方案中,访问线程安全 Lazy 对象的 Value 属性的第一个线程会为所有线程上的所有后续访问对其初始化,且所有线程共享相同的数据。 因此,哪个线程初始化对象并不重要,争用条件是良性的。

以下示例演示了相同的 Lazy<int> 实例对于三个单独的线程输出相同的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Initialize the integer to the managed thread id of the
// first thread that accesses the Value property.
Lazy<int> number = new Lazy<int>(() => Thread.CurrentThread.ManagedThreadId);

Thread t1 = new Thread(() => Console.WriteLine("number on t1 = {0} ThreadID = {1}",
number.Value, Thread.CurrentThread.ManagedThreadId));
t1.Start();

Thread t2 = new Thread(() => Console.WriteLine("number on t2 = {0} ThreadID = {1}",
number.Value, Thread.CurrentThread.ManagedThreadId));
t2.Start();

Thread t3 = new Thread(() => Console.WriteLine("number on t3 = {0} ThreadID = {1}", number.Value,
Thread.CurrentThread.ManagedThreadId));
t3.Start();

// Ensure that thread IDs are not recycled if the
// first thread completes before the last one starts.
t1.Join();
t2.Join();
t3.Join();

执行结果:

1
2
3
4
5
6
/* Sample Output:
number on t1 = 11 ThreadID = 11
number on t3 = 11 ThreadID = 13
number on t2 = 11 ThreadID = 12
Press any key to exit.
*/

如果每个线程需要单独的数据,使用 ThreadLocal 类型

Lazy.Value

Lazy对象创建后,并不会立即创建对应的对象,只有在变量的Value属性被首次访问时才会真正的创建,同时会将其缓存到Value中,以便将来访问。

Value属性是只读的,也就意味着如果Value存储了引用类型,将无法为其分配新对象,只可以更改此对象公共的属性或者字段等,如果Value存储的是值类型,那么就不能修改其值了,只能通过再次调用变量的函数使用新的参数来创建新的变量。

在Lazy对象创建后,在首次访问变量的Value属性前。

延迟属性

要使用延迟初始化实现公共属性,则将该属性的支持字段定义为 Lazy,并从该属性的 get 访问器返回 Value 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Customer
{
private Lazy<Orders> _orders;

public string CustomerID {get; private set;}

public Customer(string id)
{
CustomerID = id;
_orders = new Lazy<Orders>(() =>
{
return new Orders(this.CustomerID);
});
}

public Orders MyOrders
{
get
{
return _orders.Value;
}
}
}

Lazy.Value中可以得知:Value的属性是只读的,所以示例中只提供了Get的访问器,并未提供Set的访问器。

如果需要支持读取与写入属性的话,则Set访问器必须创建一个新的Lazy对象,同时必须编写自己的线程安全代码才能执行此操作。

循环依赖问题

在构建应用程序时,良好的设计应该避免服务之间的循环依赖,循环依赖是指某些组件直接或间接相互依赖。

比如下面这样:

Circular dependency

在.NET Core中使用依赖注入,如果产生循环依赖关系,则会报一下错误:

System.InvalidOperationException: A circular dependency was detected for the service of type ‘Demo.IA’

注入IServiceProvider

当应用复杂度达到一定程度时,很难避免造成服务循环依赖的问题,理想的情况下,应该是选择重构。

但是项目时间紧,任务重,没有时间重构代码

image-20231031100909845

我们可以通过注入IServiceProvider 去获取服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C : IC
{
private readonly IA _a;

public C(IA a)
{
_a = a;
}

public void Bar()
{
...
_a.Foo()
...
}
}

为了避免依赖性循环,可以注入 IServiceProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class C : IC
{
private readonly IServiceProvider _services;

public C(IServiceProvider services)
{
_services = services;
}

public void Bar()
{
...
var a = _services.GetRequiredService<IA>();
a.Foo();
...
}
}

这种方式有一定弊端,例如强制依赖IOC,并且很难看到类的依赖关系。

巧用 Lazy<T>

新建一个 IServiceCollection 的扩展类AddLazyResolution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static IServiceCollection AddLazyResolution(this IServiceCollection services)
{
return services.AddTransient(
typeof(Lazy<>),
typeof(LazilyResolved<>));
}

private class LazilyResolved<T> : Lazy<T>
{
public LazilyResolved(IServiceProvider serviceProvider)
: base(serviceProvider.GetRequiredService<T>)
{
}
}

Startup.cs中注册

1
services.AddLazyResolution();

在依赖的类中IA,注入Lazy,当要使用IA时,只需访问Lazy的值 Value 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C : IC
{
private readonly Lazy<IA> _a;

public C(Lazy<IA> a)
{
_a = a;
}

public void Bar()
{
...
_a.Value.Foo();
...
}
}

这个方法不是最完美的,但是解决了根本问题,并且依赖项在构造函数中有明确声明。

为了打破循环依赖关系,我们需要一个服务工厂,而不是实际的对象,在示例中,IServiceProviderLazy都被用作工厂。

可以看到在ABP中也提供了IAbpLazyServiceProvider

1
2
3
4
5
6
7
public virtual object? GetService(Type serviceType)
{
return CachedServices.GetOrAdd(
serviceType,
_ => new Lazy<object?>(() => ServiceProvider.GetService(serviceType))
).Value;
}

img

参考:c# - Does .net core dependency injection support Lazy - Stack Overflow

[Lazily resolving services to fix circular dependencies in .NET Core - Thomas Levesque’s .NET Blog]