【问题标题】:Get the result of Func<object> when object is a Task<Something>当对象是 Task<Something> 时获取 Func<object> 的结果
【发布时间】:2019-11-28 00:30:00
【问题描述】:

我目前正在使用此代码尝试动态执行已保存的Func&lt;object&gt;

public async Task<object> GetFuncResult(string funcName) {
    Func<object> func = _savedFuncs[funcName];
    bool isAwaitable = func.Method.ReturnType.GetMethod(nameof(Task.GetAwaiter)) != null;
    if (!isAwaitable) return func();
    else return await ((Func<Task<object>>)func)();
}

如果有人存储Func&lt;Task&lt;object&gt;&gt;Func&lt;[anything]&gt;,则此代码可以正常工作。但如果有人存储 Func&lt;Task&lt;string&gt;&gt;(或任务中的任何其他通用参数),它就会中断。

Unable to cast object of type Func&lt;Task&lt;System.String&gt;&gt; to type Func&lt;Task&lt;System.Object&gt;&gt;

我的问题是:此时我如何等待Func&lt;Task&lt;Something&gt;&gt; 的结果并将该值作为object 返回?

完整的测试代码:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace TestConsole
{
    class Program
    {
        static Dictionary<string, Func<object>> _savedFuncs;

        static async Task Main(string[] args)
        {
            _savedFuncs = new Dictionary<string, Func<object>>();
            Func<Task<string>> myTask = async () => { return "Test Success"; };
            _savedFuncs.Add("myFunc", myTask);
            Console.WriteLine((await GetFuncResult("myFunc")) ?? "No Value Returned");
            Console.ReadKey();
        }

        public static async Task<object> GetFuncResult(string funcName)
        {
            Func<object> func = _savedFuncs[funcName];
            bool isAwaitable = func.Method.ReturnType.GetMethod(nameof(Task.GetAwaiter)) != null;
            if (!isAwaitable) return func();
            return await ((Func<Task<object>>)func)();
        }
    }
}

【问题讨论】:

  • 你为什么把 Object 作为泛型的类型? 使用对象作为类型实际上是发明/实现泛型的目的。在不知道这些任务可能具有的具体返回值的情况下,我们无能为力。
  • @Christopher 我很欣赏这个问题。这些函数实际上可以返回任何对象。这本质上是一个组合根,它允许用户注册一个表示对象的函数,该对象实际上可以是任何东西。稍后他们可以根据需要取出该对象。
  • 然后按字面意思执行Task&lt;T&gt; GetFuncResult&lt;T&gt;(string funcName)
  • @Selvin 这个例子你没有错。 (只是稍微)更复杂的实际代码需要用户的一些灵活性签名。他们中的大多数都是通用的,如GetFuncResult&lt;T&gt;(...),你的建议会奏效。但是其中有几个就像GetFuncResult(Type type, ...),这是行不通的。就像我希望的那样简单。我需要演示的非泛型方法签名才能工作。
  • 在很多情况下,反射并没有错,这可能就是其中之一。另外值得注意的是,反射是在内部缓存的,因此您的性能可能仍然很不错。

标签: c# delegates generic-programming


【解决方案1】:

我不完全清楚您的意图是什么,因为代码不清楚。您正在寻找返回类型上的GetAwaiter() 方法,但当然还有Task 以外的类型具有此方法。此外,您的代码将错过通过扩展方法等待的内容。

如果您要假设该函数返回一个任务对象(代码当前所做的),那么您应该只检查它而不是 GetAwaiter() 方法。相反,您应该只动态调用GetAwaiter() 方法,以便适应该方法的任何内容。

就个人而言,如果不经常调用此代码,我会使用dynamic,尝试调用GetAwaiter(),如果失败则捕获异常(因为该方法不存在)并调用直接代表。如果 perf 很重要,您可以记住 type-to-awaiter 状态,以便在您点击一次后可以跳过异常。请注意,使用dynamic,您将适应大多数等待的场景(它仍然找不到扩展方法GetAwaiter()s)。

这是一个例子:

private static readonly HashSet<MethodInfo> _notAwaitable = new HashSet<MethodInfo>();

public static async Task<object> GetFuncResult(string funcName)
{
    Func<object> func = _savedFuncs[funcName];
    dynamic result = func();

    if (!_notAwaitable.Contains(func.Method))
    {
        try
        {
            return await result;
        }
        catch (RuntimeBinderException) { } // not awaitable

        _notAwaitable.Add(func.Method);
    }

    return result;
}

