【问题标题】:How can I simulate inheriting from a generic parameter?如何模拟从泛型参数继承?
【发布时间】:2015-08-18 14:58:26
【问题描述】:

实际上无法从泛型参数继承的问题已经被讨论过很多次了。我的问题是,实际上,如何才能达到类似的效果?用例:

public interface ILotsOfMethods
{ 
    // Lots of methods go here
}

public class LotsOfMethods<T> : T, ILotsOfMethods
{
    public Func<string, bool> Method1Delegate { get; set; }
    public Func<string, bool> Method2Delegate { get; set; }
    // ...
    public Func<string, bool> MethodNDelegate { get; set; }

    public bool ILotsOfMethods.Method1(string str)
    {
        if (this.Method1Delegate != null)
        {
            return this.Method1Delegate(str);
        }

        throw new NotImplementedException();
    }

    // the other methods all follow this pattern
}

public class LotsOfMethodsList : List<ILotsOfMethods>, ILotsOfMethods
{
    public bool ILotsOfMethods.Method1(string str)
    {
        foreach (var handler in this)
        {
            try
            {
                return handler.Method1(str); // even if defined, this COULD throw NotImplementedException if it decides it's not interested in str
            }
            catch(NotImplementedException)
            { }
        }

        throw new NotImplementedException();
    }

    // the other methods all follow this pattern
}

通过这种设置,我可以轻松地通过编写仅实现我感兴趣的方法子集:

public class MethodAwareClient : ThirdPartyClient, ILotsOfMethods
{
    // concrete implementations of ILotsOfMethods as default handlers

    public void override Exec(Query query)
    {
        Log.Trace("Executing!");  // could be something more meaningful
        base.Exec(query); // third party code
    }

    public void override QueryComplete(Query query)
    {
        LotsOfMethodsList handlers;
        handlers.Add(this);
        handlers.Add(query as ILotsOfMethods); //null checking ommitted for brevity

        if (handlers.Method1("needsApproval"))
        {
            throw new BigBadException(); // Don't judge the logic of throwing after the query completes too harshly...simply for example!
        }
    }
}

public void Foo()
{
    Query normalQuery;
    LotsOfMethods<Query> scaryQuery;
    scaryQuery.Method1Handler = (string str) => { return true; };

    MethodAwareClient mac;
    mac.Exec(normalQuery);
    mac.Exec(scaryQuery); // this should throw
}

总体目标是允许任何类型的任何对象在每个实例的基础上轻松覆盖特定任务,而不必为每个想要实现某些功能的类生成ILotsOfMethods 的潜在大型实现。

一个更具体的例子:想象一个类MySqlClient,它实现了IQueryApproval 的基本处理程序,并从第三方SqlClient 继承。该接口将具有IQueryApproval.NeedsApproval(Query)IQueryApproval.GetApprovers(Query)。默认可能是更新需要批准,而选择不需要。但是,如果我正在编写一个我认为很危险的查询(无论是从 PII 的角度还是从性能的角度来看),也许我想覆盖这种行为。所以我将QueryApproval&lt;Query&gt; 传递给MySqlClient,它由SqlClient 以各种方式传递。通过继承 Query,即使MySqlClient 覆盖了方法CanRunQuery(Query query),我们也可以在保留SqlClient 定义的底层接口的同时维护有关Query 的上下文信息。

我可以通过将IQueryApproval 分解为INeedsApprovalIGetApprovers 并为每个实现委托包装器来了解如何做到这一点。我还可以通过在每个派生类中完全实现ILotsOfMethods 来了解如何做到这一点,但这两者似乎都有些笨拙。我承认我的上述方法也有点开销,但如果实现了接口的用户会简化他们的代码,这才是真正的目标。

【问题讨论】:

  • 那么,您的问题是,您可以使用什么代码/设计来获得与非编译示例代码或多或少相同的好处?
  • 除了设计的优点/缺点之外,您是否考虑过类似Decorator pattern 的东西?将 Method1MethodN 定义在未实现的基本 ILotsOfMethods 组件(每个抛出 NotImplementedException)上,定义一个通用的 abstract 基,将这些方法实现为 virtual 并默认调用包装的 @987654346 @instance 对应方法。每个具体实现MethodAwareClientoverride他们实现的方法。
  • 听起来类似于设置模拟对象(例如在Moq中)
  • Decorator 的问题在于(参考我的后一个示例)SqlClient 可能没有由Query 实现的IQuery。由于这个理论库是第 3 方代码,我无法自己更改实现。没有解决实际继承Query 的需要;问题是如何最好地去做。
  • @Rollie:好吧,如果你愿意重新实现ILotsOfMethods,你可以传入你正在装饰的包装对象,然后实现每个方法来调用该包装对象的等效方法。

