Пример на С#.
Возвращаем коллекцию с помощью yield
public static class Foo
{
public static IEnumerable Test()
{
var rand = new Random().Next(1, 3);
if (rand == 1)
yield return 1;
if (rand == 2)
yield return 2;
yield return 3;
yield return "foo";
yield return true;
}
}
Пример 2. Возвращаем коллекцию с помощью обычного листа.
public static class Foo1
{
public static IEnumerable Test()
{
var list = new List
Ответ
Ну, отличие на самом деле кардинальное.
Дело в том, что в первом случае у вас ленивое, а во втором — энергичное вычисление ответа. Это значит, что элементы выходной последовательности в энергичном случае вычисляются все и сразу, а в ленивом случае — только когда запрошены и только те, что запрошены.
Давайте посмотрим, где с практической стороны есть разница.
Для случая ленивого вычисления вся последовательность не присутствует полностью в памяти. Это значит, что при поэлементной обработке у нас не выделяется память, и сохраняется cache locality:
IEnumerable
IEnumerable
Вычисляем функцию на всей последовательности, сравниваем расход памяти:
var seqLazy = GenerateHugeSequenceLazy();
// вычисляем максимум вручную
var max = 0;
foreach (var v in seqLazy)
if (v > max)
max = v;
var memLazy = GC.GetTotalMemory(forceFullCollection: false);
var seqEager = GenerateHugeSequenceEager();
// вычисляем максимум вручную
max = 0;
foreach (var v in seqEager)
if (v > max)
max = v;
var memEager = GC.GetTotalMemory(forceFullCollection: false);
Console.WriteLine($"Memory footprint lazy: {memLazy}, eager: {memEager}");
Результат:
Memory footprint lazy: 29868, eager: 6323088
Затем, у нас довольно большие различия в смысле операций. Ленивые вычисления происходят в момент, когда они нужны. А значит, состояние аргументов будет взято на момент реального перечисления. Вот пример:
IEnumerable
IEnumerable
Смотрим на отличия:
var seq = new List
Console.WriteLine("Eager: " + string.Join(" ", eagerlyDoubled));
Console.WriteLine("Lazy : " + string.Join(" ", lazilyDoubled));
// выводит оба раза 2, покамест различий нет
seq.Add(2); // модифицируем *исходную* последовательность
Console.WriteLine("Eager: " + string.Join(" ", eagerlyDoubled)); // 2
Console.WriteLine("Lazy : " + string.Join(" ", lazilyDoubled)); // 2 4
Поскольку ленивое вычисление происходит при перечислении, мы видим, что при изменении последовательности ленивая версия подхватывает изменения.
Другой пример. Посмотрим, что будет, если мы не вычисляем всю последовательность. Вычислим одну и ту же последовательность энергично и лениво:
IEnumerable
IEnumerable
Берём только 2 элемента из результата:
foreach (var e in Eager10().Take(2))
Console.WriteLine($"Obtained: {e}");
foreach (var e in Lazy10().Take(2))
Console.WriteLine($"Obtained: {e}");
foreach (var e in Lazy10())
{
Console.WriteLine($"Obtained: {e}");
if (e == 1)
break;
}
Получаем такой вывод на консоль:
Eager
Adding: 0
Adding: 1
Adding: 2
Adding: 3
Adding: 4
Adding: 5
Adding: 6
Adding: 7
Adding: 8
Adding: 9
Eagerly computed: 10
Obtained: 0
Obtained: 1
Lazy
Adding: 0
Obtained: 0
Adding: 1
Obtained: 1
Lazily computed: 2
Lazy
Adding: 0
Obtained: 0
Adding: 1
Obtained: 1
Lazily computed: 2
Видите разницу? Ленивый вариант прогнал цикл всего два раза, и не вычислял «хвост» последовательности.
Ещё одна разница между случаями — когда сообщаются ошибки. В случае энергичного вычисления они сообщаются сразу. В случае ленивого — лишь при перечислении результата. Пример:
IEnumerable
IEnumerable
Применяем try/catch:
Console.WriteLine("Eager:");
IEnumerable
if (seqEager != null)
foreach (var e in seqEager)
Console.WriteLine(e);
Console.WriteLine("Lazy:");
IEnumerable
if (seqLazy != null)
foreach (var e in seqLazy)
Console.WriteLine(e);
Получаем результат:
Eager:
Exception caught
Lazy:
Unhandled Exception: System.ArgumentException: value cannot be 0
at Program.
Ещё один случай различия — зависимость от внешних данных в процессе вычисления. Следующий код пытается влиять на вычисления, изменяя глобальное состояние. (Это не очень хороший код, не делайте так в реальных программах!)
bool evilMutableAllowCompute;
IEnumerable
IEnumerable
Используем:
Console.WriteLine("Eager:");
evilMutableAllowCompute = true;
foreach (var e in EagerGet5WithExternalDependency())
{
Console.WriteLine($"Obtained: {e}");
if (e > 0)
evilMutableAllowCompute = false;
}
Console.WriteLine("Lazy:");
evilMutableAllowCompute = true;
foreach (var e in LazyGet5WithExternalDependency())
{
Console.WriteLine($"Obtained: {e}");
if (e > 0)
evilMutableAllowCompute = false;
}
Результат:
Eager:
Obtained: 0
Obtained: 1
Obtained: 2
Obtained: 3
Obtained: 4
Lazy:
Obtained: 0
Obtained: 1
Мы видим, что изменение глобальных данных даже после формальной отработки ленивой функции может влиять на вычисления.
(Это ещё один аргумент в пользу того, что функциональное программирование и мутабельное состояние плохо сочетаются.)
Комментариев нет:
Отправить комментарий