Самая легковесная коллекция .NET
Скажем, требуется передать IEnumerable в метод. У нас имеется от 2 до 10 элементов. Какой класс коллекции был бы наиболее уместен и удачен в плане производительности, если после передачи коллекции в метод нам больше не потребуется эта коллекция (то есть функционала IEnumerable для нее вполне достаточно)? Вот примерное применение нужной коллекции:
Task.WhenAll(new List<Task> { SomeTask1, SomeTask2, SomeTask3 });
Дополнение: Много интересных вариантов и предложений, спасибо всем)
Ответы (3 шт):
В шарпе и List и массив одинаково легковесны. Но с одним уточнением - если лист создавался с предустановленным количеством элементов. Это связано с тем, что в шарпе List является ArrayList - то есть враппером вокруг массива. И если ты задаешь наперед количество элементов - они, фактически, идентичны между собой по легковесности.
Можно вот так Task.WhenAll(SomeTask1, SomeTask2, SomeTask3). А вообще самая легковесная коллекция - это массив, то есть new Task[].
В .NET Core появилась еще одна штука - ArrayPool<T>. Ее можно не только с Task использовать, а вообще с чем угодно. Единственная проблема - вы должны заранее знать минимальный требуемый объем выделяемого вам массива.
ArrayPool<Task> pool = ArrayPool<Task>.Shared;
Task[] tasks = pool.Rent(3);
tasks[0] = ...;
await Tasks.WhenAll(tasks[..3]);
pool.Return(tasks);
В этом случае аллокация массива произойдет только один раз, и далее он будет переиспользоваться. В общем, быстрее дальше некуда.
Но там есть тонкости, пул дает вам массив длиной больше либо равной той, которую вы запросили. Так что конкретно с тасками его может быть и не очень удобно будет использовать, но здесь надо смотреть на то, как себя будет вести Task.WhenAll с недозаполненным массивом, я не проверял. Поэтому я в коде использую System.Range - tasks[..3].
Бонус
Еще один вариант (правда не в тему) - это аллокация массива в стеке, такой фокус позволяет провернуть в .NET Core Span<T>. Но с Task это работать не будет, а будет работать только с неуправляемыми типами - примитивами. И так же, этот фокус нельзя вытворять в асинхронных методах. Так же такой способ быстрой аллокации не подходит для больших массивов, 1 МБ стека (по умолчанию) может легко переполниться, что будет дальше - вы знаете.
Span<byte> span = stackalloc byte[100]; // аллокация в стеке - очень быстро
Span<byte> span = new byte[100]; // обычная аллокация в куче
Подробнее можно прочитать в документации.
Всё же напишу ещё один ответ.
Метод Task.WhenAll имеет перегрузки с ключевым словом params. Что позволяет использовать его так:
Task.WhenAll(SomeTask1, SomeTask2, SomeTask3)
То есть не нужно явно создавать коллекцию. Массив будет создан автоматически.
Удобно? Да. Перечисление массива эффективно? Несомненно.
Однако, некоторые статические анализаторы на такой код выдают предупреждение об array allocation - выделении памяти под массив.
Если этот код будет использован однократно, то можно так и оставить.
Но если он будет вызываться многократно, а ваше приложения является высоконагруженным, то следует избавиться от лишней аллокации.
Как раз для этого в новых версиях языка и придуманы ArrayPool и Span + stackalloc. С кожалению, пул выдаст массив с размером 16 элементов. А у WhenAll нет перегрузки, принимающей спан.
Вероятно, проще всего будет создать поле для нашего массива:
private Task[] tasks = new Task[3]; // это именно ПОЛЕ класса, а не локальная переменная!
В методе заполняем его и используем:
tasks[0] = SomeTask1;
tasks[1] = SomeTask2;
tasks[2] = SomeTask3;
await Task.WhenAll(tasks);
В итоге получаем единственную аллокацию на все вызовы.
Но это пригодно если только количество элементов не меняется от вызова к вызову.