这应该可以满足您的需求,并且还应该是高性能的。 dynamic 运行时支持已经缓存了可等待场景的分辨率,并且通过将不可等待的 MethodInfo 实例存储在哈希集中,代码避免了对于任何给定的委托目标方法不止一次遭受 RuntimeBinderException 的影响。

一旦代码“热身”(即以后续传递的方式调用),它就不应该成为瓶颈。

请注意,上述实现假设您使用多播委托。鉴于上下文,这似乎是一个合理的假设,因为没有内置语言支持可等待的多播委托(或者更确切地说,它会起作用,但运行时中没有任何东西可以解决关于 which 可等待的歧义正在等待)。但是,如果您确实需要,当然可以扩展上述内容以支持多播委托。

如果您不关心支持所有等待的场景,而只关心基于Task 的场景,您可以将上面的内容简化如下:

public static async Task<object> GetFuncResult(string funcName)
{
    Func<object> func = _savedFuncs[funcName];
    object result = func();

    if (result is Task task)
    {
        await task;
        return ((dynamic)task).Result;
    }

    return result;
}

这里,Task 的类型检查用于代替哈希集。同样,dynamic 运行时支持将为使用此方法的每种类型的任务缓存 Result 访问器,因此一旦预热,将与任何其他面向动态的解决方案一样执行。

最后,请注意,如果你有一个Func&lt;Task&gt;,上面的方法将不起作用,因为它假定所有Task 对象都有一个有效的结果。有人可能会争辩说,考虑到歧义,最好不要一开始就用类似的东西填充字典。但假设这种情况值得关注,则可以修改上述内容以考虑这种可能性:

public static async Task<object> GetFuncResult(string funcName)
{
    Func<object> func = _savedFuncs[funcName];
    object result = func();

    if (result is Task task)
    {
        Type resultType = result.GetType();

        // Some non-result task scenarios return Task<VoidTaskResult> instead
        // of a plain non-generic Task, so check for both.
        if (resultType != typeof(Task) &&
            resultType.GenericTypeArguments[0].FullName != "System.Threading.Tasks.VoidTaskResult")
        {
            await task;
            return ((dynamic)task).Result;
        }
    }

    return result;
}

不幸的是,因为在某些情况下编译器使用Task&lt;VoidTaskResult&gt; 而不是非泛型Task 类型,仅检查Task 是不够的。此外,因为VoidTaskResult 不是公共类型,代码必须检查类型名称是否为string 值而不是typeof(Task&lt;VoidTaskResult&gt;)。所以,有点尴尬。但它会解决返回的东西是Task 本身而不是任务结果的情况。

当然,GetAwaiter() 方法没有这个问题。因此,如果这确实令人担忧,那将是选择GetAwaiter() 方法而不是is Task 方法的原因之一。

【讨论】:

  • 你们俩都打字太快了,我没说出口。这不是每秒数百万次的循环或任何事情,但每次点击入口点时,此方法可能会被点击 5-20 次。真的不是什么大不了的事。我喜欢这里提到的一些想法,并且我确实看到代码示例伪代码没有完全编译,但我正在考虑其中的一些并尝试提出如何使用它。虽然我对你所说的一切都很熟悉,但我还不能完全掌握我拥有的这个小众用例以及如何使用这些概念。
  • Peter,我喜欢这两个代码示例。我确实找到了一种让它与反射一起工作的方法,但退缩了,我对dynamic 同样感到高兴和冷淡,但我想我会坚持使用你的动态示例。他们实际上测试得很好并且可以按我的预期工作。谢谢!
  • 除非有人把Func&lt;Task&gt;
  • 请注意,动态可能非常缓慢。我建议您对此进行基准测试。
  • @DavidG:我的经验是 DLR 缓存解决了最糟糕的性能问题。我不会在紧密循环中使用dynamic,但在大多数其他情况下它运行良好。尽管如此,基准测试的建议对于代码的任何性能关键区域始终有效。无论如何,我看不出有什么办法可以解决潜在的潜在性能问题。如果该方法是在运行时确定应该发生什么,则必然涉及某种类型的动态代码,无论是dynamic 本身还是基于反射的技术,它们做同样的事情但更明确。
猜你喜欢
  • 2018-02-16
  • 1970-01-01
  • 2016-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多