When using Task<T>
, you must supply the Func<>
or Action<>
delegate (i.e., function pointer to the desired work to perform) as a constructor argument. It is indeed somewhat unfortunate that there isn't any constructor which lets you bypass this requirement and supply the delegate at a later time (yet obviously still prior to calling Start()
), since this severely hampers the ability to extend the Task
and Task<TResult>
classes via inheritance altogether.
The reason it's a crippling omission is that no delegate you supply as a constructor argument can possibly directly incorporate a reference to the instance you are trying to construct, since that instance (again, obviously) doesn't exist yet, chicken/egg style.
Hence @Arturo's answer, which shows that you can, in fact, supply a static delegate, but since such a delegate has no obvious way of referencing one particular Task
instance, it essentially defeats the purpose of inheriting from Task
in the first place.
--- reflection disclaimer ---
I've been using this technique in my own projects for years on .NET Framework 4.7 (desktop) with no problems whatsoever, but please note that code which uses reflection to access non-public behavior is subject to breakage if the .NET internals change in a later version. You have been warned.
Here's a more flexible workaround for the problem, a general-purpose abstract base class for Task<TResult>
which allows you to provide the desired work code in the normal way for derived type hierarchies: as an instance method override. This is a reflection solution; the way it works is to provide a dummy "placeholder" delegate to the base constructor call, but then immediately in the constructor body, swap it out for the "real," desired abstract instance work method, which at that point is no longer unknowable or rather unbindable.
abstract class TaskBase<TResult> : Task<TResult>
{
readonly static FieldInfo m_action =
typeof(Task).GetField("m_action", BindingFlags.Instance | BindingFlags.NonPublic);
readonly static Func<TResult> _dummy = () => default;
public TaskBase(CancellationToken ct, TaskCreationOptions opts)
: base(_dummy, ct, opts) =>
m_action.SetValue(this, (Func<TResult>)function);
public TaskBase(CancellationToken ct)
: this(ct, TaskCreationOptions.None)
{ }
public TaskBase(TaskCreationOptions opts)
: this(default, opts)
{ }
public TaskBase()
: this(default, TaskCreationOptions.None)
{ }
protected abstract TResult function(); // <-- override with your work code
};
To use this, simply inherit from TaskBase<TResult>
, and override the abstract method function()
to implement your task work logic. There is no need for a version of this base class where the work function accepts a AsyncState
argument/parameter(s), since you can simply declare all the relevant context for the specific work instance as additional instance fields (and instance methods, and instance properties...) in your derived class. So the constructor variations I declared exactly match those provided by Task<TResult>
, but minus the 'function' and 'state' arguments. And finally, don't forget to call Start()
when your packaged work instance is ready to go!
The above is a actually a simplified version of the TaskBase<TResult>
code I've had much success with. My enhanced version avoids creating the Func<TResult>
delegate which must be created for each TaskBase
instance in order to "wrap" the C# method function()
as an instance delegate. Instead of initially providing a 'dummy' delegate to the base constructor, I always provide (the same) static delegate, a singleton which acts as a "thunk" that universally reinterprets, or "upcasts" a Task<TResult>
's AsyncState
object as a pertinent TaskBase<TResult>
instance, and then calls function()
directly on that instance. Like so:
static Func<Object,TResult> thunk = obj => ((TaskBase<TResult>)obj).function();
So fn_redirect
is the only "excess" delegate we need to create once at startup, and this singleton is always passed-in as the base constructor work delegate. Now as with that constructor argument, the "async state" object is also only passed in as a constructor argument and normally cannot later be changed. We don't need a "dummy" in this approach, because you can--and should--pass in 'null' for state
. Similar to before we use reflection to set a field, but this time it's m_stateObject
field instead of m_action
, to replace the 'null' value we just installed for the instance this
pointer:
public TaskBase(CancellationToken ct, TaskCreationOptions opts)
: base(thunk, default(Object), ct, opts)
{
m_stateObject.SetValue(this, this);
}
Voila, allocating one extra delegate for each TaskBase
instance is avoided. Finally, recall that there are no adverse loss of capability when co-opting the state object for the purpose of this enhancement because as I mentioned earlier, the whole AsyncObject
argument-passing mechanism is unnecessary when you entirely control the derived class you are writing.