标签: c# generics inheritance


【解决方案1】:

首先,我怀疑这可能是XY Problem 的情况。如果是这样,也许包括你正在尝试做的更完整的上下文。假设不是,那么……

其次,我担心使用大ILotsOfMethods;这很可能违反了Interface Segregation Principle,并且可能是您头痛的根本原因。我认为这应该是您考虑重新设计的第一个途径。假设这个界面设计是固定的/最适合你,那么......

根据您的 cmets,也许这是基于 Decorator pattern 的可行设计。

首先,定义基本接口和装饰器:

public interface ILotsOfMethods
{ 
    // Lots of methods go here
    bool Method1(string arg);
    bool Method2(string arg);
    TimeSpan Method6(string arg);
}

public abstract class LotsOfMethodsDecorator : ILotsOfMethods
{
    private readonly ILotsOfMethods LotsOfMethodsToBeDecorated;

    protected LotsOfMethodsDecorator(ILotsOfMethods lotsOfMethods)
    {
        this.LotsOfMethodsToBeDecorated = lotsOfMethods;
    }

    public virtual bool Method1(string arg)
    {
        return LotsOfMethodsToBeDecorated.Method1(arg);
    }

    public virtual bool Method2(string arg)
    {
        return LotsOfMethodsToBeDecorated.Method2(arg);
    }

    public virtual TimeSpan Method6(string arg)
    {
        return LotsOfMethodsToBeDecorated.Method6(arg);
    }
}

然后是一个公共终止点,它将为每个方法抛出一个NotImplementedException

public class LotsOfMethodsNotImplemented : ILotsOfMethods
{
    public bool Method1(string arg)
    {
        throw new NotImplementedException();
    }

    public bool Method2(string arg)
    {
        throw new NotImplementedException();
    }

    public TimeSpan Method6(string arg)
    {
        throw new NotImplementedException();
    }
}

方法感知客户端现在可以简单地覆盖他们知道如何实现的方法:

public class Method1Client : LotsOfMethodsDecorator
{
    public Method1Client(ILotsOfMethods lotsOfMethods)
        : base(lotsOfMethods)
    {

    }

    public override bool Method1(string arg)
    {
        return  arg == "shouldContinue";
    }
}

不能从基础装饰器继承的第三方组件应该用基础装饰器包装,并且可以执行第三方特定代码:

public class Some3rdPartQueryWrapper : LotsOfMethodsDecorator
{
    private readonly Some3rdPartyQuery Query;

    public Some3rdPartQueryWrapper(Some3rdPartyQuery query, ILotsOfMethods lotsOfMethods)
        : base(lotsOfMethods)
    {
        this.Query = query;
    }

    public override bool Method2(string arg)
    {
        if (this.Query.MaybeSomeThirdPartyValidation())
            return false;
        else
            return this.Query.SomeThirdPartyMethod2(arg);
    }
}

现在,我对您实现ILotsOfMethods 的高级“MethodAwareClient”有点困惑。出于此答案的目的,我将假设这也是一个装饰器,但您可以轻松地将其更改为简单地包装装饰器,或者理想情况下只是一个 ILotsOfMethods 实例:

public class MethodAwareClient : LotsOfMethodsDecorator
{
    public MethodAwareClient(ILotsOfMethods lotsOfMethods)
        : base(lotsOfMethods)
    {

    }

    public void HandleForm()
    {
        if (Method1("shouldContinue"))
        {
            //...
        }

        if (Method2("needSleep"))
        {
            Task.Delay(Method6("sleepTime"));
        }
    }
}

将它们连接在一起:

var some3rdPartyQuery = new Some3rdPartyQuery();

var client = 
    new MethodAwareClient(
    new Some3rdPartQueryWrapper(some3rdPartyQuery, 
    new Method1Client(
    new LotsOfMethodsNotImplemented()
    )));

client.HandleForm();

嵌套可能会很麻烦,但一些 API 调整可以解决这个问题。

再次,我怀疑这不是您面临的具体问题的理想解决方案。我强烈建议您首先考虑我最初的两个问题(XY 问题和接口隔离原则)。

