如何找到两种类型之间最佳拟合的最小协变类型?

问题描述 投票:21回答:3

IsAssignableFrom方法返回一个布尔值,表示一种类型是否可以从另一种类型分配。

我们怎样才能测试它们是否可以相互分配,还能够知道最佳拟合的最小协变类型?

考虑以下示例(C#4.0)

  • // method body of Func is irrelevant, use default() instead Func<char[]> x = default(Func<char[]>); Func<int[]> y = default(Func<int[]>); Func<Array> f = default(Func<Array>); Func<IList> g = default(Func<IList>); g=x; g=y; y=x; // won't compile x=y; // won't compile // following two are okay; Array is the type for the covariance f=x; // Array > char[] -> Func<Array> > Func<char[]> f=y; // Array > int[] -> Func<Array> > Func<int[]> // following two are okay; IList is the interface for the covariance g=x; g=y;

在上面的例子中,找到的是char[]int[]之间的类型。

c# types covariance contravariance
3个回答
25
投票

更新:

事实证明FindInterfaceWith可以简化,并且构建flatten类型层次结构变得多余,因为基类不一定涉及,只要我们在它是接口时考虑类型本身;所以我添加了一个扩展方法GetInterfaces(bool)。由于我们可以通过覆盖规则对交互进行排序,因此接口的排序交集是候选。如果所有这些都同样好,我说没有一个被认为是最好的。如果情况并非如此,那么最好的一个必须覆盖其中一个;并且因为它们是有序的,所以这种关系应存在于数组中最右边的两个接口中,以表示存在最具特异性的最佳接口。


使用Linq可以简化代码;但在我的场景中,我应尽可能减少引用和命名空间的要求。

  • using System; public static class TypeExtensions { static int CountOverlapped<T>(T[] ax, T[] ay) { return IntersectPreserveOrder(ay, ax).Length; } static int CountOccurrence(Type[] ax, Type ty) { var a = Array.FindAll(ax, x => Array.Exists(x.GetInterfaces(), tx => tx.Equals(ty))); return a.Length; } static Comparison<Type> GetCoverageComparison(Type[] az) { return (tx, ty) => { int overlapped, occurrence; var ay = ty.GetInterfaces(); var ax = tx.GetInterfaces(); if(0!=(overlapped=CountOverlapped(az, ax).CompareTo(CountOverlapped(az, ay)))) { return overlapped; } if(0!=(occurrence=CountOccurrence(az, tx).CompareTo(CountOccurrence(az, ty)))) { return occurrence; } return 0; }; } static T[] IntersectPreserveOrder<T>(T[] ax, T[] ay) { return Array.FindAll(ax, x => Array.FindIndex(ay, y => y.Equals(x))>=0); } /* static T[] SubtractPreserveOrder<T>(T[] ax, T[] ay) { return Array.FindAll(ax, x => Array.FindIndex(ay, y => y.Equals(x))<0); } static Type[] GetTypesArray(Type typeNode) { if(null==typeNode) { return Type.EmptyTypes; } var baseArray = GetTypesArray(typeNode.BaseType); var interfaces = SubtractPreserveOrder(typeNode.GetInterfaces(), baseArray); var index = interfaces.Length+baseArray.Length; var typeArray = new Type[1+index]; typeArray[index]=typeNode; Array.Sort(interfaces, GetCoverageComparison(interfaces)); Array.Copy(interfaces, 0, typeArray, index-interfaces.Length, interfaces.Length); Array.Copy(baseArray, typeArray, baseArray.Length); return typeArray; } */ public static Type[] GetInterfaces(this Type x, bool includeThis) { var a = x.GetInterfaces(); if(includeThis&&x.IsInterface) { Array.Resize(ref a, 1+a.Length); a[a.Length-1]=x; } return a; } public static Type FindInterfaceWith(this Type type1, Type type2) { var ay = type2.GetInterfaces(true); var ax = type1.GetInterfaces(true); var types = IntersectPreserveOrder(ax, ay); if(types.Length<1) { return null; } Array.Sort(types, GetCoverageComparison(types)); var type3 = types[types.Length-1]; if(types.Length<2) { return type3; } var type4 = types[types.Length-2]; return Array.Exists(type3.GetInterfaces(), x => x.Equals(type4)) ? type3 : null; } public static Type FindBaseClassWith(this Type type1, Type type2) { if(null==type1) { return type2; } if(null==type2) { return type1; } for(var type4 = type2; null!=type4; type4=type4.BaseType) { for(var type3 = type1; null!=type3; type3=type3.BaseType) { if(type4==type3) { return type4; } } } return null; } public static Type FindAssignableWith(this Type type1, Type type2) { var baseClass = type2.FindBaseClassWith(type1); if(null==baseClass||typeof(object)==baseClass) { var @interface = type2.FindInterfaceWith(type1); if(null!=@interface) { return @interface; } } return baseClass; } }

有两种递归方法;一个是FindInterfaceWith,另一个是重要的方法GetTypesArray,因为已经有一个名为GetTypeArray的类Type的方法,具有不同的用法。

它的工作方式就像Akim提供的GetClassHierarchy方法;但在这个版本中,它构建了一个数组,如:

  • 层次结构的输出 a[8]=System.String a[7]=System.Collections.Generic.IEnumerable`1[System.Char] a[6]=System.Collections.IEnumerable a[5]=System.ICloneable a[4]=System.IComparable a[3]=System.IConvertible a[2]=System.IEquatable`1[System.String] a[1]=System.IComparable`1[System.String] a[0]=System.Object

正如我们所知,它们是按照特定的顺序排列的,这就是它如何使事情发挥作用。建造的阵列GetTypesArray实际上是一棵平坦的树。该数组实际上在模型中如下:

  • 图 请注意,某些接口实现的关系(如IList<int>实现ICollection<int>)与此图中的行没有关联。

返回数组中的接口按Array.Sort排序,并使用GetCoverageComparison提供的排序规则。

有一些事情要提,例如,在一些答案中不仅提到了多个接口实现的可能性(如[this]);我已经定义了解决它们的方法,它们是:

  • 注意 GetInterfaces方法不以特定顺序返回接口,例如按字母顺序或声明顺序。您的代码不得依赖于返回接口的顺序,因为该顺序会有所不同。 由于递归,基类始终是有序的。 如果两个接口具有相同的覆盖范围,则它们都不会被视为合格。 假设我们定义了这些接口(或者类很好): public interface IDelta { } public interface ICharlie { } public interface IBravo: IDelta, ICharlie { } public interface IAlpha: IDelta, ICharlie { } 那么哪一个更适合分配IAlphaIBravo?在这种情况下,FindInterfaceWith只返回null

在问题[How to find the smallest assignable type in two types (duplicate)?]中,我说:

  • 错误的扣除 如果这个假设是正确的,那么FindInterfaceWith就变成了一种冗余的方法;因为FindInterfaceWithFindAssignableWith之间的唯一区别是: 如果有最好的选择,FindInterfaceWith会返回null;而FindAssignableWith直接返回确切的类。

但是,现在我们可以看一下FindAssignableWith的方法,它必须调用其他两种方法是基于原始的假设,这个矛盾的bug只是神奇地消失了。


关于排序接口的覆盖率比较规则,在委托GetCoverageComparison中,我使用:

  • 双重规则 通过调用CountOverlapped比较源接口数组中的两个接口,每个接口覆盖源中的其他接口 如果规则1没有区分它们(返回0),则二级排序是调用CountOccurrence来确定哪些被其他人继承了多次然后比较 这两个规则等同于Linq查询: interfaces=( from it in interfaces let order1=it.GetInterfaces().Intersect(interfaces).Count() let order2=( from x in interfaces where x.GetInterfaces().Contains(it) select x ).Count() orderby order1, order2 select it ).ToArray(); 然后FindInterfaceWith将执行可能的递归调用,以弄清楚这个界面是否足以被识别为最常见的界面或只是另一个关系,如IAlphaIBravo

关于方法FindBaseClassWith,它返回的内容与原始假设不同,如果任何参数为null,则返回null。它实际上返回传入的另一个参数。

这与关于What should the method `FindBaseClassWith` return?的方法链接的问题[FindBaseClassWith]有关。在当前的实现中,我们可以将其称为:

  • 方法链 var type= typeof(int[]) .FindBaseClassWith(null) .FindBaseClassWith(null) .FindBaseClassWith(typeof(char[])); 它将返回typeof(Array);感谢这个功能,我们甚至可以打电话 var type= typeof(String) .FindAssignableWith(null) .FindAssignableWith(null) .FindAssignableWith(typeof(String)); 我们可能无法对我的实现做的是像上面那样调用FindInterfaceWith,因为有可能像IAlphaIBravo这样的关系。

我在某些情况下通过调用FindAssignableWith测试了代码,如下所示:

  • 可分配类型的输出 (Dictionary`2, Dictionary`2) = Dictionary`2 (List`1, List`1) = IList (Dictionary`2, KeyValuePair`2) = Object (IAlpha, IBravo) = <null> (IBravo, IAlpha) = <null> (ICollection, IList) = ICollection (IList, ICollection) = ICollection (Char[], Int32[]) = IList (Int32[], Char[]) = IList (IEnumerable`1, IEnumerable`1) = IEnumerable (String, Array) = Object (Array, String) = Object (Char[], Int32[]) = IList (Form, SplitContainer) = ContainerControl (SplitContainer, Form) = ContainerControl List'1测试出现IList是因为我用typeof(List<int>)测试了typeof(List<String>);和Dictionary'2都是Dictionary<String, String>。对不起,我没有做工作来提供确切的类型名称。

2
投票

最简单的情况是迭代一个对象的基类型并检查它们是否可以与另一个类型一起分配,如下所示:

  • public Type GetClosestType(Type a, Type b) { var t=a; while(a!=null) { if(a.IsAssignableFrom(b)) return a; a=a.BaseType; } return null; }

这将产生System.Object两种不相关的类型,如果它们都是类。我不确定这种行为是否符合您的要求。

对于更高级的情况,我使用名为IsExtendablyAssignableFrom的自定义扩展方法。

它可以处理不同的数字类型,泛型,接口,泛型参数,隐式转换,可空,装箱/拆箱,以及我在实现自己的编译器时遇到的几乎所有类型。

我已将代码上传到单独的github存储库[here],因此您可以在项目中使用它。


1
投票

如果你只查看基类,问题是微不足道的,并且Impworks的答案给出了一个解决方案(“迭代一个对象的父级并检查它们是否可以与另一个类型一起分配”)。

但是如果你想要包含接口,那么问题没有独特的解决方案,因为你会注意到自己的IDeltaICharlie示例。两个或更多接口可以很容易地同样“好”,因此没有单一的最佳解决方案。人们可以很容易地构造任意复杂的界面继承图(图),从这些图中很容易看出,没有明确定义的“FindAssignableWith”。

此外,C#中的协方差/逆变用于泛型类型的方差种类。让我举个例子。我们有

type1: System.Func<string>
type2: System.Func<Tuple<int>>

当然,对于基类,“FindAssignableWith”可能是

solutionA: System.MulticastDelegate

但类型Func<out T>在其类型参数out中也是协变的(T)。因此,类型

solutionB: System.Func<System.Object>

也是一个解决方案,它IsAssignableFrom两个给定类型type1type2。但同样可以这么说

solutionC: System.Func<System.IComparable>

这是有效的,因为stringTuple<>都是IComparable

所以在一般情况下,没有独特的解决方案。因此,除非您指定描述所需内容的精确规则,否则我们无法提出找到解决方案的算法。

© www.soinside.com 2019 - 2024. All rights reserved.