-
API Proposalnamespace System.Threading.Tasks
{
public class Task<TResult> : Task
{
+ public Task<TNewResult> Then<TNewResult>(Func<TResult, TNewResult> selector);
+ public Task<TNewResult> Then<TNewResult, TArg>(Func<TResult, TNewResult> selector, TArg);
}
public readonly struct ValueTask<TResult> : IEquatable<ValueTask<TResult>>
{
+ public ValueTask<TNewResult> Then<TNewResult>(Func<TResult, TNewResult> selector);
+ public ValueTask<TNewResult> Then<TNewResult, TArg>(Func<TResult, TNewResult> selector, TArg);
}
} The functions given to these API Usageasync Task M(){
string text = await File.ReadAllTextAsync(path).Then(static text => text.ReplaceLineEndings());
}
Task<string> M(){
return File.ReadAllTextAsync(path).Then(static text => text.ReplaceLineEndings());
} Background and motivationMethod chaining is a very common way to manipulate data. void M(){
string text = File.ReadAllText(path).ReplaceLineEndings();
} and today expressions are one of the main ways in which data gets manipulated in .NET with C# continuing to make it easier to "expression-ify" things where it makes sense to do so. Unfortunately, once async comes into the picture you need to wrap things in parenthesis in order for the associativity to keep working the way the developer wants and we need to be in an async method. async Task M(){ // need to be async method
string text = (await File.ReadAllTextAsync(path)).ReplaceLineEndings(); // must wrap all awaits in parenthesis or have on separate line
} You can try and solve some of this with the Lets say we wanted to write our own extension method with a signature like this public static Task<TNewResult> Then<TResult, TNewResult>(this Task<TResult> task, Func<TResult, TNewResult> selector) a naïve implementation may look like this public static Task<TNewResult> Then<TResult, TNewResult>(this Task<TResult> task, Func<TResult, TNewResult> selector)
{
return task.ContinueWith(ContinuationFunction, state: (object?)selector);
static TNewResult ContinuationFunction(Task<TResult> task, object? state)
{
if (task.IsCompletedSuccessfully && state is not null)
{
var selector = (Func<TResult, TNewResult>)state;
return selector(task.Result);
}
// this helper needs to account for canceled tasks and exceptions
throw new InvalidOperationException();
}
} A few problems with this: 1. It needs to handle cancellation and exceptionsThis is not impossible to do just tricky. It is unfortunate that this is necessary. In a world where folks are 2. You cannot pass in args without boxingSince namespace MyNamsspace
{
public enum LineEndingKind
{
CrLf,
Lf
}
public class Program
{
public static Task<string> M(string path, LineEndingKind lineEndingKind)
{
// there exists some overload of ReplaceLineEndings that accepts a LineEndingKind
return File.ReadAllTextAsync(path).Then(static (text, kind) => text.ReplaceLineEndings(kind), lineEndingKind);
}
}
}
namespace System.Threading.Tasks
{
public static class TaskExtensions
{
public static Task<TNewResult> Then<TResult, TArg, TNewResult>(this Task<TResult> task, Func<TResult, TArg, TNewResult> selector, TArg arg)
{
return task.ContinueWith(ContinuationFunction, state: (object?)(selector, arg)); // if arg is a struct we are boxing it here
static TNewResult ContinuationFunction(Task<TResult> task, object? state)
{
if (task.IsCompletedSuccessfully && state is not null)
{
var (selector, arg) = ((Func<TResult, TArg, TNewResult>, TArg))state;
return selector(task.Result, arg);
}
// this helper needs to account for cancelled tasks and exceptions
throw new InvalidOperationException();
}
}
}
} 3. This approach cannot be taken on
|
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
originally posted this as a discussion to make sure there were no obvious holes here. I've now moved this to #76839 |
Beta Was this translation helpful? Give feedback.
originally posted this as a discussion to make sure there were no obvious holes here. I've now moved this to #76839