在 Ioc 世界中,有些框架(例如 Autofac/NInject/Unity)支持传递默认参数,有些框架(例如 SimpleInjector/LightInjector 等)则不支持。作为 My.Ioc 来说,我们支持默认参数。
当我们在 My.Ioc 中注册对象时,有些对象类型 (System.Type) 要求我们必须提供默认参数,而有些则不是必要的。在 My.Ioc 中,默认参数有两个作用:1. 容器根据默认参数来选择用于构建对象的构造函数。而一旦选定构造函数之后,今后容器便会一直使用该构造函数来构造对象实例;2. 如果用户在向容器请求对象实例(即调用任何一个 container.Resolve 重载方法)时没有传入覆盖参数(即未调用任何包含 overridenParameters 参数的 container.Resolve 重载方法),容器便会使用这些默认参数来构造对象实例。下面我们通过一个示例来加以说明:
using System; using System.Diagnostics; using My.Ioc; namespace ConstructorSelectAndParameterOverride { #region Test Types public class ParameterClass1 { } public class ParameterClass2 { } public class ParameterClass3 { } public class ParameterClass4 { } public class ParameterClass5 { } public class PositionalTarget { private ParameterClass1 _c1; private ParameterClass2 _c2; private ParameterClass3 _c3; private ParameterClass4 _c4; private ParameterClass5 _c5; private int _age; private string _name; public PositionalTarget(ParameterClass1 c1, int age, string name, ParameterClass2 c2, ParameterClass3 c3, ParameterClass4 c4) { _c1 = c1; _c2 = c2; _c3 = c3; _c4 = c4; _name = name; _age = age; } public PositionalTarget(ParameterClass1 c1, int age, string name, ParameterClass5 c5, ParameterClass3 c3, ParameterClass4 c4) { _c1 = c1; _c5 = c5; _c3 = c3; _c4 = c4; _name = name; _age = age; } public ParameterClass5 ParameterClass5 { get { return _c5; } } public ParameterClass2 ParameterClass2 { get { return _c2; } } public string Name { get { return _name; } } public int Age { get { return _age; } } } public class NamedTarget { private ParameterClass1 _c1; private ParameterClass2 _c2; private ParameterClass3 _c3; private ParameterClass4 _c4; private ParameterClass5 _c5; private int _age; private string _name; public NamedTarget(ParameterClass1 c1, int age, ParameterClass2 c2, ParameterClass3 c3, string name, ParameterClass4 c4) { _c1 = c1; _c2 = c2; _c3 = c3; _c4 = c4; _name = name; _age = age; } public NamedTarget(ParameterClass1 c1, int age, ParameterClass5 c5, ParameterClass3 c3, ParameterClass4 c4) { _c1 = c1; _c5 = c5; _c3 = c3; _c4 = c4; _age = age; } public ParameterClass5 ParameterClass5 { get { return _c5; } } public ParameterClass2 ParameterClass2 { get { return _c2; } } public string Name { get { return _name; } } public int Age { get { return _age; } } } #endregion class Program { static void Main(string[] args) { IObjectContainer container = new ObjectContainer(false); Register(container); ResolvePositionalTargetWithoutOverridingParameters(container); ResolvePositionalTargetAndOverrideWithPositionalParameters(container); ResolvePositionalTargetAndOverrideWithNamedParameters(container); ResolveNamedTargetWithoutOverridingParameters(container); ResolveNamedTargetAndOverrideWithPositionalParameters(container); ResolveNamedTargetAndOverrideWithNamedParameters(container); Console.ReadLine(); } static void Register(IObjectContainer container) { container.Register<ParameterClass1>(); container.Register<ParameterClass2>(); container.Register<ParameterClass3>(); container.Register<ParameterClass4>(); container.Register<ParameterClass5>(); container.Register<PositionalTarget>() .WithConstructor( Parameter.Auto, Parameter.Positional(30), Parameter.Positional("China"), Parameter.Positional<ParameterClass5>()); container.Register<NamedTarget>() .WithConstructor(Parameter.Named("age", 90)); container.CommitRegistrations(); } static void ResolvePositionalTargetWithoutOverridingParameters(IObjectContainer container) { Console.WriteLine("Resolve PositionalTarget without overriding default parameters: "); var positional = container.Resolve<PositionalTarget>(); Debug.Assert(positional.ParameterClass5 != null); Console.WriteLine(positional.Name); Console.WriteLine(positional.Age); } static void ResolvePositionalTargetAndOverrideWithPositionalParameters(IObjectContainer container) { Console.WriteLine("Resolve PositionalTarget and override default parameters using positional parameters: "); var positional = container.Resolve<PositionalTarget>( Parameter.Auto, Parameter.Positional(99), Parameter.Positional("ZhongHua")); Debug.Assert(positional.ParameterClass5 != null); Console.WriteLine(positional.Name); Console.WriteLine(positional.Age); } static void ResolvePositionalTargetAndOverrideWithNamedParameters(IObjectContainer container) { Console.WriteLine("Resolve PositionalTarget and override default parameters using named parameters: "); var positional = container.Resolve<PositionalTarget>( Parameter.Named("name", "HuaXia")); Debug.Assert(positional.ParameterClass5 != null); Console.WriteLine(positional.Name); Console.WriteLine(positional.Age); } static void ResolveNamedTargetWithoutOverridingParameters(IObjectContainer container) { Console.WriteLine("Resolve NamedTarget without overriding default parameters: "); var named = container.Resolve<NamedTarget>(); Debug.Assert(named.ParameterClass5 != null); Debug.Assert(named.Name == null); Console.WriteLine(named.Age); } static void ResolveNamedTargetAndOverrideWithPositionalParameters(IObjectContainer container) { Console.WriteLine("Resolve NamedTarget and override default parameters using positional parameters: "); var named = container.Resolve<NamedTarget>( Parameter.Auto, Parameter.Positional(12)); Debug.Assert(named.ParameterClass5 != null); Debug.Assert(named.Name == null); Console.WriteLine(named.Age); } static void ResolveNamedTargetAndOverrideWithNamedParameters(IObjectContainer container) { Console.WriteLine("Resolve NamedTarget and override default parameters using named parameters: "); var named = container.Resolve<NamedTarget>( Parameter.Named("age", 68)); Debug.Assert(named.ParameterClass5 != null); Debug.Assert(named.Name == null); Console.WriteLine(named.Age); } } }
在上面这个示例中,我们看到有两个类 PositionalTarget 和 NamedTarget,这两个是我们打算向容器请求的目标类。之所以有两个,是因为我们要分别演示两种传递默认参数的方法,即命名参数 (Named Parameter) 和定位参数 (Positional Parameter) 的用法。
在 My.Ioc 中,我们把参数分为两个大类:一种是可以自动装配 (Autowirable) 的参数,一种是不可自动装配 (NonAutowirable) 的参数。不可自动装配的参数,顾名思义,容器没有办法为这类参数自动构建一个参数值。这类参数包括所有值类型,以及 string 型和 Type 型(请参见 My.Ioc.Helpers.TypeExtensions 的 IsNotAutowirable 和 IsAutowirable 两个扩展方法)。可以自动装配的参数则是除了不可自动装配的参数之外的所有其他参数类型,这种类型的参数是容器可以自动装配的(废话),它们要么是用户已经注册的类型,要么是符合某种约定的、容器可以自动注册/解析的类型(参见My.Ioc 代码示例——实现自动注册/解析)。
对于可以自动装配的参数来说,是否提供默认值是可选的。而对于不可自动装配的参数来说,由于其是不可自动装配的(又是废话),因此用户必须为其提供一个默认值。
如果用户要提供默认参数值,则有两个选择:或是以定位参数方式提供默认值,或是以命名参数方式提供默认值,二者不可混用。也就是说,在提供默认参数值时,不能同时使用命名参数和定位参数,只能任意选用其中一种。譬如,下面这种用法就是错误的(在 IDE 中也会提示出错,而无法通过编译):
container.Register<PositionalTarget>() .WithConstructor( Parameter.Auto, Parameter.Positional(30), Parameter.Named("name", "China"), Parameter.Positional<ParameterClass5>());
在选择(或者说匹配)构造函数时,无论使用定位参数还是命名参数,有一个原则是二者都必须共同遵循的,那就是 [所有“不可自动装配的参数”都必须显式指定一个默认值]。但在用法方面,二者略有一些不同。
使用定位参数时,容器将根据参数位置来匹配参数。这里有一点要特别说明一下。在一些构造函数中,有可能会出现“不可自动装配的参数”位于“可以自动装配的参数”后面的情况(本文的示例代码中给出的就是这种情况)。在这种情况下,由于是按位置匹配参数,因此对于排在“不可自动装配的参数”之前的“可以自动装配的参数”,用户也需要显式提供一个默认值。但此时用户既可以指定提供一个该类型的对象实例作为参数值,也可以使用“Parameter.Auto”。后者是 My.Ioc 容器中提供的一种占位参数,表示此构造参数的参数值将由容器自行决定(即由容器自动装配)。
使用命名参数时,参数位置不重要,容器将会根据参数名称来匹配参数。一般说来,只要参数名称正确,而且为所有“不可自动装配的参数”都提供了默认值,容器便能成功匹配构造函数。
有了上面这些知识作为背景,接下来我们来看一下 PositionalTarget 这个类的构造函数。我们看到在 PositionalTarget 这个类中,有两个构造函数,其签名分别是:
public PositionalTarget(ParameterClass1 c1, int age, string name, ParameterClass2 c2, ParameterClass3 c3, ParameterClass4 c4) public PositionalTarget(ParameterClass1 c1, int age, string name, ParameterClass5 c5, ParameterClass3 c3, ParameterClass4 c4)
在这两个构造函数中,都包含了 int 类型和 string 类型的构造参数,因此,我们知道要想构造这个类的对象,无论选择哪个构造函数都是需要提供默认参数的。
在示例代码中,对象注册是在 Register 方法中完成的。因此,接下来我们就来看一下这个 Register 方法:
1 static void Register(IObjectContainer container) 2 { 3 container.Register<ParameterClass1>(); 4 container.Register<ParameterClass2>(); 5 container.Register<ParameterClass3>(); 6 container.Register<ParameterClass4>(); 7 container.Register<ParameterClass5>(); 8 9 container.Register<PositionalTarget>() 10 .WithConstructor( 11 Parameter.Auto, 12 Parameter.Positional(30), 13 Parameter.Positional("China"), 14 Parameter.Positional<ParameterClass5>()); 15 16 container.Register<NamedTarget>() 17 .WithConstructor(Parameter.Named("age", 90)); 18 19 container.CommitRegistrations(); 20 }
在这个方法中,我们首先注册了 ParameterClass1、ParameterClass2、ParameterClass3、ParameterClass4 和 ParameterClass5 等类,这几个类型将要用作 PositionalTarget 和 NamedTarget 的构造参数,它们也是所谓的“可以自动装配的参数”。由于它们已经注册到容器中,因此容器在创建 PositionalTarget 和 NamedTarget 的实例时,便可以自动创建这几个类的实例以满足构造函数需要(也即自动装配的意思)。
接下来,在第 9 行到第 14 行中,我们采用定位参数的方式指定了用于构造 PositionalTarget 对象的默认参数。我们看到第一个默认参数是 Parameter.Auto。这是由于该参数是 ParameterClass1 类型(属于“可以自动装配的参数”),但它后面的构造参数是 int 类型(属于“不可自动装配的参数”),所以我们这里需要为它提供一个默认参数值。
后面两个参数 Parameter.Positional(30) 和 Parameter.Positional("China") 的含义都比较简单,这里略过不谈。
我们看到最后还提供了一个默认参数 Parameter.Positional<ParameterClass5>()。这里之所以要提供这个参数,是因为 PositionalTarget 这个类的构造函数中,前面几个构造参数的参数类型都完全相同,因此没有办法区分到底匹配哪个构造函数。只有到了第四个构造参数时,其参数类型才显示出有所不同(一个是 ParameterClass5 类型,一个是 ParameterClass2 类型),因此我们这里需要提供这个默认参数才能加以区分。当我们提供了这个默认参数之后,要选用哪个构造函数的问题也就一目了然了。
在 Register 这个方法的末后,可以看到我们注册了 NamedTarget 这个类型。NamedTarget 这个类型也有两个构造函数,分别是:
public NamedTarget(ParameterClass1 c1, int age, ParameterClass2 c2, ParameterClass3 c3, string name, ParameterClass4 c4) public NamedTarget(ParameterClass1 c1, int age, ParameterClass5 c5, ParameterClass3 c3, ParameterClass4 c4)
在上面注册 NamedTarget 的代码中,我们看到只提供了一个命名参数 Parameter.Named("age", 90)。根据 [所有“不可自动装配的参数”都要显式指定一个默认值] 的原则,我们也可以很容易明白作者想要选用的是下面一个构造函数。
上面,我们向大家介绍了在什么情况下需要提供默认参数以及如何提供默认参数。接下来,我们将向各位简单介绍一下如何覆盖在注册时提供的默认参数。
在一般的编程实践中,如果我们要创建一个对象,我们可以给这个对象的构造函数传递不同的参数值以创建不同的对象实例。My.Ioc 中的覆盖参数 (Overridden Parameters) 的意义也即在此。通过使用覆盖参数,我们也可以要求容器为我们提供不同的对象实例。
在使用覆盖参数时,由于此时对象的默认构造函数已经由容器选定,而且所有不可自动装配的参数也都有了默认值,因此我们不需要为所有“不可自动装配的参数”指定一个覆盖参数值。如果我们想要覆盖某个默认参数,我们便为其提供一个覆盖参数值;如果我们不需要覆盖某个默认参数,只需略过该参数即可。但是,我们传递的覆盖参数仍然要符合一定的原则。
譬如,如果我们以定位方式传递覆盖参数,仍然需要符合 [对于排在“不可自动装配的参数”之前的“可以自动装配的参数”,需要显式提供一个覆盖参数值] 这个原则。也就是说,即使我们要覆盖的目标参数前面的参数是可以自动装配的,我们也要为其提供一个覆盖参数值。一般情况下,提供 Parameter.Auto 即可。此外,如果在要覆盖的目标参数的前面还有其他“不可自动装配的参数”,而我们并不想覆盖它们,也可以使用 Parameter.Auto。
使用命名参数方式提供覆盖参数时,只需按名称指定要覆盖的参数,并为其提供一个参数值即可。用法比较简单,此处不再多言。
本文示例代码以及 My.Ioc 框架源码可在此处获取。