Search code examples
c#polymorphismtask-parallel-library.net-5code-duplication

How to treat List<Task<T>> polymorphically to handle exceptions


I'm trying to create one method that logs the inner exceptions of a list of tasks. The problem is that in some places I have just (A) List<Task> and in other places I have (B) List<Task<Something>>. I tried to create a method that accepts A but then the compiler does not accept calls to that method with B, so I call B as "MultipleTasksExceptionHandler(B as A)" but inside the method I receive null. So I had to create to almost identical methods like below and they work but is there any other way to avoid this code duplication?

    public static void MultipleTasksExceptionHandler<T>(IList<Task<T>> tasks, ILogger logger)
    {
        var exceptions = tasks.Where(task => task.IsFaulted).SelectMany(x => x.Exception.InnerExceptions);

        foreach (var exception in exceptions)
        {
            logger.LogError(exception, exception.Message);
        }
    }

    public static void MultipleTasksExceptionHandler(IList<Task> tasks, ILogger logger)
    {
        var exceptions = tasks.Where(task => task.IsFaulted).SelectMany(x => x.Exception.InnerExceptions);

        foreach (var exception in exceptions)
        {
            logger.LogError(exception, exception.Message);
        }
    }

Solution

  • Use a covariant collection, such as IEnumerable<T> or IReadOnlyList<T>, rather than IList<T>:

    public void MultipleTasksExceptionHandler(IEnumerable<Task> tasks, ILogger logger)
    {
        var exceptions = tasks.Where(task => task.IsFaulted).SelectMany(x => x.Exception.InnerExceptions);
    
        foreach (var exception in exceptions)
        {
            logger.LogError(exception, exception.Message);
        } 
    }
    

    It's correct that you can't assign an object of type IList<Task<T>> to a variable of type IList<Task>. If this were allowed, you would be able to do list.Add(Task.CompletedTask), and insert a non-generic Task into your IList<Task<T>>, which would obviously be wrong.

    For example:

    IList<Task<int>> listOfTaskOfT = new List<Task<int>>();
    
    // This isn't allowed
    IList<Task> listOfTask = listOfTaskOfT;
    
    // ... if it was, you'd be able to do this:
    listOfTask.Add(new Task());
    Task<int> task = listOfTaskOfT[0];
    // But that item is a Task, not a Task<int>. Oops!
    

    However, this isn't a problem if you can only ever take things out of your collection. This is called generic covariance, and generic interfaces can mark themselves as covariant using the out keyword. IEnumerable<out T> and IReadOnlyList<out T> both do this.

    You're then allowed to do things like:

    IEnumerable<Task> tasks = new List<Task<T>>();
    

    ... provided that all of the generic type arguments involved are reference types.

    (A similar feature exists for generic "contravariant" interfaces where you can only ever add items, using the in keyword).


    You can get similar functionality using generics:

    public void MultipleTasksExceptionHandler<T>(IList<T> tasks, ILogger logger) where T : Task
    {
       var exceptions = tasks.Where(task => task.IsFaulted).SelectMany(x => x.Exception.InnerExceptions);
    
        foreach (var exception in exceptions)
        {
            logger.LogError(exception, exception.Message);
        } 
    }
    

    Here, you're prevented from adding a Task into an IList<Task<Whatever>> by virtue of the fact that T is Task<Whatever>. This isn't needed in your case, but can be useful if you do actually need to insert items into your collection.