C# 泛型生成器






4.56/5 (10投票s)
C# 泛型类型的组合式生成。
引言
我在这里发布一些 C# 代码,用于构建泛型类型的各种可能组合。用户指定一个具有未解析泛型参数的泛型类型(例如 Tuple< , , >
),并指定可用于构建的可用类型(例如 int
、string
)。然后,GenericTypeGenerator
类会返回所有可以构建的类型。
在刚才的示例中,GenericTypeGenerator
将返回以下类型的类型:
Tuple<int, int, int>
Tuple<int, int, string>
Tuple<int, string, int>
Tuple<string, int, int>
Tuple<string, string, string>
Tuple<string, string, int>
Tuple<string, int, string>
Tuple<int, string, string>
背景
尽管入门示例相对简单,但解决通用问题的代码却并非如此。然而,任何对 .NET 泛型有基本了解的人都应该能够检查和修改代码。
动机
我创建此工具的动机是改进测试。在开发 C# 应用程序时,我决定使用 策略设计模式。虽然我也可以使用 策略模式,但我认为策略当时是最佳方法。策略通常是传递给策略宿主的构造函数参数。在 C++ 中,策略通常是应用于策略宿主的模板参数;在 C# 中,我当然使用模板的等效物,即泛型。
以下是您可能偏爱策略(通过泛型参数)而非策略(通过构造函数参数)的几个原因:
- 您希望在编译时保留策略的实际类型。这可以使代码更易用、更高效。如果您想支持“增强策略”,这也很有必要。增强策略是策略宿主公开的常规策略。它们可以包含客户端想要但宿主不知道的其他功能。
- 您希望利用 泛型约束。每个策略的多个约束以及策略之间的裸约束,每个都可根据策略宿主任意指定,通过泛型表达效果最佳。
- 您希望将每个策略的生命周期管理问题正式委托给策略宿主。
- 您不想接受构造函数参数为 null 的风险。
- 策略设计模式同样适用于静态类。由于静态类的构造函数不是手动调用的,因此策略模式应用于静态类时会有些 awkward。
如果这些原因中的任何一个似乎与您的应用程序无关(大多数情况下可能都不会),那么策略可能是更好的选择。您可能偏爱策略而非策略的一个特定原因是,支持类上的多个泛型参数的写法很丑陋且相对繁琐。实际上,FxCop 甚至为此有一个 规则!
回到主题,假设一个 C# 策略宿主包含四种策略类型:
public interface IMyGame
{
int MyTestableMethod();
}
public class MyGame<TDisplayPolicy, TAudioPolicy,
TMemoryPolicy, TDebugPolicy> : IMyGame
where TDisplayPolicy : IDisplayPolicy
where TAudioPolicy : IAudioPolicy
where TMemoryPolicy : IMemoryPolicy
where TDebugPolicy : IDebugPolicy
{
public int MyTestableMethod()
{
// Assume the functionality of this method is dependent on one or more
// of the policy types. Assume it may even be dependent on the
// *interaction* between the policy types. (Note that this scenario is only
// realistic if the policies have class or interface constraints that we can
// work with here in this method.)
return 0;
}
}
作为我测试计划的一部分,我想针对 MyGame
的每个版本运行测试,其中 MyGame
会根据传递给它的不同泛型参数策略而变化。在此特定示例中,我特别想测试 MyTestableMethod
。但是,如果每种策略类型只有四种变体(例如 MyDisplayPolicy1
、MyDisplayPolicy2
、MyDisplayPolicy3
、MyDisplayPolicy4
),那么可能的 MyGame
类的数量将是 256!通过手动编码来测试 256 个类是不可行的。我们需要一种组合测试方法。
我的总体目标是某种程度上创建所有 256 个类型,通过反射创建这些类型的实例,然后将这些实例强制转换为一个简单但可测试的接口,该接口不随所讨论的泛型参数而变化(在本例中,该接口是 IMyGame
)。然后,我就可以对该接口运行一系列统一的测试。
乍一看,我似乎可以简单地设置四个嵌套的 for
循环,并在不经太多思考的情况下创建 256 个类型。但请记住——策略也可能是泛型类型,并且它们可以任意组合。
public class MyDisplayPolicy1<TMemoryPolicy, TDebugPolicy> { }
public class MyAudioPolicy1<TMemoryPolicy, TDebugPolicy> { }
public class MyMemoryPolicy1<TDebugPolicy> { }
复杂性不止于此。考虑任意泛型约束:
public class MyDisplayPolicy2<TMemoryPolicy, TDebugPolicy>
where TMemoryPolicy : IMemoryPolicy<TDebugPolicy>,
IDisposable,
IMyInterface1<TDebugPolicy, string>,
IList<IMyInterface2>
{
}
我们最终无法用四个 for
循环来解决这个问题。
这个问题可以用泛型参数或构造函数参数来非常相似地呈现。无论以哪种方式呈现,它都具有相当大的复杂性(与 背包问题有相似之处)。当以泛型参数的形式呈现时,存在独特的顾虑需要解决。这段代码旨在解决这些顾虑,同时解决通用问题。
Using the Code
该代码使用 C# 4.0 和 .NET 4.0 库。截至目前,我仅在 Windows 环境下的 Visual Studio 2010 中使用和测试过它。
我还包括一些辅助代码,这些代码可能对任何尝试使用 GenericTypeGenerator
的人有所帮助。
有一个非常有用的测试套件称为 TestApi。它有一个出色的组合测试库,但目前不支持泛型类型的组合测试。我建议在一般情况下使用 TestApi 进行组合测试。事实上,我使用它来测试此代码的某些方面(此依赖关系非常轻量,并且可以根据需要轻松断开)。在其他具有泛型和非泛型组合测试需求的应用程序中,我将 GenericTypeGenerator
与 TestApi 结合使用;它们可以轻松协同工作。
就公共接口而言,以下是最简单的方法:
static public class GenericTypeGenerator
{
static public IEnumerable<Type> BuildTypes(
Type typeToBuildFrom,
IEnumerable<Type> availableTypes,
ParallelOptions parallelOptions = null);
static public IEnumerable<Type> BuildTypes(
Type typeToBuildFrom,
IDictionary<Type, int> availableTypeToTimesUsableMap,
ParallelOptions parallelOptions = null)
}
使用最简单的方法看起来会像这样:
foreach(Type builtType in GenericTypeGenerator.BuildTypes(typeof(MyClass<,,>),
new Type[]
{
typeof(int),
typeof(float),
typeof(string),
typeof(MyClass1),
typeof(MyClass2),
typeof(MyClass3<double>),
typeof(MyClass4<,>)
}))
{
// Do something with builtType. For example, create an instance of it with
// reflection and cast to a testable interface…
// Reflection methods will return an object. It won't be possible to cast this
// object to an instance of its concrete type since that type's generic arguments
// will vary.
object myClassObj = Activator.CreateInstance(builtType);
// However, we can cast this object to a relatively simple interface.
IMyClass myClassInterface = (IMyClass)myClassObj;
// We can now run tests on this interface.
Assert.IsTrue(myClassInterface.MyMethodThatShouldReturnTrue());
Assert.AreEqual(myClassInterface.MyMethodThatShouldReturn11(), 11);
}
默认情况下,非泛型类型和具有已解析泛型参数的泛型类型可以在单个构建类型中不限次数地使用。但是,具有未解析泛型参数的泛型类型只能使用一次(或在可用类型列表中指定的次数)。如果没有此默认规则,就很容易导致无限递归。
正如您所见,BuildTypes
有一个重载。通过它,客户端可以指定在单个构建类型中可以使用多少次可用类型。-1 表示无限制。公共接口中的其他方法会返回构建的类型以及构建它们的类型详情。
查看用于测试 GenericTypeGenerator
的代码是了解其工作原理的简单方法。这是一个测试的样子:
[TestMethod]
public void Test_CyclicalNakedConstraints()
{
VerifyBuiltTypes(
typeof(CyclicalNakedConstraints<,>),
new Type[]
{
typeof(CyclicalNakedConstraints_TImpl),
typeof(CyclicalNakedConstraints_UImpl),
typeof(CyclicalNakedConstraints_TAndUImpl)
},
new Type[]
{
typeof(CyclicalNakedConstraints<CyclicalNakedConstraints_TImpl,
CyclicalNakedConstraints_UImpl>),
typeof(CyclicalNakedConstraints<CyclicalNakedConstraints_UImpl,
CyclicalNakedConstraints_UImpl>),
typeof(CyclicalNakedConstraints<CyclicalNakedConstraints_TAndUImpl,
CyclicalNakedConstraints_TAndUImpl>)
});
}
第一个参数是要构建的类型,第二个参数是可用的类型,第三个参数是预期的构建类型。其中包含大量测试,说明了预期的结果和需要注意的问题。
关注点
这是随代码附带的测试的一部分。除了作为测试的一部分,它还说明了一个特定观点:
private class T_U_tripleNestedIDictionary<T, U>
where U : IDictionary<T, IDictionary<IDictionary<T, object>, T>>
{
}
IEnumerable<Type> builtTypes =
GenericTypeGenerator.BuildTypes(
typeof(T_U_tripleNestedIDictionary<,>),
new Type[]
{
typeof(Func<>),
typeof(IDictionary<,>),
typeof(IDictionary<lt;,>),
typeof(IDictionary<,>),
typeof(object),
typeof(float)
});
粗略估计一下这会产生多少类型,以及底层算法需要评估多少个唯一类型。
Returned: 2
Evaluated: 750+
稍作调整,已评估的类型就会超过 10,000。请注意,指定可相互组合的可用类型可能会付出高昂的代价。
历史
- 2010 年 6 月 19 日
- 初始发布。
- 2010 年 7 月 9 日
- 添加了更多测试。
- 重构了一些现有测试,以实现更精细化的测试粒度。