Q1)我想知道调用s.Last()
linq扩展方法是否和调用s[s.Length-1]
一样高效。我更喜欢第一个选项,但我不知道实现是否利用了当前类型。
Q2)这可能是另一个有趣的问题。linq扩展方法在使用时是利用了类型,还是只将对象视为IEnumerable
?
不,它不会像直接索引那样高效,直接索引是O(1)。我们可以在Enumerable.Last
的参考来源中看到:
public static TSource Last<TSource>(this IEnumerable<TSource> source) {
if (source == null) throw Error.ArgumentNull("source");
IList<TSource> list = source as IList<TSource>;
if (list != null) {
int count = list.Count;
if (count > 0) return list[count - 1];
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (e.MoveNext()) {
TSource result;
do {
result = e.Current;
} while (e.MoveNext());
return result;
}
}
}
throw Error.NoElements();
}
由于String
不实现IList<char>
,它将转到使用枚举器的分支,要求检查所有字符,直到找到最后一个字符(即O(n))。
正如您所看到的,在某些情况下,LINQ方法考虑了访问各种接口提供的数据的更有效的方式。其他示例包括First
、Count
和ElementAt
。
它没有那么有效,如果您在实现IList
但不用于字符串的东西上调用它,它会有一个特殊情况。以下是Reflector的实现。
[__DynamicallyInvokable]
public static TSource Last<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw Error.ArgumentNull("source");
}
IList<TSource> list = source as IList<TSource>;
if (list != null)
{
int count = list.Count;
if (count > 0)
{
return list[count - 1];
}
}
else
{
using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
if (enumerator.MoveNext())
{
TSource current;
do
{
current = enumerator.Current;
}
while (enumerator.MoveNext());
return current;
}
}
}
throw Error.NoElements();
}
您可以看到它枚举了整个序列,然后只返回最后一个元素。
如果您非常关心string.Last()
的性能,那么您可以通过实现自己的Last()
过载来两全其美。如果您的过载比Enumerable.Last()
更匹配,则将使用您的过载。
internal class Program
{
private static void Main()
{
Console.WriteLine("Hello".Last());
}
}
public static class StringExtensions
{
public static char Last(this string text)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
int length = text.Length;
if (length == 0)
{
throw new ArgumentException("Argument cannot be empty.", "text");
}
return text[length - 1];
}
}
如果你想冒这个风险,去掉论据检查,你也可以这样做,但我不会。
我进行了测试以确认StringExtensions.Last()
正在被调用,尽管我经常使用这种技术来确定它是否有效。:-)
注意:为了调用重载,必须将变量声明为字符串,以便编译器知道它是字符串。如果IEnumerable<char>
在运行时恰好是字符串,则不会调用更有效的方法,例如:
private static void Main()
{
IEnumerable<char> s = "Hello";
Console.WriteLine(s.Last());
}
这里不调用StringExtensions.Last()
,因为编译器不知道s
是一个字符串,只知道它是IEnumerable<char>
(记住,成员重载解析是在编译时决定的)。对于字符串来说,这不是什么大问题,但对于其他优化来说,这可能是个问题。