Unity.Container 快速入门
2024-11-03 00:57:51

第三方 IoC 容器库不少,之所以选择这个是因为只有它支持 .NET Framework 4.0,这样可以让那些古老的程序也能焕发青春。

安装

包地址:https://www.nuget.org/packages/Unity.Container/

注册方式

注册主要分为三种方式

按类型注册

1
2
Container.RegisterType<Foo>(); // 可有可无的一行代码
var foo = Container.Resolve<Foo>();

如果是非接口类型,可以不用注册。

按接口注册

1
2
container.RegisterType<IFoo, Foo>();
var foo = container.Resolve<IFoo>();

如果接口有多个实现,默认按最后一次注册的为准

1
2
3
Container.RegisterType<IFoo, Foo>();
Container.RegisterType<IFoo, Foo2>();
var foo = Container.Resolve<IFoo>(); // Foo2

具名注册,解决同一接口有多个实现的问题

1
2
3
Container.RegisterType<IFoo, Foo>("xxx"); // 给一个名称
Container.RegisterType<IFoo, Foo2>();
var foo = Container.Resolve<IFoo>("xxx");

按实例注册

和按类型注册差不多,只是传入的参数是实例。传入后容器会持有引用,以后检索会返回该实例。

1
2
3
4
5
6
7
8
9
10
var sampleClass = new SampleClass();

container.RegisterInstance(sampleClass);
var obj1 = (SampleClass)container.Resolve(typeof(SampleClass));
obj1.Increase();
Debug.WriteLine(obj1.ReferenceCount);

var obj2 = (SampleClass)container.Resolve(typeof(SampleClass));
obj2.Increase();
Debug.WriteLine(obj2.ReferenceCount);

这样每次获得的都是同一个实例,起到了单例的效果。
支持具名化,方便检索同类型的不同实例

1
container.RegisterInstance("name", sampleClass);

工厂函数

注册一个工厂函数,以便在解析时动态创建实例。

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
var container = new UnityContainer();
container.RegisterType<School>();
container.RegisterFactory<People>(c => new People());

var x = container.Resolve<School>();
x.Foo();

internal class School
{
private readonly Func<People> _factory;

public School(Func<People> factory)
{
_factory = factory;
}

public void Foo()
{
var people = _factory();
Console.WriteLine(people);
}
}

internal class People
{
private string Name { get; set; }
}

生命周期

瞬态生存期

类型:TypeLifetime.Transient
所谓瞬态就是每次解析时都创建一个新的实例。这也是默认的生命周期管理方式。

1
2
3
4
5
6
Container.RegisterType<SampleClass>();
// or
Container.RegisterType<SampleClass>(TypeLifetime.Transient);

var obj1 = Container.Resolve<SampleClass>(); // 新实例
var obj2 = Container.Resolve<SampleClass>(); // 新实例

解析生存期

类型:TypeLifetime.PerResolve
这个稍微难理解点,表面上看和Transient是一样的,但是它发生在库内部隐式注入对象时。
直接看例子比较容易理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A
{
}

class B
{
public A a { get; set; }

public B(A arg)
{
Debug.WriteLine($"B: {arg.GetHashCode()}");
}
}

class C
{
public C(A arg1, B arg2)
{
Debug.WriteLine($"A: {arg1.GetHashCode()}");
}
}

如果按瞬态类型:

1
2
3
4
5
Container.RegisterType<A>(TypeLifetime.Transient);
var x = Container.Resolve<C>();

// 输出B: 30631159
// 输出A: 7244975

两次输出结果不同,意味着传递给 C 的对象 a 实例与传递给 B 对象的 a 实例不同。
如果改为PerResolve方式:

1
2
3
4
5
Container.RegisterType<A>(TypeLifetime.PerResolve);
var x = Container.Resolve<C>();

// 输出B: 65204782
// 输出A: 65204782

顾名思义,就是每次调用Resolve时对于同一类型只会创建一个实例。

容器树单例(全局唯一)

类型:TypeLifetime.Singleton
它确保对象在整个容器树内只被创建一次。
注册类型总是将注册放置在容器树的根,并使其对该容器的所有子容器全局可用。注册是否发生在子容器的根节点并不重要,注册目的地总是根节点。

1
2
3
4
5
6
7
8
IUnityContainer container1 = new UnityContainer();
var container2 = container1.CreateChildContainer();

container2.RegisterType<IA, A>(TypeLifetime.Singleton); // 在子容器上注册的注册行为,实际上推到了根容器

