在C#中将Task<T>转换为Task<object>而不需要T。

38

我有一个静态类,其中包含许多扩展方法,每个方法都是异步的并返回一些值 - 就像这样:

public static class MyContextExtensions{
  public static async Task<bool> SomeFunction(this DbContext myContext){
    bool output = false;
    //...doing stuff with myContext
    return output;
  }

  public static async Task<List<string>> SomeOtherFunction(this DbContext myContext){
    List<string> output = new List<string>();
    //...doing stuff with myContext
    return output;
  }
}

我的目标是能够从另一个类的单个方法中调用这些方法中的任何一个,并将它们的结果作为对象返回。它会像这样:

public class MyHub: Hub{
  public async Task<object> InvokeContextExtension(string methodName){
    using(var context = new DbContext()){
      //This fails because of invalid cast
      return await (Task<object>)typeof(MyContextExtensions).GetMethod(methodName).Invoke(null, context);
    }
  }
}

问题在于转换失败。我的困境是无法向“InvokeContextExtension”方法传递任何类型参数,因为它是SignalR中心的一部分,并且由JavaScript调用。在某种程度上,我并不关心扩展方法的返回类型,因为它只会被序列化为JSON并发送回JavaScript客户端。但是,我必须将Invoke返回的值强制转换为一个Task,以便使用await运算符。我必须提供一个泛型参数与该“Task”,否则它将把返回类型视为void。所以问题就在于如何成功地将带有泛型参数T的Task强制转换为带有对象泛型参数的Task,其中T表示扩展方法的输出。

3
为什么不使用基类Task呢?反正你最终仍需使用反射来获取结果。或者编写一些方法,为你掩盖这些差异:async Task<object> GetResult<TResult>(Task<TResult> task) { return await task; } - ta.speot.is
8个回答

35
你可以分两步完成 - 使用基类await任务,然后使用反射或dynamic收集结果:
using(var context = new DbContext()) {
    // Get the task
    Task task = (Task)typeof(MyContextExtensions).GetMethod(methodName).Invoke(null, context);
    // Make sure it runs to completion
    await task.ConfigureAwait(false);
    // Harvest the result
    return (object)((dynamic)task).Result;
}

以下是一个完整的运行示例,将上述通过反射调用Task技术放入上下文中:
class MainClass {
    public static void Main(string[] args) {
        var t1 = Task.Run(async () => Console.WriteLine(await Bar("Foo1")));
        var t2 = Task.Run(async () => Console.WriteLine(await Bar("Foo2")));
        Task.WaitAll(t1, t2);
    }
    public static async Task<object> Bar(string name) {
        Task t = (Task)typeof(MainClass).GetMethod(name).Invoke(null, new object[] { "bar" });
        await t.ConfigureAwait(false);
        return (object)((dynamic)t).Result;
    }
    public static Task<string> Foo1(string s) {
        return Task.FromResult("hello");
    }
    public static Task<bool> Foo2(string s) {
        return Task.FromResult(true);
    }
}

这很好,但如果我有一个可能是TaskTask<T>类型的Task对象怎么办?如果我将它转换为动态类型,除了捕获RuntimeBinderException异常之外,如何检查它是否具有Result属性? - Paul Knopf
@PaulKnopf 首先需要检查参数,如果任务不是结果任务,则抛出异常。 - shtse8
我的基准测试显示,dynamic 的性能几乎与直接、类型化访问 .Result 相当。反射慢了一个数量级。 - Arshia001

29
一般来说,要将 Task<T> 转换为 Task<object>,我会选择直接使用简单的 continuation mapping:
Task<T> yourTaskT;

// ....

Task<object> yourTaskObject = yourTaskT.ContinueWith(t => (object) t.Result);

(此处为文档链接)


然而,您实际上的特定需求是通过反射调用Task并获取其(未知类型)结果

为此,您可以参考完整的dasblinkenlight的答案,这应该适合您的确切问题。


2
OP的问题在于他通过反射调用任务,所以他没有T,也无法创建yourTaskT。他能做的最好的就是使用Task,但这样他就不能在ContinueWith中使用t.Result了。 - Sergey Kalinichenko
是的。不幸的是,看起来我过于关注标题“一般问题”,而忽略了实际的提问者问题。 - Pac0
我同意,标题和问题之间并不完全一致。标题很直接,但问题有一个重要的转折点。虽然你的答案可能不能解决OP的问题,但它与当前标题完美匹配。我不会删除它 - 相反,我会编辑添加第二部分,展示如何通过反射调用具有未知结果类型的任务。 - Sergey Kalinichenko
谢谢您的建议。我已经在您的回答中添加了一个链接,对于这个“第二部分”,我想不出更好的方法,也不喜欢把别人的答案复制到我的回答中。 - Pac0

8

我想提供一个实现,这是在我看来早期答案的最佳组合:

  • 精准的参数处理
  • 无动态分派
  • 通用扩展方法

下面是具体实现:

/// <summary> 
/// Casts a <see cref="Task"/> to a <see cref="Task{TResult}"/>. 
/// This method will throw an <see cref="InvalidCastException"/> if the specified task 
/// returns a value which is not identity-convertible to <typeparamref name="T"/>. 
/// </summary>
public static async Task<T> Cast<T>(this Task task)
{
    if (task == null)
        throw new ArgumentNullException(nameof(task));
    if (!task.GetType().IsGenericType || task.GetType().GetGenericTypeDefinition() != typeof(Task<>))
        throw new ArgumentException("An argument of type 'System.Threading.Tasks.Task`1' was expected");

    await task.ConfigureAwait(false);

    object result = task.GetType().GetProperty(nameof(Task<object>.Result)).GetValue(task);
    return (T)result;
}

object result = task.GetType().GetProperty(nameof(Task<object>.Result)).GetValue(task); 这个代码中的 GetValue 不会很慢吗?我的意思是,它与 dynamic 调度相比如何? - morty
2
我不知道你对“足够快”的定义是什么。但如果你真的想消除这样的性能损失,你可能不应该想要开始转换任务。 - JBSnorro

6

你不能将 Task<T> 强制转换为 Task<object>,因为 Task<T> 不是协变的(也不是逆变的)。最简单的解决方法是使用更多的反射:

var task   = (Task) mi.Invoke (obj, null) ;
var result = task.GetType ().GetProperty ("Result").GetValue (task) ;

这种方法虽然低效而且速度较慢,但如果不经常执行此代码,它仍可用。顺带一提,如果您将阻塞等待其结果,那么拥有异步MakeMyClass1方法的意义何在?

另一种可能性是编写一个扩展方法以实现此目的:

  public static Task<object> Convert<T>(this Task<T> task)
    {
        TaskCompletionSource<object> res = new TaskCompletionSource<object>();

        return task.ContinueWith(t =>
        {
            if (t.IsCanceled)
            {
                res.TrySetCanceled();
            }
            else if (t.IsFaulted)
            {
                res.TrySetException(t.Exception);
            }
            else
            {
                res.TrySetResult(t.Result);
            }
            return res.Task;
        }
        , TaskContinuationOptions.ExecuteSynchronously).Unwrap();
    }

这是一种非阻塞的解决方案,可以保留任务的原始状态/异常。


2
尽管你的第一个解决方案肯定可行,但 OP 将无法利用你的第二个解决方案,因为他没有调用中 this Task<T> task 部分所需的 T - Sergey Kalinichenko
@dasblinkenlight 这只是为了未来考虑 :) 也许其他人到这一点需要它。我只是想要一个完整的答案。当然,我说过“另一种可能性”。谢谢你回复,duo。 - M.R.Safari
2
这是一个不错的解决方案,因为它保留了取消状态。如果您不关心这一点,那么 public static async Task<object> Convert<T>(this Task<T> t) => await t; 是非常简洁的。 - Eric Lippert
@M.R.Safari,感谢您提供的解决方案。我已经将dasblinkenlight的解决方案标记为答案,但这个也可以工作。我不确定这两个方案是否有任何性能差异。 - ncarriker
@M.R.Safari,我也想回答你的附加问题:扩展方法既可以从服务器端逻辑调用,也可以从SignalR Hub调用,因此有时它们会被等待,有时则不会。Hub方法是异步的,因为即使在真实代码中必须等待扩展方法的结果,还有其他未等待的任务也会被调用。 - ncarriker

4
最高效的方法是使用自定义等待器:
struct TaskCast<TSource, TDestination>
    where TSource : TDestination
{
    readonly Task<TSource> task;

    public TaskCast(Task<TSource> task)
    {
        this.task = task;
    }

    public Awaiter GetAwaiter() => new Awaiter(task);

    public struct Awaiter
        : System.Runtime.CompilerServices.INotifyCompletion
    {
        System.Runtime.CompilerServices.TaskAwaiter<TSource> awaiter;

        public Awaiter(Task<TSource> task)
        {
            awaiter = task.GetAwaiter();
        }

        public bool IsCompleted => awaiter.IsCompleted;    
        public TDestination GetResult() => awaiter.GetResult();    
        public void OnCompleted(Action continuation) => awaiter.OnCompleted(continuation);
    }
}

以下是使用方法:

Task<...> someTask = ...;
await TaskCast<..., object>(someTask);

这种方法的局限性在于结果不是一个Task<object>,而是一个可等待的对象。

3

我基于dasblinkenlight的回答编写了一个小扩展方法:

public static class TaskExtension
{
    public async static Task<T> Cast<T>(this Task task)
    { 
        if (!task.GetType().IsGenericType) throw new InvalidOperationException();

        await task.ConfigureAwait(false);

        // Harvest the result. Ugly but works
        return (T)((dynamic)task).Result;
    }
}

使用方法:

Task<Foo> task = ...
Task<object> = task.Cast<object>();

这样你就可以将 Task<T> 中的 T 更改为任何想要的内容。


你应该检查参数以防止传递空任务。 - shtse8
@shtse8 这样的代码 if(task == null) throw new ArgumentNullException() 够用了吗? - Mariusz Jamro
如果任务的类型不是泛型,就抛出无效操作异常。因为类型可能是 void 任务,没有任何 Result 属性,导致运行时异常被抛出。 - shtse8
不幸的是,!task.GetType().IsGenericType 无法捕获从 Task.CompletedTask 创建的 task,因为该任务实际上是一个 Task<VoidTaskResult>,其中 VoidTaskResult 是一个内部空结构体类型。然后 ((dynamic)task).Result 抛出一个 RuntimeBinderException,因为我认为它无法公开一个内部类型。 - Theodor Zoulias

2

为了达到最佳效果,不使用反射和动态丑陋的语法,也不传递泛型类型。我将使用两个扩展方法来实现此目标。

    public static async Task<object> CastToObject<T>([NotNull] this Task<T> task)
    {
        return await task.ConfigureAwait(false);
    }

    public static async Task<TResult> Cast<TResult>([NotNull] this Task<object> task)
    {
        return (TResult) await task.ConfigureAwait(false);
    }

使用方法:

    Task<T1> task ...
    Task<T2> task2 = task.CastToObject().Cast<T2>();

这是我的第二种方法,但不推荐使用

public static async Task<TResult> Cast<TSource, TResult>([NotNull] this Task<TSource> task, TResult dummy = default)
{
    return (TResult)(object) await task.ConfigureAwait(false);
}

使用方法:

Task<T1> task ...
Task<T2> task2 = task.Cast((T2) default);

// Or

Task<T2> task2 = task.Cast<T1, T2>();

这是我的第三种方法,但不建议使用(类似于第二种方法)

public static async Task<TResult> Cast<TSource, TResult>([NotNull] this Task<TSource> task, Type<TResult> type = null)
{
    return (TResult)(object) await task.ConfigureAwait(false);
}

// Dummy type class
public class Type<T>
{
}

public static class TypeExtension
{
    public static Type<T> ToGeneric<T>(this T source)
    {
        return new Type<T>();
    }
}

使用方法:

Task<T1> task ...
Task<T2> task2 = task.Cast(typeof(T2).ToGeneric());

// Or

Task<T2> task2 = task.Cast<T1, T2>();

2
没有任何代码片段实际上解决了一个没有泛型的“任务”并获取其结果的问题! - Venson
@Venson 请仔细阅读问题。 - shtse8

0

在动态/反射调用中混合await不是一个好主意,因为await是一个编译器指令,会在调用方法周围生成大量的代码,使用更多的反射、续体、包装等来“模拟”编译器工作没有实际意义。

既然你需要在运行时管理代码,那就忘记async await语法糖,它只在编译时起作用。重写SomeFunctionSomeOtherFunction,并在运行时创建自己的任务开始操作。你将获得相同的行为,但代码清晰明了。


我很感激您的见解,我能够看出在许多情况下这将是理想的实现方式。不幸的是,对我来说,这并不是一个实际的解决方案,因为异步模式已经被确立,并且会对许多项目产生重大的影响,如果现在更改它,我会有一些不满意的团队成员。 - ncarriker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接