第三方 IoC 容器库不少,之所以选择这个是因为只有它支持 .NET Framework 4.0,这样可以让那些古老的程序也能焕发青春。
安装
包地址:https://www.nuget.org/packages/Unity.Container/
注册方式
注册主要分为三种方式
按类型注册
1 | Container.RegisterType<Foo>(); // 可有可无的一行代码 |
如果是非接口类型,可以不用注册。
按接口注册
1 | container.RegisterType<IFoo, Foo>(); |
如果接口有多个实现,默认按最后一次注册的为准
1 | Container.RegisterType<IFoo, Foo>(); |
具名注册,解决同一接口有多个实现的问题
1 | Container.RegisterType<IFoo, Foo>("xxx"); // 给一个名称 |
按实例注册
和按类型注册差不多,只是传入的参数是实例。传入后容器会持有引用,以后检索会返回该实例。
1 | var sampleClass = new SampleClass(); |
这样每次获得的都是同一个实例,起到了单例的效果。
支持具名化,方便检索同类型的不同实例
1 | container.RegisterInstance("name", sampleClass); |
工厂函数
注册一个工厂函数,以便在解析时动态创建实例。
1 | var container = new UnityContainer(); |
生命周期
瞬态生存期
类型:TypeLifetime.Transient
所谓瞬态就是每次解析时都创建一个新的实例。这也是默认的生命周期管理方式。
1 | Container.RegisterType<SampleClass>(); |
解析生存期
类型:TypeLifetime.PerResolve
这个稍微难理解点,表面上看和Transient
是一样的,但是它发生在库内部隐式注入对象时。
直接看例子比较容易理解
1 | class A |
如果按瞬态类型:
1 | Container.RegisterType<A>(TypeLifetime.Transient); |
两次输出结果不同,意味着传递给 C 的对象 a 实例与传递给 B 对象的 a 实例不同。
如果改为PerResolve
方式:
1 | Container.RegisterType<A>(TypeLifetime.PerResolve); |
顾名思义,就是每次调用Resolve
时对于同一类型只会创建一个实例。
容器树单例(全局唯一)
类型:TypeLifetime.Singleton
它确保对象在整个容器树内只被创建一次。
注册类型总是将注册放置在容器树的根,并使其对该容器的所有子容器全局可用。注册是否发生在子容器的根节点并不重要,注册目的地总是根节点。
1 | IUnityContainer container1 = new UnityContainer(); |
容器树单例(向下传递)
类型:TypeLifetime.PerContainer
、TypeLifetime.ContainerControlled
与TypeLifetime.Singleton
类似,只不过不会推到根容器去注册,但是会向子容器传递。
1 | IUnityContainer container1 = new UnityContainer(); |
容器单例
类型:TypeLifetime.Hierarchical
、TypeLifetime.Scoped
容器级别的单例。父容器与子容器都可以有自己唯一的实例,互不打扰。
1 | IUnityContainer container1 = new UnityContainer(); |
容器瞬态
类型:TypeLifetime.PerContainerTransient
容器会持有对象,直到容器销毁。且如果对象实现了 IDisposable 接口,则会在释放时调用。
1 | var container = new UnityContainer(); |
线程单例
类型:TypeLifetime.PerThread
每个线程都有自己的一个单例。
1 | Container.RegisterType<IFoo, Foo>(TypeLifetime.PerThread); |
外部生存期
类型:TypeLifetime.External
容器将持有对象实例的弱引用,每次调用Resolve
都会返回该实例。如果实例不再有效,则创建新实例。
1 | Container.RegisterType<IFoo, Foo>(TypeLifetime.External); |
依赖注入
通过一系列以Injection
开头的类注入。用的最多的就三类:构造注入、属性注入、方法注入。
构造注入
如果一个类有多个构造函数,容器会选择具有最多参数的构造函数来进行注入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public 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"使用 InjectionConstructor 标记构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class MyClass
{
[ ]
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]
指定构造函数。通过 InjectionConstructor 配置构造参数信息,以便可以调用指定的构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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 指定构造函数。容器将优先使用注册类型时指定的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public 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"在这个例子中,同时使用了注解和注册时指定两种方式。后者优先级更高。
覆盖参数
1
2
3
4
5
6
7public class MyClass
{
public MyClass(string name) {}
}
container.RegisterType<MyClass>(new InjectionConstructor("jack"));
container.Resolve<MyClass>(new ParameterOverride("name", "annie")); // 参数名大小写敏感。不能写为 "Name"注意,参数名是区分大小写的。
覆盖参数主要应用在需要向构造函数传递参数的时候。
属性注入
使用 Dependency 标记属性。
1
2[ ]
public IFoo Foo { get; set; }-
1
2
3
4
5
6
7
8public class MyClass
{
public string Name { get; set; }
}
container.RegisterType<MyClass>(new InjectionProperty("Name", "jack"));
var obj = container.Resolve<MyClass>();
Debug.WriteLine(obj.Name); // 输出 "jack" 覆盖属性
1
2
3
4
5
6
7public class MyClass
{
public string Name { get; set; }
}
container.RegisterType<MyClass>(new InjectionProperty("Name"));
container.Resolve<MyClass>(new PropertyOverride("Name", "annie"));
方法注入
使用 InjectionMethod 标记方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class MyClass
{
public MyClass(IA a, IB b)
{
Debug.WriteLine("2");
}
[ ]
public void Foo1(IA a)
{
Debug.WriteLine("InjectionMethod 1");
}
[ ]
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"方法会在构造函数结束时调用。
-
1
2
3
4
5
6public class MyClass
{
public void Foo(IB b) { }
}
container.RegisterType<MyClass>(new InjectionMethod("Foo"));
字段注入
一般很少会用这种方式注入。
1 | public class MyClass |
注意字段访问权限要求是public
。
实践经验
不要互相注入
1
2
3
4
5
6
7
8
9
10
11
12interface 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
8IUnityContainer 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
2childContainer.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