var obj1 = container1.Resolve<IA>(); // 根容器可以检索到实例
var obj2 = container2.Resolve<IA>();
Console.WriteLine(obj1 == obj2); // 输出 True

容器树单例(向下传递)

类型:TypeLifetime.PerContainerTypeLifetime.ContainerControlled
TypeLifetime.Singleton类似,只不过不会推到根容器去注册,但是会向子容器传递。

1
2
3
4
5
6
7
8
IUnityContainer container1 = new UnityContainer();
var container2 = container1.CreateChildContainer();

container1.RegisterType<IA, A>(TypeLifetime.PerContainer);

var obj1 = container1.Resolve<IA>();
var obj2 = container2.Resolve<IA>();
Console.WriteLine(obj1 == obj2); // 输出 True。因为子容器共享父容器的实例

容器单例

类型:TypeLifetime.HierarchicalTypeLifetime.Scoped
容器级别的单例。父容器与子容器都可以有自己唯一的实例,互不打扰。

1
2
3
4
5
6
7
8
IUnityContainer container1 = new UnityContainer();
var container2 = container1.CreateChildContainer();

container1.RegisterType<IA, A>(TypeLifetime.Scoped);

var obj1 = container1.Resolve<IA>();
var obj2 = container2.Resolve<IA>();
Console.WriteLine(obj1 == obj2); // 输出 False。因为子容器有自己的实例

容器瞬态

类型:TypeLifetime.PerContainerTransient
容器会持有对象,直到容器销毁。且如果对象实现了 IDisposable 接口,则会在释放时调用。

1
2
3
4
var container = new UnityContainer();
container.RegisterType<IFoo, Foo>(TypeLifetime.PerContainerTransient); // 容器级瞬态
var x = container1.Resolve<IFoo>(); // 容器会持有 x,直到容器销毁
container.Dispose(); // 销毁容器,会触发 x.Dispose()

线程单例

类型:TypeLifetime.PerThread
每个线程都有自己的一个单例。

1
Container.RegisterType<IFoo, Foo>(TypeLifetime.PerThread);

外部生存期

类型:TypeLifetime.External
容器将持有对象实例的弱引用,每次调用Resolve都会返回该实例。如果实例不再有效,则创建新实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Container.RegisterType<IFoo, Foo>(TypeLifetime.External);

var x1 = Container.Resolve<IFoo>(); // 容器会持有 x 的弱引用
var x2 = Container.Resolve<IFoo>(); // x 还在有效范围内,会返回上一次的实例

// Hash相同
Debug.WriteLine(x1.GetHashCode());
Debug.WriteLine(x2.GetHashCode());

// 让GC回收x引用
x1 = null;
x2 = null;
GC.Collect();

var x3 = Container.Resolve<IFoo>(); // 由于容器持有的弱引用对象已经被GC回收,这里会产生新对象
Debug.WriteLine(x3.GetHashCode());

依赖注入

通过一系列以Injection开头的类注入。用的最多的就三类:构造注入、属性注入、方法注入。

构造注入

  1. 如果一个类有多个构造函数,容器会选择具有最多参数的构造函数来进行注入。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class MyClass
    {
    public MyClass(IA a)
    {
    Debug.WriteLine("1");
    }

    public MyClass(IA a, IB b)
    {
    Debug.WriteLine("2");
    }

    public MyClass(IA a, IB b, string name)
    {
    Debug.WriteLine("3");
    }
    }

    container.RegisterType<IA, A>();
    container.RegisterType<IB, B>();
    var myObject = container.Resolve<MyClass>(); // 输出"2"
  2. 使用 InjectionConstructor 标记构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MyClass
    {
    [InjectionConstructor]
    public MyClass(IA a)
    {
    Debug.WriteLine("1");
    }

    public MyClass(IA a, IB b)
    {
    Debug.WriteLine("2");
    }
    }

    container.RegisterType<IA, A>();
    container.RegisterType<IB, B>();
    var myObject = container.Resolve<MyClass>(); // 输出"1"

    根据默认规则,容器会匹配最多参数的构造函数。但可以用[InjectionConstructor]指定构造函数。

  3. 通过 InjectionConstructor 配置构造参数信息,以便可以调用指定的构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MyClass
    {
    public MyClass(string name)
    {
    Debug.WriteLine(name);
    }

    public MyClass(IA a)
    {
    Debug.WriteLine("1");
    }
    }

    container.RegisterType<IA, A>();
    container.RegisterType<MyClass>(new InjectionConstructor("jack"));
    container.Resolve<MyClass>(); // 输出 "jack"

    容器虽然能匹配到带有IA参数的构造函数,但是仍会调用 InjectionConstructor 指定构造函数。

  4. 容器将优先使用注册类型时指定的构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MyClass
    {
    public MyClass(string name)
    {
    Debug.WriteLine(name);
    }

    [InjectionConstructor]
    public MyClass(IA a)
    {
    Debug.WriteLine("1");
    }
    }

    container.RegisterType<IA, A>();
    container.RegisterType<MyClass>(new InjectionConstructor("jack"));
    container.Resolve<MyClass>(); // 输出 "jack"

    在这个例子中,同时使用了注解和注册时指定两种方式。后者优先级更高。

  5. 覆盖参数

    1
    2
    3
    4
    5
    6
    7
    public class MyClass
    {
    public MyClass(string name) {}
    }

    container.RegisterType<MyClass>(new InjectionConstructor("jack"));
    container.Resolve<MyClass>(new ParameterOverride("name", "annie")); // 参数名大小写敏感。不能写为 "Name"

    注意,参数名是区分大小写的
    覆盖参数主要应用在需要向构造函数传递参数的时候。

