繁体   English   中英

如何使用受约束的泛型类型参数将 class 实例化为派生接口

[英]How to instantiate a class as the interface that it derives from with constrained generic type parameter

以下接口定义了一个数据包。

public interface IPacket
{
    int Size { get; }
}

有两个实现,每个都有自己的附加属性。

public class FooPacket : IPacket
{
    public int Size => 10;
    public string FooProperty { get; set; }
}

public class BarPacket : IPacket
{
    public int Size => 20;
    public string BarProperty { get; set; }
}

以上是我无法控制的库代码。 我想为数据包创建一个处理程序

public interface IPacketHandler<T> where T : IPacket
{
    void HandlePacket(T packet) ;
}

并为具体数据包创建两个实现。

public class FooPacketHandler : IPacketHandler<FooPacket>
{
    public void HandlePacket(FooPacket packet) { /* some logic that accesses FooProperty */ }
}

public class BarPacketHandler : IPacketHandler<BarPacket>
{
    public void HandlePacket(BarPacket packet) { /* some logic that accesses BarProperty */ }
}

我想将数据包处理程序列表注入到管理数据包处理的 class 中,以便将来可以使用其他数据包处理程序对其进行扩展。

public class PacketHandlerManager
{
    public PacketHandlerManager(IEnumerable<IPacketHandler<IPacket>> packetHandlers)
    {

    }
}

我遇到的麻烦是在创建注入参数时。 我不能做

var packetHandlers = new List<IPacketHandler<IPacket>>
{
    new FooPacketHandler(),
    new BarPacketHandler()
};

因为我不能像这样创建一个实例:

IPacketHandler<IPacket> packetHandler = new FooPacketHandler();

我收到错误Cannot implicitly convert type 'FooPacketHandler' to 'IPacketHandler<IPacket>. An explicit conversion exists (are you missing a cast?) Cannot implicitly convert type 'FooPacketHandler' to 'IPacketHandler<IPacket>. An explicit conversion exists (are you missing a cast?)

我看了一个类似的问题: Casting generic type with interface constraint 在那个问题中,OP没有显示接口的成员,只是从generics的角度来定义它。 据我所知,如果我的接口没有使用泛型类型参数作为输入,我可以使用out关键字使其成为协变的,但这不适用于此处。

如何做到让管理者坚持开闭原则? 我唯一的办法是将接口定义更改为

public interface IPacketHandler
{
    void HandlePacket(IPacket packet);
}

然后在实现中转换为特定的数据包?

问题的核心是最终你会调用你的处理程序,将一个具体的数据包(具体类型)作为参数传递给它,即使你将参数隐藏在IPacket后面。

不知何故,尝试使用BarPacket参数调用HandlePacket( FooPacket )将不得不失败,唯一的问题是它何时/何地失败。

正如您已经注意到的那样,将通用参数引入数据包处理程序会使其在编译时失败,并且没有简单的解决方法。

您删除通用参数的想法,即拥有

public interface IPacketHandler
{
   void HandlePacket(IPacket packet);
}

是一个可能的解决方案。 然而,它将可能的失败推送到运行时,您现在必须检查是否使用不适当的参数调用了处理程序。

您还可以做的是通过为其引入合同来使此运行时检查更加明确:

public interface IPacketHandler
{
   bool CanHandlePacket(IPacket packet);
   void HandlePacket(IPacket packet);
}

这使消费者更容易安全地调用HandlePacket - 假设他们之前调用CanHandlePacket得到了肯定的结果。

例如,一个可能的对数据包列表的简单循环并调用您的处理程序将变为

foreach ( var packet in _packets )
  foreach ( var handler in _handlers )
    if ( handler.CanHandlePacket(packet) )
      handler.HandlePacket(packet);

你可以通过一点反思来解决这个问题。

首先,为了方便(并且对类型安全有一点帮助),引入一个“Tag”接口,你的所有IPacketHandler<T>接口都将实现它:

public interface IPacketHandlerTag // "Tag" interface.
{
}

这并不是真正必要的,但这意味着您可以稍后使用IEnumerable<IPacketHandlerTag>而不是IEnumerable<object> ,这确实使事情变得更加明显。

然后你的IPacketHandler<T>接口变成:

public interface IPacketHandler<in T> : IPacketHandlerTag where T : IPacket
{
    void HandlePacket(T packet);
}

现在您可以编写一个PacketHandlerManager使用反射来挑选用于处理数据包的方法,并将其添加到字典中,如下所示:

public class PacketHandlerManager
{
    public PacketHandlerManager(IEnumerable<IPacketHandlerTag> packetHandlers)
    {
        foreach (var packetHandler in packetHandlers)
        {
            bool appropriateMethodFound = false;
            var handlerType = packetHandler.GetType();
            var allMethods  = handlerType.GetMethods(BindingFlags.Public | BindingFlags.Instance);

            foreach (var method in allMethods.Where(m => m.Name == "HandlePacket"))
            {
                var args = method.GetParameters();

                if (args.Length == 1 && typeof(IPacket).IsAssignableFrom(args[0].ParameterType))
                {
                    _handlers.Add(args[0].ParameterType, item => method.Invoke(packetHandler, new object[]{item}));
                    appropriateMethodFound = true;
                }
            }

            if (!appropriateMethodFound)
                throw new InvalidOperationException("No appropriate HandlePacket() method found for type " + handlerType.FullName);
        }
    }

    public void HandlePacket(IPacket packet)
    {
        if (_handlers.TryGetValue(packet.GetType(), out var handler))
        {
            handler(packet);
        }
        else
        {
            Console.WriteLine("No handler found for packet type " + packet.GetType().FullName);
        }
    }

    readonly Dictionary<Type, Action<IPacket>> _handlers = new Dictionary<Type, Action<IPacket>>(); 
}

如果传递给PacketHandlerManager构造函数的数据包处理程序未使用可从IPacket分配的单个参数实现称为HandlePacket的方法,它将抛出InvalidOperationException

例如,尝试使用以下 class 的实例将导致构造函数抛出:

public class BadPacketHandler: IPacketHandlerTag
{
    public void HandlePacket(string packet)
    {
        Console.WriteLine("Handling string");
    }
}

现在您可以这样调用它:

    var packetHandlers = new List<IPacketHandlerTag>
    {
        new FooPacketHandler(),
        new BarPacketHandler()
    };

    var manager = new PacketHandlerManager(packetHandlers);

    var foo = new FooPacket();
    var bar = new BarPacket();
    var baz = new BazPacket();

    manager.HandlePacket(foo);
    manager.HandlePacket(bar);
    manager.HandlePacket(baz);