【讨论】:

  • 唯一的问题是在第 3 方内部传递的上下文将会丢失。请参阅 MethodAwareClientFoo 的更新以获取说明。
  • @Rollie:然后提供MethodAwareClient : ThirdPartyClient 类对您的ILotsOfMethods 装饰对象的引用?为什么MethodAwareClient需要实现ILotsOfMethods
  • MethodAwareClient 实际上根本不需要实现ILotsOfMethods。我可以提供MethodAwareClient 对不相关的ILotsOfMethods 实现的引用,但我正在寻找的是基于每个查询 的引用。查看我在原帖中更新的示例代码:QueryComplete 将被调用两次,一次用于normalQuery,一次用于scaryQueryQueryComplete 中的逻辑如何知道哪个是哪个?其中一个需要基于特定查询的附加逻辑,而一个不需要。
【解决方案2】:

我遇到了类似的问题。 我有一个我不能指望在运行时可用的程序集,但如果它存在,我希望能够使用其中的对象。

问题是我不能在我自己的内部引用程序集。

假设我想要的对象是这样的:

public class RunTimeClass {
    public RunTimeClass () { }
    public void DoSomethingCool() { /*...*/ }
    public string SomeProp { get; set; }
}

我所做的是定义一个匹配这个类的接口:

public class ICompileTime {
   void DoSomethingCool();
   string SomeProp { get; set; }
}

然后我写了一些代码,给定接口或抽象类,一个程序集和一个类的名称将动态编写一个实现接口的新类,并将所有方法重新路由到程序集中的目标类。实际上,这使得动态适配器连接到RunTimeClass

用法:

Assembly asm = Assembly.Load("DoNothing");
Type t = asm.GetType("DoNothing.Nothing");

AdapterCompiler compiler = new AdapterCompiler();
AdapterFactory<NothingAdapter> factory = compiler.DefineAdapter<NothingAdapter>(t);

NothingAdapter adapter = factory.Construct(new object[] { "dave" });

int sum = adapter.Sum(1, 2, 3);

我将继续将整个 shebang 放在这里 - 它很长,但不是超长。一、AdapterCompiler:

public class AdapterCompiler
{
    public AdapterFactory<T> DefineAdapter<T>(Type targetType)
    {
        return DefineAdapter<T>(targetType, null);
    }

    public AdapterFactory<T> DefineAdapter<T>(Type targetType, string outputFile)
    {
        Type abstractType = typeof(T);
        AssemblyBuilder ab = null;

        // Get the TypeBuilder for the new class
        TypeBuilder tb = GetTypeBuilder(abstractType, out ab, outputFile);

        // Make a field for the target type within the new class
        FieldBuilder fb = DefineTargetObjectField(tb, targetType);

        // Map the abstract methods onto the target type
        DefineAbstractMethods(tb, fb, abstractType, targetType);

        // make a constructor that can vector out to the target type
        DefineConstructor(tb, fb, abstractType, targetType);

        // build the class
        Type adaptedType = tb.CreateType();

        if (outputFile != null)
            ab.Save(outputFile);

        // Make a factory object for building the class
        return new AdapterFactory<T>(adaptedType, targetType);
    }

    private string GetTargetObjectFieldName(Type targetType)
    {
        // create a consistent name mangled field name
        return "_f" + targetType.Name;
    }

    private FieldBuilder DefineTargetObjectField(TypeBuilder tb, Type targetType)
    {
        // Define the actual field
        return tb.DefineField(GetTargetObjectFieldName(targetType), targetType, FieldAttributes.Private);
    }