属性注入

  1. 使用 Dependency 标记属性。

    1
    2
    [Dependency]
    public IFoo Foo { get; set; }
  2. InjectionProperty

    1
    2
    3
    4
    5
    6
    7
    8
    public class MyClass
    {
    public string Name { get; set; }
    }

    container.RegisterType<MyClass>(new InjectionProperty("Name", "jack"));
    var obj = container.Resolve<MyClass>();
    Debug.WriteLine(obj.Name); // 输出 "jack"
  3. 覆盖属性

    1
    2
    3
    4
    5
    6
    7
    public class MyClass
    {
    public string Name { get; set; }
    }

    container.RegisterType<MyClass>(new InjectionProperty("Name"));
    container.Resolve<MyClass>(new PropertyOverride("Name", "annie"));

方法注入

  1. 使用 InjectionMethod 标记方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class MyClass
    {
    public MyClass(IA a, IB b)
    {
    Debug.WriteLine("2");
    }

    [InjectionMethod]
    public void Foo1(IA a)
    {
    Debug.WriteLine("InjectionMethod 1");
    }

    [InjectionMethod]
    public void Foo2(IB b)
    {
    Debug.WriteLine("InjectionMethod 2");
    }
    }

    container.RegisterType<IA, A>();
    container.RegisterType<IB, B>();
    container.Resolve<MyClass>(); // 依次输出 "2"、"InjectionMethod 1"、"InjectionMethod 2"

    方法会在构造函数结束时调用。

  2. InjectionMethod

    1
    2
    3
    4
    5
    6
    public class MyClass
    {
    public void Foo(IB b) { }
    }

    container.RegisterType<MyClass>(new InjectionMethod("Foo"));

字段注入

一般很少会用这种方式注入。

1
2
3
4
5
6
7
8
9
public class MyClass
{
[Dependency]
public IA a;

public IB b;
}

container.RegisterType<MyClass>(new InjectionField("b"));

注意字段访问权限要求是public

实践经验

  • 不要互相注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface IA {}
    interface IB {}

    internal sealed class A : IA
    {
    internal A(IB b) {}
    }

    internal sealed class B : IB
    {
    internal B(IA a) {}
    }

    这会陷入死循环,从而引发 System.StackOverflowException 异常。

  • 子容器可以使用父容器的接口,反之则不行。

    1
    2
    3
    4
    5
    6
    7
    8
    IUnityContainer container = new UnityContainer();
    container.RegisterType<IA, A>();

    var childContainer = container.CreateChildContainer();
    childContainer.RegisterType<IB, B>();

    var a = childContainer.Resolve<IA>(); // 子容器知道父容器注册的接口
    var b = container.Resolve<IB>(); // 错误!父容器不知道子容器注册的接口

    单例是例外,因为单例被推到了父容器中注册

    1
    2
    childContainer.RegisterType<IB, B>(TypeLifetime.Singleton); // 单例会在父容器中注册
    container.Resolve<IB>();

相关阅读

http://unitycontainer.org/tutorials/Composition/composition.html
https://www.tutorialsteacher.com/ioc/unity-container
The Relationship Zoo