将它们放在一个可编译的控制台应用程序中:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace ConsoleApp1
{
    public interface IPacket
    {
        int Size { get; }
    }

    public class FooPacket : IPacket
    {
        public int    Size        => 10;
        public string FooProperty { get; set; }
    }

    public class BarPacket : IPacket
    {
        public int    Size        => 20;
        public string BarProperty { get; set; }
    }

    public class BazPacket : IPacket
    {
        public int    Size        => 20;
        public string BazProperty { get; set; }
    }

    public interface IPacketHandlerTag // "Tag" interface.
    {
    }

    public interface IPacketHandler<in T> : IPacketHandlerTag where T : IPacket
    {
        void HandlePacket(T packet);
    }

    public class FooPacketHandler : IPacketHandler<FooPacket>
    {
        public void HandlePacket(FooPacket packet)
        {
            Console.WriteLine("Handling FooPacket");
        }
    }

    public class BarPacketHandler : IPacketHandler<BarPacket>
    {
        public void HandlePacket(BarPacket packet)
        {
            Console.WriteLine("Handling BarPacket");
        }
    }

    public class PacketHandlerManager
    {
        public PacketHandlerManager(IEnumerable<IPacketHandlerTag> packetHandlers)
        {
            foreach (var packetHandler in packetHandlers)
            {
                bool appropriateMethodFound = false;
                var handlerType = packetHandler.GetType();
                var allMethods  = handlerType.GetMethods(BindingFlags.Public | BindingFlags.Instance);

                foreach (var method in allMethods.Where(m => m.Name == "HandlePacket"))
                {
                    var args = method.GetParameters();

                    if (args.Length == 1 && typeof(IPacket).IsAssignableFrom(args[0].ParameterType))
                    {
                        _handlers.Add(args[0].ParameterType, item => method.Invoke(packetHandler, new object[]{item}));
                        appropriateMethodFound = true;
                    }
                }

                if (!appropriateMethodFound)
                    throw new InvalidOperationException("No appropriate HandlePacket() method found for type " + handlerType.FullName);
            }
        }

        public void HandlePacket(IPacket packet)
        {
            if (_handlers.TryGetValue(packet.GetType(), out var handler))
            {
                handler(packet);
            }
            else
            {
                Console.WriteLine("No handler found for packet type " + packet.GetType().FullName);
            }
        }

        readonly Dictionary<Type, Action<IPacket>> _handlers = new Dictionary<Type, Action<IPacket>>(); 
    }

    class Program
    {
        public static void Main()
        {
            var packetHandlers = new List<IPacketHandlerTag>
            {
                new FooPacketHandler(),
                new BarPacketHandler()
            };

            var manager = new PacketHandlerManager(packetHandlers);

            var foo = new FooPacket();
            var bar = new BarPacket();
            var baz = new BazPacket();

            manager.HandlePacket(foo);
            manager.HandlePacket(bar);
            manager.HandlePacket(baz);
        }
    }
}

output 是:

处理 FooPacket

处理 BarPacket

找不到数据包类型 ConsoleApp1.BazPacket 的处理程序

感谢您的回答。 我最终得到的解决方案是这样的,从库代码开始:

public enum PacketType
{
    Foo,
    Bar
}

public interface IPacket
{
    PacketType Type { get; }
}

public class FooPacket : IPacket
{
    public PacketType Type => PacketType.Foo;
    public string FooProperty { get; }
}

public class BarPacket : IPacket
{
    public PacketType Type => PacketType.Bar;
    public string BarProperty { get; }
}

上面的版本是一个更好的近似真实的东西。

public interface IPacketHandler
{
    void HandlePacket(IPacket packet);
}

public abstract class PacketHandler<T> : IPacketHandler where T : IPacket
{
    public abstract PacketType HandlesPacketType { get; }

    public void HandlePacket(IPacket packet)
    {
        if (packet is T concretePacket)
        {
            HandlePacket(concretePacket);
        }
    }

    protected abstract void HandlePacket(T packet);
}

public class FooPacketHandler : PacketHandler<FooPacket>
{
    public override PacketType HandlesPacketType => PacketType.Foo;

    protected override void HandlePacket(FooPacket packet) { /* some logic that accesses FooProperty */ }
}

public class BarPacketHandler : PacketHandler<BarPacket>
{
    public override PacketType HandlesPacketType => PacketType.Bar;

    protected override void HandlePacket(BarPacket packet) { /* some logic that accesses BarProperty */ }
}

public class PacketHandlerManager
{
    public PacketHandlerManager(Library library, IEnumerable<IPacketHandler> packetHandlers)
    {
        foreach (var packetHandler in packetHandlers)
        {
            library.Bind(packetHandler.HandlesPacketType, packetHandler.HandlePacket);
        }
    }
}

PacketHandlerManager中还有一些逻辑,我在这里省略了。 library将数据包分派给处理程序,因此在使用Bind方法注册处理程序后,我不必显式处理它。

这不是我想象的那样,但它会做到的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM