【问题标题】:How to find the minimum covariant type for best fit between two types?如何找到两种类型之间最佳拟合的最小协变类型?
【发布时间】:2013-01-06 11:35:33
【问题描述】:

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[] 之间的类型。

【问题讨论】:

  • @DWright:如果还是不行请告诉我,非常感谢。
  • 更好。但“最可能”是什么意思?最多的组合数?
  • 现在我理解得更好了。我想知道标题是否可以更好地反映这一点。也许:“将类型分配给彼此的所有方法是什么?”,但这并不能涵盖您所谈论的所有内容。但这样的标题更能传达您正在调查的内容。

标签: c# types covariance contravariance


【解决方案1】:

更新:

事实证明FindInterfaceWith 可以简化,构建扁平的类型层次结构变得多余,因为不一定涉及基类,只要我们在作为接口时考虑类型本身即可;所以我添加了一个扩展方法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&lt;int&gt; 实现ICollection&lt;int&gt; 在这个图中没有用线链接。

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

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

  • 注意

    1. GetInterfaces 方法不按特定顺序返回接口,例如字母顺序或声明顺序。您的代码不能依赖于返回接口的顺序,因为该顺序会有所不同。

    2. 由于递归,基类始终是有序的。

    3. 如果两个接口具有相同的覆盖范围,则它们都不会被视为合格。

      假设我们定义了这些接口(或者类就可以了):

      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中,我使用:

  • 双重规则

    1. 通过调用CountOverlapped

    2. ,比较源接口数组中的两个接口,每个接口涵盖源中的其他接口数量
    3. 如果规则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。它实际上返回另一个传入的参数。

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

  • 方法链

    var type=
        typeof(int[])
            .FindBaseClassWith(null)
            .FindBaseClassWith(null)
            .FindBaseClassWith(typeof(char[]));
    

    它将返回typeof(Array);感谢这个功能,我们甚至可以调用

    var type=
        typeof(String)
            .FindAssignableWith(null)
            .FindAssignableWith(null)
            .FindAssignableWith(typeof(String));
    

    由于IAlphaIBravo 等关系的可能性,我们可能无法对我的实现进行调用FindInterfaceWith

我在某些情况下通过调用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&lt;String&gt;) 测试了typeof(List&lt;int&gt;);和Dictionary'2 都是Dictionary&lt;String, String&gt;。对不起,我没有做工作来提供确切的类型名称。

【讨论】:

  • 你:...(List`1, List`1) = IList ...对不起,我没有做工作来提供确切的类型名称。如果你有一个System.Type,比如@ 987654381@,如果你使用t.Name,你只会得到"List`1"。如果改为使用t.ToString(),您将获得更多信息:"System.Collections.Generic.List`1[System.Int32]"
  • @JeppeStigNielsen:啊,谢谢你。我的测试代码是用 Linq 编写的(因为很多情况下),但是 Find X^9 With 方法的返回值可能返回 null。我没有编写扩展方法来完成这项工作。实际上,要使显示的名称(带有实际的T)没有命名空间的名称,还有一些工作要做。
【解决方案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],因此您可以在项目中使用它。

【讨论】:

  • 代码非常庞大,尤其是与数字相关的部分。我相信 GitHub 上的存储库几乎是永恒的 :)
  • 我已经阅读了代码,并认为IsExtendablyAssignableFrom 可能不符合要求,但MostWideType 似乎很接近。
  • @KenKin,它不会自行解析最接近的类型。我相信您可以将MostWideType 用于数字,并将我原始帖子中的sn-p 使用IsExtendablyAssignableFrom 而不是IsAssignableFrom 用于引用类型。
【解决方案3】:

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

但是,如果您还想包含接口,则该问题没有唯一的解决方案,正如您在 IDeltaICharlie 示例中所指出的那样。两个或多个接口很容易同样“好”,因此没有单一的最佳解决方案。可以很容易地构建任意复杂的接口继承图(图),并且从这些图中很容易看出没有明确定义的“FindAssignableWith”。

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

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

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

solutionA: System.MulticastDelegate

Func&lt;out T&gt; 类型在其类型参数T 中也是协变 (out)。因此,类型

solutionB: System.Func<System.Object>

IsAssignableFrom 两个给定类型type1type2 的意义上也是一个解决方案。但也可以这样说

solutionC: System.Func<System.IComparable>

这很有效,因为stringTuple&lt;&gt; 都是IComparable

所以在一般情况下,没有唯一的解决方案。因此,除非您指定精确的规则来描述您想要什么,否则我们无法提出找到解决方案的算法。

【讨论】:

  • @KenKin 协方差是使Func&lt;object&gt;“可分配自”Func&lt;string&gt; 的原因,即使Func&lt;object&gt; 不是Func&lt;string&gt; 的基类。逆变是相似的,但反过来,例如IComparer&lt;string&gt; 是“可分配自”IComparer&lt;object&gt;。阅读What's the difference between covariance and assignment compatibility? 了解有关此术语的更多信息。另见MSDN help
猜你喜欢
  • 1970-01-01
  • 2012-12-15
  • 2019-07-12
  • 1970-01-01
  • 1970-01-01
  • 2014-12-02
  • 2019-04-23
  • 2012-07-02
  • 1970-01-01
相关资源
最近更新 更多