如果您决定从Task
or继承Task<TResult>
,您可能会遇到挫折,即为任务提供实际工作的Action<Object>
orFunc<Object,TResult>
委托必须在构建 Task 派生对象时指定,并且以后无法更改。即使基类构造函数不是Start()
新创建的任务也是如此,事实上它可能直到很久以后才会启动,如果有的话。
这使得Task
在实例必须在其最终工作的全部细节可用之前创建的情况下使用 - 派生类变得困难。
一个例子可能是一个由众所周知的节点组成的无定形网络,它们致力于一个共同的目标,这样它们就可以以自组织的方式Task<TResult>
访问彼此的Result
属性。保证您可以在网络中的任意节点上运行的最简单方法是在启动任何节点之前预先构建所有节点。这巧妙地避免了尝试分析工作图依赖关系的问题,并允许运行时因素确定何时、是否以及以何种顺序需要值。Wait()
Result
这里的问题是,对于某些节点,您可能无法在构建时提供定义工作的功能。如果创建必要的 lambda 函数需要关闭Result
网络中其他任务的值,那么我们想要的Task<TResult>
提供的Result
可能还没有构建。即使它恰好是在预构建阶段较早构建的,你也不能调用Start()
它,因为它可能包含对其他节点的依赖关系,而其他节点没有。请记住,预先构建网络的全部目的是避免这样的复杂性。
好像这还不够,还有其他原因,不得不使用 lambda 函数来提供所需的函数是不方便的。因为它作为参数传递到构造函数中,所以函数无法访问this
最终任务实例的指针,这使得代码很难看,特别是考虑到 lambda 必须定义在 - 并且可能无意中关闭 -一些不相关this
的指针。
我可以继续说下去,但最重要的是,在派生类中定义扩展功能时,您不必忍受运行时闭包膨胀和其他麻烦。这不会错过多态性的全部意义吗?以正常方式定义派生类的工作委托会更优雅Task
,即基类中的抽象函数。
这是如何做到的。诀窍是定义一个私有构造函数,它关闭它自己的参数之一。由(链接的)被调用者传递的参数null
充当占位符变量,您可以将其关闭以创建Task
基类所需的委托。一旦进入构造函数体,“this”指针就可用,因此您可以将实际函数指针替换为封闭参数,替换null
. 请注意,这样做不会“为时已晚”,因为外部委托还不可能被调用。
从“任务”派生:
public abstract class DeferredActionTask : Task
{
private DeferredActionTask(Action _a, CancellationToken ct, TaskCreationOptions opts)
: base(_ => _a(), null, ct, opts)
{
_a = this.action;
}
protected DeferredActionTask(
CancellationToken ct = default(CancellationToken),
TaskCreationOptions opts = TaskCreationOptions.None)
: this(default(Action), ct, opts)
{
}
protected abstract void action();
};
从“Task<TResult>”派生:
public abstract class DeferredFunctionTask<TResult> : Task<TResult>
{
private DeferredFunctionTask(Func<TResult> _f, CancellationToken ct, TaskCreationOptions opts)
: base(_ => _f(), null, ct, opts)
{
_f = this.function;
}
protected DeferredFunctionTask(
CancellationToken ct = default(CancellationToken),
TaskCreationOptions opts = TaskCreationOptions.None)
: this(default(Func<TResult>), ct, opts)
{
}
protected abstract TResult function();
};
请记住,与构造Task
实例的任何其他用途一样,构造实例Task
不会在构造时自动启动,因此使用此技术您仍然必须Start()
在稍后的某个时间显式调用。当然,如上所述,这就是重点。
最后,请注意,我让私有构造函数始终传递基构造函数null
的参数,这实际上阻止了将只读属性设置为有用的值。如果您愿意,您可以将其更改为包括传递这样的值,但这里的原因再次是,重点是消除对启动数据进行预先确定的要求。几乎没有意义——当你现在有你自己的整个派生类来填充,在调用之前的任何时间,用相关的实例数据——必须在逻辑上不相关的时间,很可能提前很长时间,准确地挑出一个“特殊" 数据参数,表示任务最终有用工作的详细信息。state
Task
AsyncState
Start