当object是Task <Something>时获取Func <object>的结果

时间:2019-11-28 00:30:00

标签: c# delegates generic-programming

我当前正在使用此代码尝试动态执行已保存的Func<object>

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<Task<object>>Func<[anything]>,则此代码可以正常工作。但是,如果有人存储了Func<Task<string>>(或“任务”中的任何其他通用参数),它将中断。

Unable to cast object of type Func<Task<System.String>> to type Func<Task<System.Object>>

我的问题是:我现在如何等待Func<Task<Something>>的结果并将其值返回为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)();
        }
    }
}

1 个答案:

答案 0 :(得分:2)

我不清楚您的意图是什么,因为代码不清楚。您正在寻找一种关于返回类型的GetAwaiter()方法,但是当然还有Task以外的其他类型都具有此方法。而且,您的代码将丢失通过扩展方法可以等待的东西。

如果要假设函数返回一个任务对象(代码当前正在执行),则应检查该对象而不是GetAwaiter()方法。相反,您应该只动态地调用GetAwaiter()方法,以便容纳该方法中的所有内容。

就个人而言,如果此代码不会经常被调用,我将使用dynamic,尝试调用GetAwaiter(),如果失败则捕获异常(因为该方法不存在) ),然后直接调用委托。如果性能很重要,则可以记住类型输入等待者的状态,以便在单击一次异常之后可以跳过该异常。请注意,使用dynamic,您将适应大多数等待的情况(它仍然找不到扩展方法GetAwaiter())。

这是一个例子:

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多次遭受任何给定的委托目标方法。

一旦代码“热身”(即,在后续遍历中已被调用),它就不应成为瓶颈。

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

如果您不关心仅支持基于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<Task>,则上面的命令将无效,因为它假定所有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<VoidTaskResult>而不是非通用的Task类型,仅检查Task是不够的。另外,由于VoidTaskResult不是公共类型,因此代码必须将类型名称作为string值而不是typeof(Task<VoidTaskResult>)进行检查。所以,这有点尴尬。但这将解决以下情况:返回的内容是Task本身,而不是任务的结果。

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