在C#2.0的时候出现了匿名方法,在3.0的时候出现了Linq,
这里讲述的linq就是Linq to object的实现,与to sql的底层是由差别的,但是语言层面的用法是差不多的。
核心记住:序列与集合不同,序列有延迟计算的性质,集合虽然有序列的性质,但是集合的内部元素往往都是确定的。实现Linq必会实现装箱与拆箱,注意消耗。
一、实现foreach的核心->IEnumerable与IEnumerator
这是两个接口,我们称之为序列,或者是迭代器。这是实现Linq的核心,也是实现foreach的核心功能的主要接口。
那么现在来看看foreach是如何实现的:
//这是一种仿照foreach的遍历集合的方法
List<int> s = new List<int>{1,5,3,7,23};
var enumerator = s.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
上面这种方式实现了核心的foreach方法,当然实际的方法会比这种更加细节,如判断异常等功能。
之后就是要了解迭代器IEnumerable,IEnumerator的两个接口,这两个一个是公开枚举数,一个是枚举器,支持简单迭代。理解:一个集合实现IEnumerable接口,就可以用IEnumerator进行简单迭代。这也就理解了list,hashset为什么可以用foreach进行迭代的原理。
这里我们利用yield return来返回一个公开枚举数:
//这段代码很好理解:声明一个变量,返回一个公开枚举数,序列里包含着是1~100的字符串序列。这里的yield return与IEnumerable搭配使用,返回一个公开枚举数。我们称为序列。
public static IEnumerable<string> GenerateSequence()
{
var i = 0;
while (i++<100)
{
yield return i.ToString();
}
}
public static IEnumerable<string> SequenceFormConsole()
{
string text = Console.ReadLine();
while (text != "done")
{
yield return text;
text = Console.ReadLine();
}
}
序列的短路性质且具有延迟计算的性质,下面的例子有很好的解释性:
var input = SequenceFormConsole().Select(x => int.Parse(x));
foreach (var item in input)
{
Console.WriteLine($"\t{item}");
}
二、Select的实现用法与内涵
调用Select是为了生成一个新的序列(一个满足我们需求的序列),在官方API中的解释是:通过合并元素的索引投影到一个新窗体的序列的每一个元素。
//这是一个简单得利用Linq的Select实现索引的效果,GenerateSequence()这个方法就是上面的例子方法。
var sequence = GenerateSequence().Select((a,index) =>
new
{
index,
a
});
foreach (var item in sequence)
{
Console.WriteLine(item);
}
Select原理核心实现:
//这是一种简单的原理实现,但是缺点也很明显,泛型类型确定,实现特定,扩展性差。
public static IEnumerable<string> Select(this IEnumerable<int> source,Func<int,string> selector)
{
foreach (var item in source)
{
yield return selector(item);
}
}
//这种是最接近底层的一种实现,扩展性良好。
public static IEnumerable<TResult> Select<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
foreach (TSource item in source)
{
yield return selector(item);
}
}
三、where的实现用法与内涵
//where是一个静态方法,在LinQ的语法中,他可以当做一个筛选器。
var sequence = GenerateSequence2().Where(x => x%5 == 0).Select((x,index)=>
new
{
index,
formatted = x.ToString()
});
foreach (var item in sequence)
{
Console.WriteLine(item);
}
Where的核心原理实现:
/// <summary>
/// 这是一种无参数的序列判断,当然这正方式并不好,我们要自己的定义判断函数,这就需要一种方法,也就是委托的实现,这种方式能极大的满足lambda函数的可重用性质
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<string> Where(this IEnumerable<string> source)
{
foreach (string item in source)
{
if (item.Length<2)
{
yield return item;
}
}
}
/// <summary>
/// 这是一个Where的实现,也是一个Where的重载方法,是上面的重用扩展,这种方法的扩展性会更好,但还不是最好
/// 由这个方法可知,这种只能传递一个String的参数,所以扩展性不能说最好,为了满足可重用性的原则,我们需要
/// 将这个方法的泛型改为通用泛型T
/// </summary>
/// <param name="source"></param>
/// <param name="predicate"></param>
/// <returns></returns>
public static IEnumerable<string> Where(this IEnumerable<string> source,Func<string,bool> predicate)
{
foreach (string item in source)
{
//这里实现了委托的传递参数,函数为:x=>x.Length<2
if (predicate(item))
{
yield return item;
}
}
}
/// <summary>
/// 这种方式第三种形式,泛型的重用,实现了自定义泛型
/// </summary>
/// <param name="source"></param>
/// <param name="predicate"></param>
/// <returns></returns>
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (T item in source)
{
//这里实现了委托的传递参数,函数为:x=>x.Length<2
if (predicate(item))
{
yield return item;
}
}
}
四、Any的实现用法与内涵
作用:确定序列是否包含任何元素,可以传递Lambda函数来判断。
//any方法
Console.WriteLine(SequenceFormConsole().Any(x=>x=="hello"));
Any的核心实现
/// <summary>
/// 实现了Any的重载,确定一个序列是否包含一个值,返回bool类型。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="predicate"></param>
/// <returns></returns>
public static bool Any<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
//调用where的方法判断序列是否有值并返回此值,这样调用他的子类就可以确定次序列是否有此项。
return source.Where(predicate).GetEnumerator().MoveNext();
}
五、Count的实现用法与内涵
这个方法用于实现计数
//count方法
Console.WriteLine(SequenceFormConsole().Count(x=>x.Contains("hello")));
Count的核心实现
/// <summary>
/// 实现count的核心
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static int Count<T>(this IEnumerable<T> source)
{
var count = 0;
while (source.GetEnumerator().MoveNext()!=false)
{
count++;
}
return count;
}
/// <summary>
/// 实现count的函数重载
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="predicate"></param>
/// <returns></returns>
public static int Count<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Where(predicate).Count();
}
这里的Count:元素总量 sum:元素总和 max:元素最大值 min:元素最小值 与后面的Aggregate都是-聚合函数,用法都差不多,但是性能差别却很大,用的时候小心且要注意。
六、Aggregate的用法与内涵
这是一个累加器,与Count类似,但是也有很大区别,他有更广泛的用法,不仅仅是实现计数,还可以实现如字符串的累加(拼接)等等。下面就是实现一个拼接的例子。
Console.WriteLine(SequenceFormConsole().Aggregate((x, y) => x + "," + y));
核心实现:
/// <summary>
/// 实现一个累加器的方法,这种方式更加的通用,对于任意类型都可以实现特的函数方式的累加。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">方法调用者</param>
/// <param name="func">一个方法函数</param>
/// <returns></returns>
public static T Aggregate<T>(this IEnumerable<T> source, Func<T, T, T> func)
{
var enumerator = source.GetEnumerator();
enumerator.MoveNext();
var sum = enumerator.Current;
while (enumerator.MoveNext())
{
sum = func(sum, enumerator.Current);
}
return sum;
}
七、OrderBy与ThenBy的使用
这两个方法为排序方法,这里不做实现,实现很复杂,只要求熟练掌握并会使用就行。具体看示例:
这里有一个排序语法:descending 降序排列 。 默认升序ascending。
//OrderBy(排序)方法:这里的orderby与thenby方法并不会具体实现,这种排序方法实际上非常复杂,你可以尝试去阅读一些源码,这里索
//要知道的只有 1、排序方法非常消耗计算机资源,不只是C#,在java,object-c等等语言上都是这样,但是这种消耗的资源我们会尽量的
//去少消耗 2、了解一些排序的原理,这里的排序不会去根据不同的lambda函数一次一次的遍历序列,直至返回排序的序列,而是等待着,根
//根据所有的lambda函数,去中和出一个满足所有lambda函数的排序规则,一次性的遍历出满足所有的lamabd函数的序列并返回,这种方式
//无疑是效率很高的一种方法,不必去多次排序序列。3、合理使用OrderBy与ThenBy。
var sequences = SequenceFormConsole().OrderBy(x => x.Length).ThenBy(x => x);
foreach (var item in sequences)
{
Console.WriteLine(item);
}
八、总结与一些性能的优化与选择
性能是难以衡量的,在面对不同的工作环境时,一个算法可能并不适用所有的工作环境,取决于任务量,复杂度等等因素,Linq也是这样的一个库,在一些大多数工作环境中他往往可以胜任,但是有一些工作比如要追求极致的性能提升,可能Linq比不适用,也可能一个任务可能用Linq的一个方法比用另一个方法更快也更简洁,这就需要开发者去探索,去开发。这也正是编程的乐趣所在。下面我们就通过一些例子来分析一下那些在Linq中的有趣的方法案例。
1、有时候Any比Count更好
//在查询一个集合是否为空的时候,可以选择Count来实现计数比较。但是在集合达到百万级别(不同电脑的性能)的时候,这时候Count可能就不适合了,这个统计会统计全局的参数,而Any则具有短路的性质,这样他可以更快地完成任务。
var sequence = Enumerable.Range(0,30000000);
//你可以使用Count这样写:
sequence.Count() != null;
//你也可以用Any这样写:
sequence.Any();
//当然,这两种写法的执行效率是不同的。在数据量少时可能没区别,但是数据量过大时,就是指数级的性能提升。
但是有时往往会有偏差,比如我想统计一个餐馆来过三次以上的客人:这是后的Any就不适用,因为谈返回的结果可能不是我想要的,但是借助其他的方法,可能就是实现性能上的提升。
//介绍一些Take()与Skip();
//两个方法很好理解:Take():拿到指定数目的序列并返回。
// Skip():跳过指定数目的序列,并返回剩余的序列。
//TakeWhile()与SkipWhile();
//这两个方法都具有短路性质,他们是从序列的头开始判断,只要不满足表达式,就执行方法,且短路执行。
//TakeWhile():返回满足表示的的序列元素。但只要第一项不满足,那就之后短路,返回空序列。
//SkipWhile():跳过满足表示的的序列元素,返回其余元素。但只要第一项不满足,那就之后短路,返回整个原序列。
var sequence = Enumerable.Range(0,30000000);
//你可以使用Count这样写:
sequence.Count() > 3;
//你也可以用Any这样写:
sequence.Skip(3).Any();
//这种方式在一定数量级上会实现性能上的提升,但是假如数量是小于10的,那可能Count会更块一些,因为使用下面的方法回创建两个枚举器,这个代价是很大的,在一定程度上的选择可能会实现最佳,但是往往没有绝对的。
之后的我不想多做介绍了,不同的工作有不同的工作方式,剩下的还需要在工作中慢慢体会,理解。
九、Linq To Object的简单原理实现与总结
总之,Linq To Object就是用迭代器+泛型+委托+扩展方法 形成的这么一个方法,他可以去对实现了IEnumerable接口的集合进行一些操作,本质上是一些操作集合的API。
下面是一个对linq to object 的where方法做简单的实现,可见与普通方法的区别,其中重要的就是:
扩展方法:实现了一种功能上的调用,与实例方法与静态方法的调用不同,特可以直接由方法的第一个参数类型 以 点(.) 的方法出现。作用就是快捷,方便,便于查阅。
泛型:免于拆箱与装箱,为性能提供了保障。提高了代码的重用性。
委托:提升了代码的重用性,逻辑更好的保证了。
迭代器:可以用延迟加载的思想。
/// <summary>
/// 扩展方法:要求类必须开放且静态,且其中的方法也要开放且静态、方法的第一个参数用this关键字修饰。
/// </summary>
public static class LinqExtend
{
// 普通的扩展泛型集合,这种是用固定的集合做为返回值,局限较大,也没有IEnumerable的延迟查询的这种特性。
public static IList<T> WhereExtend<T>(this IList<T> ts, Func<T, bool> func)
{
IList<T> ts1 = new List<T>();
foreach (var item in ts)
{
if (func(item))
{
ts1.Add(item);
}
}
return ts1;
}
// 这是一个用IEnumerable作为返回值的,他可以实现延迟加载的功能特性。
public static IEnumerable<T> degelateWhere<T>(this IEnumerable<T> ls, Func<T, bool> func)
{
foreach (var item in ls)
{
if (func(item))
{
// 这里用状态机来实现延迟的思想。
yield return item;
}
}
}
}