通过外部功能约束通用类型

时间:2019-12-11 20:05:12

标签: c# generics dynamic interface

背景

我正在与一个不断增长的数据类型集合的组织合作,他们需要从这些数据类型中提取数据。我无权更改这些数据类型。有些是由其他组织提供的XML文件由机器生成的;有些是由不动摇的内部开发团队控制的;有些太旧了,以至于没人担心会破坏整个地球的稳定并使其坠入太阳,因此没人愿意以任何方式改变它们。这些类不共享任何公共接口,并且不衍生自object以外的任何公共类型。下面给出了一些示例类以供说明:

    public class Untouchable
    {
        public string Data;
    }

    public class Unchangeable
    {
        public int Info;
    }

好消息是,在大多数情况下,我可以使用固定功能来从各种类的实例中获取至少一些数据。不幸的是,这些类中的大多数还具有怪异且特殊的数据,需要特定于类的逻辑才能从中提取数据。另外,信息经常需要保留在数据提取器内部,因为我从中提取数据的数据对象具有“交互作用”(不要问)。

我创建了一个抽象的泛型Extractor<T>类来用作通用方法的存储库,并创建了一个IExtractor<T>接口来作为访问功能的便捷句柄。我还具有此类的一些特定(泛型?)实现,可以从根据某些数据类型构建的业务对象中提取信息。这是一些示例代码来说明:

    public interface IExtractor<T>
    {
        string ExtractionOne(T something);
        string ExtractionTwo(T something);
    }

    public abstract class Extractor<T> : IExtractor<T>
    {
        private string internalInfo; // Certain business logic requires us to keep internal info over multiple objects that we extract data from.
        protected Extractor() { internalInfo="stuff"; }

        public virtual string ExtractionOne(T something)
        {
            return "This manipulation is generally applicable to most conceivable types.";
        }

        public abstract string ExtractionTwo(T something); // This DEFINITELY needs to be overridden for each class T
    }

    public class UntouchableExtractor : Extractor<Untouchable>
    {
        public UntouchableExtractor() : base() { }

        public override string ExtractionTwo(Untouchable something)
        {
            return something.Data;
        }
    }

    public class UnchangeableExtractor : Extractor<Unchangeable>
    {
        public UnchangeableExtractor() : base() { }

        public override string ExtractionTwo(Unchangeable something)
        {
            return something.Info.ToString();
        }
    }

我尚不支持所有可用的数据类型,但是管理层希望使用命令行界面将数据提取器推出给最终用户。他们告诉我,我们应该开始提取我们可以提取的数据,然后再进行其余处理。我和其他程序员将在时间允许的情况下增加对许多不可修改类型的支持,并且最终用户应能在我们的延迟范围内工作。在我们的实际环境中,这实际上是有道理的,所以就随它去吧。

问题:

我们要从中提取信息的数据类型列表非常大。在代码中维护受支持类型的显式列表将很棘手,而且容易出错-尤其是如果我们发现特定数据提取器有任何问题并且需要撤销支持,直到修复一些错误。

我想从单个入口点开始支持大量变化的受支持数据类型,该入口点根据传入的IExtractor<>动态确定要使用的dynamic dataObject的“正确版本”。如果没有实现IExtractor<>的类来支持给定的dataObject,则应该引发错误。

什么不起作用:

我尝试使用dynamic thing并使用typeof(Extractor<>).MakeGenericType(thing.GetType())创建Extractor<Untouchable>Extractor<Unchangeable>的实例,但是它们是抽象类,所以我不能使用{{ 1}}建立这些类的实例。这种方法的核心问题是不幸的是,它正在寻找格式为Activator.CreateInstance() class 而不是格式为Extractor<> interface 。 / p>

我曾考虑将诸如IExtractor<>之类的扩展方法放在某个类中,但我担心遇到一些不可触及的类中已经存在的名为IExtractor<T> BuildExtractor(this T something)的业务逻辑。这可能是一个不健康的偏执狂,但这就是我的处境。

我需要帮助的地方

我欢迎任何有关如何为不受约束的类集合创建单个入口点的建议。预先感谢。

2 个答案:

答案 0 :(得分:1)

以下代码段创建Extractor<T>类的具体实例,并动态调用该类的方法

var test = new Unchangeable();
var baseType = typeof(Extractor<>).MakeGenericType(test.GetType());
var extractorType = Assembly.GetExecutingAssembly()
    .GetTypes().FirstOrDefault(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(baseType));
if (extractorType != null)
{
    dynamic extractor = Activator.CreateInstance(extractorType);
    string result = extractor?.ExtractionTwo(test);
}

当然,它是经过简化的,您可以传递UnchangeableUntouchable类的特定实例并限制程序集类型的扫描(并且仅获取一次所有类型)。

这里的缺点是您必须注意ExtractionOneExtractionTwo签名,因为它们是在动态对象上调用的

  

这种方法的核心问题是不幸的是   形式为Extractor<>的类,而不是   表格IExtractor<>

此代码段可以帮助您使用IExtrator<>界面浏览类型

var baseType = typeof(IExtractor<>).MakeGenericType(typeof(Unchangeable));
var extractorType = Assembly.GetExecutingAssembly()
    .GetTypes().FirstOrDefault(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t));

答案 1 :(得分:1)

结合StackOverflow的一些代码和我自己的测试,我建议使用Reflection查找实现接口的所有类型:

public static class TypeExt {
    public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));
    public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) =>
        AppDomain.CurrentDomain.GetAssemblies()
                               .SelectMany(a => a.GetLoadableTypes())
                               .Distinct()
                               .Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
                                               (includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
                                               (includeStructs || !aType.IsValueType) &&
                                               (includeSystemTypes || !aType.IsBuiltin()) &&
                                               interfaceType.IsAssignableFrom(aType) &&
                                               aType.GetInterfaces().Contains(interfaceType));
}

public static class AssemblyExt {
    //https://stackoverflow.com/a/29379834/2557128
    public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
        if (assembly == null)
            throw new ArgumentNullException("assembly");
        try {
            return assembly.GetTypes();
        } catch (ReflectionTypeLoadException e) {
            return e.Types.Where(t => t != null);
        }
    }

}

反射可能会很慢,在我的测试中,获取所有已加载类型是最慢的部分,因此我添加了对已加载类型和实现类型的缓存,但这确实意味着您需要刷新已加载类型动态加载程序集:

public static class TypeExt {
    public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));

    static Dictionary<Type, HashSet<Type>> FoundTypes = null;
    static List<Type> LoadableTypes = null;

    public static void RefreshLoadableTypes() {
        LoadableTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetLoadableTypes()).ToList();
        FoundTypes = new Dictionary<Type, HashSet<Type>>();
    }

    public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) {
        if (FoundTypes != null && FoundTypes.TryGetValue(interfaceType, out var ft))
            return ft;
        else {
            if (LoadableTypes == null)
                RefreshLoadableTypes();

            var ans = LoadableTypes
                       .Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
                                       (includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
                                       (includeStructs || !aType.IsValueType) &&
                                       (includeSystemTypes || !aType.IsBuiltin()) &&
                                       interfaceType.IsAssignableFrom(aType) &&
                                       aType.GetInterfaces().Contains(interfaceType))
                       .ToHashSet();

            FoundTypes[interfaceType] = ans;

            return ans;
        }
    }
}

public static class AssemblyExt {
    //https://stackoverflow.com/a/29379834/2557128
    public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
        if (assembly == null)
            throw new ArgumentNullException("assembly");
        try {
            return assembly.GetTypes();
        }
        catch (ReflectionTypeLoadException e) {
            return e.Types.Where(t => t != null);
        }
    }
}

一旦拥有其中之一,就可以创建一个采用动态对象的工厂方法:

public static class ImplementingFactory {
    public static Type ExtractorType(dynamic anObject) {
        Type oType = anObject.GetType();
        var iType = typeof(IExtractor<>).MakeGenericType(oType);
        var ans = iType.ImplementingTypes().FirstOrDefault();
        if (ans == null)
            throw new Exception($"Unable to find IExtractor<{oType.Name}>");
        else
            return ans;
    }
}