求知若饥,虚心若愚
支持标准查询操作符的集合接口
- 集合初始化器,使用和数组声明相似的语法构造集合
- 实现了IEnumerable接口就可使用初始化器{}
- IEnumerable接口来使用foreach语句,实际上是使用了迭代器模式,只需要知道第一个、下一个、最后一个元素就能遍历
- IEnumerable接口只有GetEnumerator方法来获取迭代器,该迭代器由IEnumerator接口定义,后者一般需要实现Current属性和MoveNext、Reset方法。单使用IEnumerator不足以解决交错遍历和错误处理的问题,所以一般使用IEnumerable来表示我可以枚举,至于具体的迭代器由类(一般是一个内嵌类,而不是集合类本身管理)自己去实现。
- IEnumerator可以实现IDisposable接口,配合using或者显式调用可以清理状态
- foreach循环内不要修改集合,一是如果是值类型改了也没用,二是新增了元素那么迭代器需要怎么处理。所以c#干脆禁用了,会抛出System.InvalidOperationException.
- 实现了IEnumerable接口的类可以配合Linq引入超过50个不需要显式实现的方法,由c#3.0起的扩展方法引入
扩展方法 描述 where 筛选数据,传入一个谓词(一个实参并返回bool),该表达式是先传给集合并不马上执行,只有当遍历时才求值执行 Select 投射数据,将原数据处理成自己想要的对象(可以是元组),有点类似c++transform或者lua walk遍历去转换数据。 Count 对元素进行计数,可传入一个谓词(不传则为全部),满足条件才计入。但是会遍历集合,如果集合本身带有count优先使用。 Any 集合是否存在一个或以上元素,有元素便停止遍历。 OrderBy/ThenBy 排序集合。ThenBy可以链式多次使用,但OrderBy每次使用都会重新排序。OrderBy返回的是IOrderedEnumerable 接口,派生自IEnumerable .(应该是限制ThenBy的使用) Join 执行内部联接,将两个有关联的集合联接在一起查询 GroupBy 分组结果。返回的是IGrouping<TKey,TElement>类型的数据项,派生自IEnumerable GroupJoin 来实现一对多的关系。和Join类似,但将符合条件的集合绑定到内联集合中。 SelectMany 和select类似,但select返回的数据项和原集合一致,当处理集合里的集合时有可能需要将二级集合元素拆出来,这时候就要用SelectMany OfType 只返回特定类型的项 Union 合并两个集合,不包含重复的项 Concat 保持顺序的情况下合并两个集合,且保留重复项 Intersert 取交集 Distinct 去除重复项 SequenceEquals 比较两个集合是否一致,包括顺序 Reverse 反转集合项顺序 Average、Sum、Max、Min 常用聚合类方法 - Linq查询具有推迟执行的特点,如果有where等过滤语句,则每次遍历时都会触发一次过滤(Count也会触发遍历),所以很容易不知不觉造成重复执行。解决方案是将过滤结果用ToXXX方法保存起来(虽然ToXXX也会执行一次)或者返回IQueryable
,避免后续使用该迭代器重复执行Linq。这个必须重视,如果是消耗比较大的查询。 - IQueryable
是没有扩展方法的IEnumerable ,但除此之外,还可通过它来实现自定义的LINQ Provider。IEnumerable 可以调用AsQueryable - 匿名类型,c#3.0的产物,可以配合LINQ生成临时数据类型。实际还是强类型,编译器会分配类型。成员以及顺序一致的会当做同一个类型,否则不是,之所以顺序也要一致是因为编译器重写了ToString,顺序不同会导致ToString结果不同。匿名类型不能用集合初始化器(Array除外),因为需要知道类型,当然可以通过实现一个泛型初始化工厂函数来避开,因为这个时候已经知道类型。总之如果是c#7.0以后就尽量用元组来代替匿名类型,可以方便传出函数外和避免类型污染。
使用查询表达式的LINQ
查询语句总是以"form子句"开始,以"select子句"或者"group···by子句"
使用了与SQL接近的语法,但顺序不同,为了支持IDE的智能感知
- 使用表达式也会推迟执行,和直接使用LINQ方法一致的规则。也会在Count、Foreach等语句重复执行,原因在于C#为了每次结果都是最新而且正确的,都会重新执行。其背后原理其实是委托,表达式会被编译器转成Lambda表达式,最后在生成委托,合适的时候调用。
- orderby语句,支持多个条件,用逗号隔开,默认是升序,可条件后紧接ascending|descending关键字改变升降序规则
- 可在第一个from子句之后、最后一个select/group···by子句之间使用范围变量关键字let,用于重复引用、生成的对象
- 使用’into’来查询延续,查询后将结果紧接着另一个查询
- 可以使用多个from筛选不同来源的数据又或者处理上一个from的数据
构建自定义集合
集合接口
- IList
,通过位置索引来获取值 - IDictionary<TKey, TValue>,通过键来获取值
- ICollection
,有两个成员Count()和CopyTo(),后者可以指定一个数组和index,表示从index开始将集合拷贝过去,需要调用者保证足够内存
主要集合类
- List
,性质和数组类似,但随着元素增多会自动扩展(TrimToSize、Capacity)。(和C++Vector有点类似)其中的Sort方法一般调用类型实现了IComparable 接口的CompareTo方法,但也可以手动传入一个IComparer 作为实参比较器来比较。 - List
常用的搜索方法有Contains、IndexOf、LastIndexOf、BinarySearch。其中BinarySearch要求集合已排好序,且如果没找到的话会返回一个负整数,对这个负整数取反(~)可快速得到查找元素应插入的位置。 - Dictionary<TKey, TValue>,使用键值对来存取,不要依赖字典键的添加顺序。构造时可以提供IEqualityComparer
的一个实现来作为相等性判断器,需要实现Equals和GetHashCode两个函数,主要如果两个对象相等,那么他们也要有相同的hashcode,反之则不然,两个不相等的对象可能有一样的hashcode,毕竟冲突不可避免。生存期间哈希码要保持不变,且这两个函数不能引发异常。 - 已排序集合:SortedDictionary<TKey, TValue>和SortedList
,有序字典是按键排序,相比无序插入和删除都会慢一点。 - 栈集合Stack
,后入先出,关键方法是Push和Pop,Peek可不修改栈的前提下获取栈顶元素 - 队列集合Queue
,先入先出,关键方法是Enqueue和Dequeue。会自动扩大空间,但不会回收。可手动调用TrimToSize回收 - 链表LinkedList
,支持正反序遍历
提供索引器
- 语法使用this关键字,后面紧接[T index],实现语法类似属性,也有get/set,可重载不同的索引参数(甚至可以是可变参数)
- 索引器的CIL属性默认名称为Item,所以不可以定义类似名的属性,但可以通过IndexerNameAttibute特性指定一个不同的名称。
返回null或者空集合
- 可使用Enumerable.Empty
生成空集合 - 空集合好处是调用者不需要判空处理
- 两者都是集合为空时的反馈
迭代器
类要支持使用foreach进行迭代,就必须实现枚举数模式,以从IEnumerable
接口获取的IEnumerator 接口为基础
选择使用迭代器,而不是手动实现枚举数模式,能显著提高程序员的编程效率(GetEnumerator配合yield)。CIL会自动生成对应的私有类来实现IEnumerator
接口,生成对应的Current属性和MoveNext方法
- 迭代器类似于函数,但它不是返回(return)一个值,而是生成(yield)一系列值
- 正确实现迭代器模式需维护一些内部状态,以便在枚举集合时跟踪记录当前位置
- 使用yield return,每次调用完以后,等待下一次调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class BinaryTree<T> : IEnumerable<T>
{
···
public IEnumerator<T> GetEnumerator()
{
// 返回根节点
yield return value;
foreach (BinartTree<T> tree in SubItems)
{
if (tree != null)
{
// 会调用子树的迭代器,递归调用
foreach (T item in tree)
{
yield return item;
}
}
}
// 注意如果树的深度比较深,那么消耗可能比较大
}
} - 使用yield break 取消更多的迭代
- 可在一个类返回多个迭代器,但返回的类型是IEnumerable
,然后外部显式调用。我的理解是生成了另一个IEnumerable ,而不是默认那个。