    private TypeBuilder GetTypeBuilder(Type abstractType, out AssemblyBuilder assemblyBuilder, string outputFile)
    {
        // make an assembly builder, module builder and type builder.
        // we only need the AssemblyBuilder and TypeBuilder
        AssemblyName assemName = new AssemblyName("Assembly" + abstractType.Name);
        assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemName,
            outputFile != null ? AssemblyBuilderAccess.RunAndSave : AssemblyBuilderAccess.Run);
        ModuleBuilder mb = outputFile != null ?
            assemblyBuilder.DefineDynamicModule(assemName.Name, outputFile) :
            assemblyBuilder.DefineDynamicModule(assemName.Name);
        TypeBuilder tb = mb.DefineType("Wrapped"+abstractType.Name, abstractType.Attributes & ~(TypeAttributes.Abstract), abstractType);
        return tb;
    }

    private void DefineAbstractMethods(TypeBuilder tb, FieldBuilder fb, Type abstractType, Type targetType)
    {
        // we need to pass along a list of already defined fields
        List<string> definedProperties = new List<string>();

        // get the abstract methods - tricky - use a lambda expression to filter via LINQ
        IEnumerable<MethodInfo> methods = abstractType.GetMethods().Where(mi => mi.IsAbstract);

        foreach (MethodInfo mi in methods)
        {
            DefineAbstractMethod(tb, fb, mi, targetType, definedProperties);
        }
    }

    private Type[] GetParameterTypes(MethodInfo mi)
    {
        // given a method, get a list of the types of its parameters
        ParameterInfo[] pi = mi.GetParameters();
        Type[] types = new Type[pi.Length];

        for (int i = 0; i < pi.Length; i++)
        {
            types[i] = pi[i].ParameterType;
        }
        return types;
    }

    private Type[] GetParameterTypes(PropertyInfo propi)
    {
        // given a property, get a list of the types of its parameters
        ParameterInfo[] pi = propi.GetIndexParameters();
        Type[] types = new Type[pi.Length];
        for (int i = 0; i < pi.Length; i++)
        {
            types[i] = pi[i].ParameterType;
        }
        return types;
    }


    private void VerifyParameterMatch(string methodName, Type[] source, Type[] dest)
    {
        // type match parameters for one method to another
        if (source.Length != dest.Length)
        {
            throw new Exception("In method " + methodName + ", parameter list length for wrapper and target object differ.");
        }

        for (int i = 0; i < source.Length; i++)
        {
            if (source[i] != dest[i])
            {
                throw new Exception("In method " + methodName + ", expected parameter " + i + " to be type " + dest[i].Name + ", but found " +
                    source[i].Name + ".");
            }
        }
    }

    private void DefineAbstractMethod(TypeBuilder tb, FieldBuilder fb, MethodInfo mi, Type targetType, List<string> definedProperties)
    {
        // map the abstract method from one class into the concrete implementation in another

        Type[] parameterTypes = GetParameterTypes(mi);

        // get the target method
        MethodInfo targetMethod = targetType.GetMethod(mi.Name, parameterTypes);

        if (targetMethod == null)
            throw new Exception("Unable to find matching method for " + mi.Name + " in class " + targetType.Name);

        // get the target method's parameter types
        Type[] targetParameterTypes = GetParameterTypes(targetMethod);

        // Ensure that they match, throw on failure
        VerifyParameterMatch(mi.Name, parameterTypes, targetParameterTypes);

        MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.ReuseSlot;
        // IsSpecialName is equivalent to "implementation of a property"
        if (mi.IsSpecialName)
        {
            attrs = attrs | MethodAttributes.SpecialName;
            // we only really handle get_/set_ properties
            if (mi.Name.StartsWith("get_") || mi.Name.StartsWith("set_"))
            {
                string propName = mi.Name.Substring(4);
                if (!definedProperties.Contains(propName))
                {
                    definedProperties.Add(propName);
                    PropertyInfo pi = mi.DeclaringType.GetProperty(propName);
                    tb.DefineProperty(propName, pi.Attributes, pi.PropertyType, GetParameterTypes(pi));
                }
            }
        }

        // define the method
        MethodBuilder mb = tb.DefineMethod(mi.Name, attrs, mi.ReturnType, parameterTypes);

        ILGenerator gen = mb.GetILGenerator();
        // fetch the target object
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Ldfld, fb);

        // pass all of our parameters on
        for (int i = 0; i < parameterTypes.Length; i++)
        {
            gen.Emit(OpCodes.Ldarg, i + 1);
        }

        // call the method, virtually or otherwise
        gen.Emit(targetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, targetMethod);
        gen.Emit(OpCodes.Ret);
    }

    private void DefineConstructor(TypeBuilder tb, FieldBuilder fb, Type abstractType, Type targetType)
    {
        // define a construct of the form:
        // .ctor(Type targetObjectType, Type[] constructorArgumentTypes, object[] constructorArguments)
        //
        // This implementation is effectively:
        // ConstructorInfo ci = targetObjectType.GetConstructor(constructorArgumentTypes)
        // _ftargetType = ci.Invoke(constructorArguments)
        //

        ConstructorBuilder cb = tb.DefineConstructor(MethodAttributes.Public,
            CallingConventions.Standard,
            new Type[] { typeof(Type), typeof(Type[]), typeof(object[]) });
        ILGenerator gen = cb.GetILGenerator();

        // see if there is a default constructor in the abstract type
        ConstructorInfo ci = abstractType.GetConstructor(Type.EmptyTypes);
        if (ci == null)
        {
            // if not, get Object contructor
            Type objectType = typeof(object);
            ci = objectType.GetConstructor(Type.EmptyTypes);
        }
        // call base class constructor
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Call, ci);

        // needed later
        gen.Emit(OpCodes.Ldarg_0);

        // get the constructor from targetObjectType
        gen.Emit(OpCodes.Ldarg_1);
        gen.Emit(OpCodes.Ldarg_2); 
        MethodInfo mi = typeof(Type).GetMethod("GetConstructor", new Type[] { typeof(Type[]) });
        gen.Emit(OpCodes.Callvirt, mi);

        // stack is now:
        // 0: this
        // 1: constuctor info

        // invoke the constructor on the arguments
        gen.Emit(OpCodes.Ldarg_3);
        mi = typeof(ConstructorInfo).GetMethod("Invoke", new Type[] { typeof(object[]) });

        gen.Emit(OpCodes.Callvirt, mi);

        // store into _ftargetType
        gen.Emit(OpCodes.Stfld, fb);
        gen.Emit(OpCodes.Ret);
    }
}

