关于Enumerable/List的评价
Regarding evaluation of Enumerable/List
我一直在玩 Lists 和 Enumerables,我想我了解基础知识:
- 可枚举:每次使用元素时都会计算元素。
- 列表:元素根据定义求值,在任何时候都不会重新求值。
我做了一些测试:
- 可枚举。 https://www.tutorialspoint.com/tpcg.php?p=bs75zCKL
- 列表:https://www.tutorialspoint.com/tpcg.php?p=PpyY2iif
- SingleEvaluationEnum:https://www.tutorialspoint.com/tpcg.php?p=209Ciiy7
从可枚举示例开始:
var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
var myEnumerable = myList.Where(p =>
{
Console.Write($"{p} ");
return p > 2;
}
);
Console.WriteLine("");
Console.WriteLine("Starting");
myEnumerable.First();
Console.WriteLine("");
myEnumerable.Skip(1).First();
输出为:
Starting
1 2 3
1 2 3 4
如果我们在 .Where(...)
之后添加 .ToList()
那么输出是:
1 2 3 4 5 6
Starting
我也可以通过这个 class:
class SingleEvaluationEnum<T>
{
private IEnumerable<T> Enumerable;
public SingleEvaluationEnum(IEnumerable<T> enumerable)
=> Enumerable = enumerable;
public IEnumerable<T> Get()
{
if (!(Enumerable is List<T>))
Enumerable = Enumerable.ToList().AsEnumerable();
return Enumerable;
}
}
可以看到输出是:
Starting
1 2 3 4 5 6
这样,评估会延迟到第一次消费,而不会在下一次消费时重新评估。但是整个列表都被评估了。
我的问题是:有没有办法得到这个输出?
Starting
1 2 3
4
换句话说:我希望 myEnumerable.First()
只评估必要的元素,而不是更多。我希望 myEnumerable.Skip(1).First()
重用已评估的元素。
编辑:澄清:我希望 Enumerable 上的任何 "query" 都适用于列表中的所有元素。这就是(据我所知)枚举器不起作用的原因。
谢谢!
基本上听起来您正在寻找一个 Enumerator
,您可以通过在 IEnumerable
上调用 GetEnumerator
来获得它。 Enumerator
跟踪它的位置。
var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
var myEnumerator = myList.Where(p =>
{
Console.Write($"{p} ");
return p > 2;
}
).GetEnumerator();
Console.WriteLine("Starting");
myEnumerator.MoveNext();
Console.WriteLine("");
myEnumerator.MoveNext();
这将为您提供输出:
Starting
1 2 3
4
编辑以回复您的评论:
首先,这听起来像是一个非常糟糕的主意。枚举器代表可以枚举的东西。这就是为什么您可以将所有那些花哨的 LINQ 查询放在它上面。然而,所有对 First
"visualize" 这个枚举的调用(这导致 GetEnumerator
被调用以获得 Enumerator
并遍历它直到我们完成然后处理它)。但是,您要求每个可视化更改 IEnumerable
它正在可视化(这不是好的做法)。
但是,既然您说这是为了学习,我将给您以 IEnumerable
结尾的代码,它将为您提供所需的输出。我不建议你在实际代码中使用它,这不是一个好的和可靠的做事方式。
首先我们创建一个自定义的 Enumerator
,它不处理,但只是不断枚举一些内部枚举器:
public class CustomEnumerator<T> : IEnumerator<T>
{
private readonly IEnumerator<T> _source;
public CustomEnumerator(IEnumerator<T> source)
{
_source = source;
}
public T Current => _source.Current;
object IEnumerator.Current => _source.Current;
public void Dispose()
{
}
public bool MoveNext()
{
return _source.MoveNext();
}
public void Reset()
{
throw new NotImplementedException();
}
}
然后我们创建一个自定义的 IEnumerable
class,而不是每次调用 GetEnumerator()
时都创建一个新的 Enumerator
,而是秘密地继续使用相同的枚举器:
public class CustomEnumerable<T> : IEnumerable<T>
{
public CustomEnumerable(IEnumerable<T> source)
{
_internalEnumerator = new CustomEnumerator<T>(source.GetEnumerator());
}
private IEnumerator<T> _internalEnumerator;
public IEnumerator<T> GetEnumerator()
{
return _internalEnumerator;
}
IEnumerator IEnumerable.GetEnumerator()
{
return _internalEnumerator;
}
}
最后我们创建一个 IEnumerable
扩展方法来将 IEnumerable
转换为我们的 CustomEnumerable
:
public static class IEnumerableExtensions
{
public static IEnumerable<T> ToTrackingEnumerable<T>(this IEnumerable<T> source) => new CustomEnumerable<T>(source);
}
我们终于可以做到这一点了:
var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
var myEnumerable = myList.Where(p =>
{
Console.Write($"{p} ");
return p > 2;
}).ToTrackingEnumerable();
Console.WriteLine("Starting");
var first = myEnumerable.First();
Console.WriteLine("");
var second = myEnumerable.Where(p => p % 2 == 1).First();
Console.WriteLine("");
我更改了最后一部分以表明我们仍然可以在其上使用 LINQ。现在的输出是:
Starting
1 2 3
4 5
LINQ 从根本上说是一种处理集合的函数式方法。假设之一是评估函数没有副作用。您在函数中调用 Console.Write
违反了该假设。
没有魔法,只有功能。 IEnumerable
只有一种方法 - GetEnumerator
。这就是 LINQ 所需的全部,这就是 LINQ 真正要做的。例如,Where
的简单实现如下所示:
public static IEnumerable<T> Where<T>(this IEnumerable<T> @this, Func<T, bool> filter)
{
foreach (var item in @this)
{
if (filter(item)) yield return item;
}
}
A Skip
可能如下所示:
public static IEnumerable<T> Skip<T>(this IEnumerable<T> @this, int skip)
{
foreach (var item in @this)
{
if (skip-- > 0) continue;
yield return item;
}
}
仅此而已。它没有关于 IEnumerable
是什么或代表什么的任何信息。事实上,这就是重点——您正在抽象化这些细节。这些方法中有一些优化,但它们没有做任何聪明的事情。最后,您的示例中 List
和 IEnumerable
之间的区别并不是根本性的 - 这是 myEnumerable.Skip(1)
有副作用(因为 myEnumerable
本身有副作用 -效果)而 myList.Skip(1)
则没有。但是两者都做完全相同的事情——逐项评估可枚举。除了 GetEnumerator
之外没有其他方法可枚举,而 IEnumerator
只有 Current
和 MoveNext
(对我们来说很重要)。
LINQ 是不可变的。这就是它如此有用的原因之一。这使您可以完全按照自己的意愿行事 - 两次查询相同的可枚举但得到完全相同的结果。但你对此并不满意。你希望事情是可变的。好吧,没有什么能阻止您创建自己的辅助函数。 LINQ 只是一堆函数,毕竟 - 您可以创建自己的函数。
一个这样的简单扩展可以是记忆化的可枚举。环绕源可枚举,在内部创建一个列表,当您迭代源可枚举时,继续向列表添加项目。下次调用 GetEnumerator
时,开始遍历您的内部列表。当你到达终点时,继续原来的方法 - 迭代源可枚举并继续添加到列表中。
这将允许您完全使用 LINQ,只需将 Memoize()
插入到您的 LINQ 查询中您希望避免多次迭代源的位置。在您的示例中,这类似于:
myEnumerable = myEnumerable.Memoize();
Console.WriteLine("");
Console.WriteLine("Starting");
myEnumerable.First();
Console.WriteLine("");
myEnumerable.Skip(1).First();
第一次调用 myEnumerable.First()
将遍历 myList
中的前三项,第二次调用仅与第四项一起工作。
我一直在玩 Lists 和 Enumerables,我想我了解基础知识:
- 可枚举:每次使用元素时都会计算元素。
- 列表:元素根据定义求值,在任何时候都不会重新求值。
我做了一些测试:
- 可枚举。 https://www.tutorialspoint.com/tpcg.php?p=bs75zCKL
- 列表:https://www.tutorialspoint.com/tpcg.php?p=PpyY2iif
- SingleEvaluationEnum:https://www.tutorialspoint.com/tpcg.php?p=209Ciiy7
从可枚举示例开始:
var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
var myEnumerable = myList.Where(p =>
{
Console.Write($"{p} ");
return p > 2;
}
);
Console.WriteLine("");
Console.WriteLine("Starting");
myEnumerable.First();
Console.WriteLine("");
myEnumerable.Skip(1).First();
输出为:
Starting
1 2 3
1 2 3 4
如果我们在 .Where(...)
之后添加 .ToList()
那么输出是:
1 2 3 4 5 6
Starting
我也可以通过这个 class:
class SingleEvaluationEnum<T>
{
private IEnumerable<T> Enumerable;
public SingleEvaluationEnum(IEnumerable<T> enumerable)
=> Enumerable = enumerable;
public IEnumerable<T> Get()
{
if (!(Enumerable is List<T>))
Enumerable = Enumerable.ToList().AsEnumerable();
return Enumerable;
}
}
可以看到输出是:
Starting
1 2 3 4 5 6
这样,评估会延迟到第一次消费,而不会在下一次消费时重新评估。但是整个列表都被评估了。
我的问题是:有没有办法得到这个输出?
Starting
1 2 3
4
换句话说:我希望 myEnumerable.First()
只评估必要的元素,而不是更多。我希望 myEnumerable.Skip(1).First()
重用已评估的元素。
编辑:澄清:我希望 Enumerable 上的任何 "query" 都适用于列表中的所有元素。这就是(据我所知)枚举器不起作用的原因。
谢谢!
基本上听起来您正在寻找一个 Enumerator
,您可以通过在 IEnumerable
上调用 GetEnumerator
来获得它。 Enumerator
跟踪它的位置。
var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
var myEnumerator = myList.Where(p =>
{
Console.Write($"{p} ");
return p > 2;
}
).GetEnumerator();
Console.WriteLine("Starting");
myEnumerator.MoveNext();
Console.WriteLine("");
myEnumerator.MoveNext();
这将为您提供输出:
Starting
1 2 3
4
编辑以回复您的评论:
首先,这听起来像是一个非常糟糕的主意。枚举器代表可以枚举的东西。这就是为什么您可以将所有那些花哨的 LINQ 查询放在它上面。然而,所有对 First
"visualize" 这个枚举的调用(这导致 GetEnumerator
被调用以获得 Enumerator
并遍历它直到我们完成然后处理它)。但是,您要求每个可视化更改 IEnumerable
它正在可视化(这不是好的做法)。
但是,既然您说这是为了学习,我将给您以 IEnumerable
结尾的代码,它将为您提供所需的输出。我不建议你在实际代码中使用它,这不是一个好的和可靠的做事方式。
首先我们创建一个自定义的 Enumerator
,它不处理,但只是不断枚举一些内部枚举器:
public class CustomEnumerator<T> : IEnumerator<T>
{
private readonly IEnumerator<T> _source;
public CustomEnumerator(IEnumerator<T> source)
{
_source = source;
}
public T Current => _source.Current;
object IEnumerator.Current => _source.Current;
public void Dispose()
{
}
public bool MoveNext()
{
return _source.MoveNext();
}
public void Reset()
{
throw new NotImplementedException();
}
}
然后我们创建一个自定义的 IEnumerable
class,而不是每次调用 GetEnumerator()
时都创建一个新的 Enumerator
,而是秘密地继续使用相同的枚举器:
public class CustomEnumerable<T> : IEnumerable<T>
{
public CustomEnumerable(IEnumerable<T> source)
{
_internalEnumerator = new CustomEnumerator<T>(source.GetEnumerator());
}
private IEnumerator<T> _internalEnumerator;
public IEnumerator<T> GetEnumerator()
{
return _internalEnumerator;
}
IEnumerator IEnumerable.GetEnumerator()
{
return _internalEnumerator;
}
}
最后我们创建一个 IEnumerable
扩展方法来将 IEnumerable
转换为我们的 CustomEnumerable
:
public static class IEnumerableExtensions
{
public static IEnumerable<T> ToTrackingEnumerable<T>(this IEnumerable<T> source) => new CustomEnumerable<T>(source);
}
我们终于可以做到这一点了:
var myList = new List<int>() { 1, 2, 3, 4, 5, 6 };
var myEnumerable = myList.Where(p =>
{
Console.Write($"{p} ");
return p > 2;
}).ToTrackingEnumerable();
Console.WriteLine("Starting");
var first = myEnumerable.First();
Console.WriteLine("");
var second = myEnumerable.Where(p => p % 2 == 1).First();
Console.WriteLine("");
我更改了最后一部分以表明我们仍然可以在其上使用 LINQ。现在的输出是:
Starting
1 2 3
4 5
LINQ 从根本上说是一种处理集合的函数式方法。假设之一是评估函数没有副作用。您在函数中调用 Console.Write
违反了该假设。
没有魔法,只有功能。 IEnumerable
只有一种方法 - GetEnumerator
。这就是 LINQ 所需的全部,这就是 LINQ 真正要做的。例如,Where
的简单实现如下所示:
public static IEnumerable<T> Where<T>(this IEnumerable<T> @this, Func<T, bool> filter)
{
foreach (var item in @this)
{
if (filter(item)) yield return item;
}
}
A Skip
可能如下所示:
public static IEnumerable<T> Skip<T>(this IEnumerable<T> @this, int skip)
{
foreach (var item in @this)
{
if (skip-- > 0) continue;
yield return item;
}
}
仅此而已。它没有关于 IEnumerable
是什么或代表什么的任何信息。事实上,这就是重点——您正在抽象化这些细节。这些方法中有一些优化,但它们没有做任何聪明的事情。最后,您的示例中 List
和 IEnumerable
之间的区别并不是根本性的 - 这是 myEnumerable.Skip(1)
有副作用(因为 myEnumerable
本身有副作用 -效果)而 myList.Skip(1)
则没有。但是两者都做完全相同的事情——逐项评估可枚举。除了 GetEnumerator
之外没有其他方法可枚举,而 IEnumerator
只有 Current
和 MoveNext
(对我们来说很重要)。
LINQ 是不可变的。这就是它如此有用的原因之一。这使您可以完全按照自己的意愿行事 - 两次查询相同的可枚举但得到完全相同的结果。但你对此并不满意。你希望事情是可变的。好吧,没有什么能阻止您创建自己的辅助函数。 LINQ 只是一堆函数,毕竟 - 您可以创建自己的函数。
一个这样的简单扩展可以是记忆化的可枚举。环绕源可枚举,在内部创建一个列表,当您迭代源可枚举时,继续向列表添加项目。下次调用 GetEnumerator
时,开始遍历您的内部列表。当你到达终点时,继续原来的方法 - 迭代源可枚举并继续添加到列表中。
这将允许您完全使用 LINQ,只需将 Memoize()
插入到您的 LINQ 查询中您希望避免多次迭代源的位置。在您的示例中,这类似于:
myEnumerable = myEnumerable.Memoize();
Console.WriteLine("");
Console.WriteLine("Starting");
myEnumerable.First();
Console.WriteLine("");
myEnumerable.Skip(1).First();
第一次调用 myEnumerable.First()
将遍历 myList
中的前三项,第二次调用仅与第四项一起工作。