【问题标题】:How to convert Func<Implementation, Task> to Func<Interface, Task> [duplicate]如何将 Func<Implementation, Task> 转换为 Func<Interface, Task> [重复]
【发布时间】:2020-10-02 11:15:44
【问题描述】:

我正在尝试将基于接口的特定实现的事件处理程序(任务)注册到属性Dictionary&lt;Type, Func&lt;Interface, Task&gt;&gt; _subscriptions;

我认为作为实现实现接口,我可以简单地将 Func&lt;Implementation, Task&gt; 转换为 Func&lt;Interface, Task&gt; 但这给了我一个运行时 InvalidCastException

Subscribe<HelloMessage>(OnHelloMessage);
Task OnHelloMessage(HelloMessage message)
{
// Handle message
}

void Subscribe<T>(Func<T, Task> handler)
           where T: class, IMessage, new()
{
  _subscriptions.TryAdd(typeof(T), (Func<IMessage, Task>) handler);
}

我希望处理函数接收实现的原因是为了避免写千遍if (message is HelloMessage helloMessage)

如何将Func&lt;HelloMessage, Task&gt; 转换为Func&lt;IMessage, Task&gt;?

【问题讨论】:

  • 你试过用IMessage替换T吗?
  • 您能否最小化您的示例,并使其可重现? AFAICS _subscriptions 字典与核心问题无关,而缺少重要的 IMessage 和 HelloMessage 类型的声明。

标签: c# generics casting


【解决方案1】:

您不能像那样将Func&lt;T, Task&gt; 转换为Func&lt;IMessage, Task&gt;,有几个非常好的理由。但是,您可以 将其转换为Delegate,然后在调用时转换回正确的类型。这在功能上等同于将实例转换为 Object 并再次返回。

这是一个示例:

public class TaskFactory
{
    private readonly Dictionary<Type, Delegate> _factories = new Dictionary<Type, Delegate>();
    
    public void Register<T>(Func<T, Task> factory)
    {
        _factories[typeof(T)] = factory;
    }
    
    public Task Invoke<T>(T value)
    {
        if (_factories.TryGetValue(typeof(T), out var del) && del is Func<T, Task> fn)
            return fn.Invoke(value);
        return Task.CompletedTask;
    }
}

这取决于编译时传递的类型,所以如果你传入IMessage,那么它将尝试为IMessage找到工厂方法,而不是实际的具体类型。

要在运行时处理正确的类型确定,您可以全力以赴并构造正确的类型以进行检查...或者相信您的代码不会搞砸任务工厂缓存的内容。这是值得信赖的版本:

public Task Invoke(object instance)
{
    if (_factories.TryGetValue(instance?.GetType(), out var del))
        return (Task)del.DynamicInvoke(instance);
    return Task.CompletedTask;
}

请注意,这里我们使用DynamicInvoke 运行工厂函数,并将结果转换回Task。只要您的_factories 字典的内容没有发生任何变化,那么您会没事的。诚实。

为了完整起见,这里是偏执的版本。这个找到实例类型,获取工厂方法,然后检查以确保工厂方法与我们期望的类型签名匹配。如果一切顺利那么它将使用DynamicInvoke 来运行函数。

public Task Invoke(object instance)
{
    if (instance is null)
        throw new ArgumentNullException(nameof(instance));
        
    // Get the true type of the instance
    var type = instance.GetType();
        
    if (_factories.TryGetValue(type, out var del))
    {
        // Get the type for Func<T, Task> where T is the true type of the parameter
        var deltype = typeof(Func<,>).MakeGenericType(type, typeof(Task));
            
        // check that the delegate matches our expected delegate type then execute
        if (deltype.IsAssignableFrom(del.GetType()))
            return (Task)del.DynamicInvoke(instance);
    }
    return Task.CompletedTask;
}

我在这里基本上忽略了IMessage 接口,但您可以简单地将其插入代码中的适当位置。


关于Subscribe 方法中的类型过滤主题,这似乎有点奇怪。你需要一个实现IMessage的类并且有一个默认的构造函数......但是你永远不会在Subscribe的任何地方构造对象的实例。通常,您只想在代码中放置您实际需要的那些限制,或者放在那里或者以后可能需要这些限制的地方。

【讨论】:

  • 我需要一个默认构造函数,因为需要访问我用作描述符的消息 ID。我序列化来自客户端 服务器的消息内容。
  • 感谢您提供此解决方案示例。它解决了我的问题,我学到了更多,非常感谢。谢谢!
【解决方案2】:

就您的代码而言,出现InvalidCastException 是因为类型解析规则不允许从Func&lt;HelloMessage, Task&gt; 转换为Func&lt;IMessage, Task。

有解决办法...

间接

您可以保持所有代码不变,例如

Dictionary<Type, Func<IMessage, Task>> _subscriptions = new Dictionary<Type, Func<IMessage, Task>>();

除了只更改订阅方法以注册一个调用您的处理程序的Func&lt;IMessage, Task&gt;(间接):

void Subscribe<T>(Func<T, Task> handler) where T: class, IMessage, new()
{
    _subscriptions.TryAdd(typeof(T), x => handler((T)x));
}

这允许您调用此代码

Subscribe<HelloMessage>(OnHelloMessage);
CallExample();

(人为的)CallExample 的样子

void CallExample() => _subscriptions[typeof(HelloMessage)](new HelloMessage());

动态

假设您希望保留 Subscribe 方法的通用签名,并且希望您的委托人保留其个人签名,解决此问题的一种方法是将字典的值设为 dynamic。

Dictionary<Type, dynamic> _subscriptions = new Dictionary<Type, dynamic>();

并将Subscribe 的实现更改为

void Subscribe<T>(Func<T, Task> handler) where T: class, IMessage, new()
{
    _subscriptions.TryAdd(typeof(T), (dynamic)handler);
}

【讨论】:

  • 老兄...为代表使用dynamic?我的意思是,我知道它有效,但仍然......它感觉很不对劲。而 IL 并不是那么直接。
  • @Corey 大多数应用程序级开发人员并不关心 IL。除非性能是一个问题,否则大多数人都不应该这样做。无论如何,我添加了替代方案:间接。还有更多的选择,但那是给其他回答者的......
  • “一般情况下,铸造不会“进入”通用参数以匹配它们。”这根本不是真的。正如副本所解释的那样,该通用参数只是逆变的,而不是协变的。参数可以更具体,而不是更具体。
  • @Servy 够公平的。我将删除这一点。然而,这些解决方案是有效的。
猜你喜欢
  • 1970-01-01
  • 2019-08-11
  • 2022-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多