接下来是 AdapterFactory:

// An AdapterFactory is a class that given a type T, will construct a derived class (generated by AdapterCompiler)
// and call its constructor
//
public class AdapterFactory<T>
{
    private Type _adaptedType, _targetType;

    public AdapterFactory(Type adaptedType, Type targetType)
    {
        _adaptedType = adaptedType;
        _targetType = targetType;
    }

    public T Construct(object[] arguments)
    {
        // get the constructor
        ConstructorInfo ci = _adaptedType.GetConstructor(new Type[] { typeof(Type), typeof(Type[]), typeof(object[]) });

        // invoke it
        Type[] argTypes = GetArgTypes(arguments);
        return (T)ci.Invoke(new object[] { _targetType, argTypes, arguments });
    }

    private Type[] GetArgTypes(object[] objs)
    {
        Type[] types = new Type[objs.Length];
        for (int i = 0; i < types.Length; i++)
        {
            types[i] = objs[i].GetType();
        }
        return types;
    }
}

【讨论】:

    【解决方案3】:

    听起来您希望能够为之前定义的类添加功能。

    尝试使用 Castle.DynamicProxy2 创建 mixin。这是一个关于这样做的文章的链接http://kozmic.net/2009/08/12/castle-dynamic-proxy-tutorial-part-xiii-mix-in-this-mix/

    总体思路类似于创建装饰器,不同之处在于不必自己创建转发方法。

    大概,根据教程,你会做这样的事情:

    public static TBaseType CreateMixin<TBaseType, TAddon>(Func<TAddon> constructAddon) where TBaseType : class where TAddon : class
    {
        var generator = new ProxyGenerator();
        var options = new ProxyGenerationOptions();
        options.AddMixinInstance(constructAddon());
        return (TBaseType)generator.CreateClassProxy(typeof(TBaseType), new Type[]{typeof(TAddon)}, options);
    }
    

    此方法返回的实际上是TBaseType 的子类,它还实现了TAddon,然后将对该接口的调用转发给在constructAddon 工厂方法中创建的实例。

    这样称呼它:

    public void Foo()
    {
        Form normalForm = CreateMixin<Form, ILotsOfMethods>(()=>new LotsOfMethods());
        ILotsOfMethods wrappedForm = (ILotsOfMethods) normalForm;
        wrappedForm.Method1Handler = (string str) => { return true; };
    
        Database normalDatabase = CreateMixin<Database, ILotsOfMethods>(()=>new LotsOfMethods());
        ILotsOfMethods wrappedDatabase = (ILotsOfMethods) normalDatabase;
        wrappedDatabase.Method3Handler = (string str) => { return false; };
    
        MethodAwareClient mac;
        mac.ProcessForm(wrappedForm);
        mac.ProcessDatabase(wrappedDatabase);
    }
    

    请注意,这只会添加到非密封类。虽然库提供了其他选项,但如果它们实现了您可以使用的接口,您仍然可以使用密封类。然后,您可以创建一个实现目标类的主接口和附加接口的代理,该代理会将调用转发到主接口的目标类实例和附加类的附加类实例。关于方法。

    这是一个link(http://www.somethingorothersoft.com/2010/08/12/type-safe-dynamic-mixins/),显示了基于它们实现的接口混合三个类,合并成一个复合接口。请注意,使用此技术您可以合并多个接口,这可能允许您将ILotsOfMethods 分解为多个IAFewRelatedMethod 接口。

    【讨论】:

      猜你喜欢
      • 2011-09-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-07-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多