Get the result of Func<object> when object is a Task<Something>

谁说胖子不能爱 提交于 2020-01-24 10:11:46

问题


I am currently using this code to attempt to dynamically execute a saved 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)();
}

If somebody stores a Func<Task<object>> or a Func<[anything]> this code works fine. But if somebody stores a Func<Task<string>> (or any other generic argument in the Task), it breaks.

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

My question is: How do I await the result of the Func<Task<Something>> at this point and just return that value as an object?

Full Test Code:

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:


I'm not entirely clear on what your intent here is, because the code is not clear. You are looking for a GetAwaiter() method on the return type, but of course there are types other than Task which have this method. Also, your code will miss things that are awaitable by virtue of extension method.

If you are going to assume the function returns a task object (which the code currently does), then you should just check for that instead of the GetAwaiter() method. Conversely, you should just invoke the GetAwaiter() method dynamically, so that anything with the method is accommodated.

Personally, if this code isn't going to be invoked very often, I would use dynamic, try to call GetAwaiter(), catch the exception if that fails (because the method isn't present) and just invoke the delegate directly. If perf is important, you can memoize the type-to-awaiter status so that the exception can be skipped after you hit it once. Note that using dynamic, you'll accommodate most awaitable scenarios (it still won't find extension-method GetAwaiter()s).

Here is an example of that:

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;
}

This should do what you want, and should also be performant. The dynamic runtime support already caches the resolution of the awaitable scenario, and by storing the non-awaitable MethodInfo instances in the hash set, the code avoids ever suffering the RuntimeBinderException more than once for any given delegate target method.

Once the code's "warmed up" (i.e. has been invoked in the way it will on subsequent passes), it should not be a bottleneck.

Note that the implementation above assumes you are not using multicast delegates. Given the context, that seems like a reasonable assumption, since there's no built-in language support for awaitable multicast delegates (or rather, it will work, but nothing in the runtime resolves the ambiguity as to which awaitable is awaited). But you could of course extend the above to support multicast delegates if you really needed that.

If you don't care about supporting all awaitable scenarios, but only Task-based ones, you can simplify the above a bit like this:

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;
}

Here, the type-check for Task is used in lieu of the hash set. Again, the dynamic runtime support will cache the Result accessor for each type of task used with this method, so once warmed up will perform just as well as any other dynamically-oriented solution.

Finally, note that if you have a Func<Task>, the above won't work, because it assumes all Task objects have a valid result. One might argue that given the ambiguity, it'd be best to not populate the dictionary with anything like that in the first place. But assuming that scenario is of concern, the above can be modified to account for the possibility:

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;
}

Unfortunately, because in some cases the compiler uses Task<VoidTaskResult> instead of the non-generic Task type, it's not sufficient to just check for Task. In addition, because VoidTaskResult is not a public type, the code has to check for the type name as a string value instead of typeof(Task<VoidTaskResult>). So, it's a little awkward. But it will address the scenarios where the thing being returned is the Task itself, not the result of the task.

Of course, the GetAwaiter() approach doesn't have this issue. So if this was actually of concern, that would be one reason to choose the GetAwaiter() approach instead of the is Task approach.



来源:https://stackoverflow.com/questions/59080219/get-the-result-of-funcobject-when-object-is-a-tasksomething

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!