集合和LINQ

软件开发过程中不可避免会用到集合.C#中的集合表现为数组和若干集合类. 不管是数组还是集合类, 它们都有各自的优缺点. 利用好集合是我们在开发过程中必须掌握的技巧.

LINQ(Language Integrated Query,语言集成查询) 提供类似SQL的语法, 能对集合进行遍历,筛选和投影. 一旦掌握LINQ, 你就发现在开发中再也离不开它.

建议16 : 元素数量可变的情况下不应该使用数组

C# 中数组一旦被创建, 长度就不能改变. 如果需要一个动态且可变长度的集合, 应该使用List<T>. 而数组本身, 尤其是一维数组, 也称为向量,遇到要求高效率时,会专门被优化提升效率, 其性能是最佳的. 在IL中使用了专门的指令来处理他们(newarr, ldelem,ldelema,ldlen,stelem).

从内存使用的角度来讲, 数组在创建时被分配了一段固定长度的内存, 如果数组元素时值类型, 则每个元素的长度等于相应的值类型的长度; 如果是引用类型,则每个元素的长度为该引用类型的IntPtr.Size.

ArrayList是链表结构, 可以动态的增减内存空间, 如果ArrayList存储的是值类型, 则会为每个元素增加12字节的空间, 其中4字节用于元素引用,8字节是元素装箱时引入的对象头. List<T>是ArrayList的泛型实现, 它省去了拆箱和装箱带来的开销.

使用数组的过程中应该注意大对象的问题. 大对象指超过85000字节的对象, 它们被分配在大对象堆里. 大对象的分配和回收和小对象相比都不太一样, 尤其是回收过程会带来效率很低的问题. 不能对数组指定过大的长度,这会让数组称为一个大对象.

如果要动态改变数组的长度, 如下所示:

  • 方式一: 将数组转换为ArrayList<T>
int[] iArr = { 0, 1, 2, 3, 4, 5, 6 };
//-------将数组转变为ArrayList----
ArrayList arrayListInt = new ArrayList(iArr);
arrayListInt.Add(7);
//-------将数组转变为List<T>------
List<int> listInt = iArr.ToList<int>();
listInt.Add(7);
  • 方式二: 用数组的复制功能, 数组继承自System.Array , 提供了Copy方法.

无论哪种方法改变数组长度就相当于重新创建一个数组对象.

可以创建一个名为ReSize的扩展方法的扩展方法:

public static class ClassForExtensions
{
    public static Array ReSize(this Array array, int newSize)
    {
        Type t = array.GetType().GetElementType();
        Array newArray = Array.CreateInstance(t, newSize);
        Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newSize));
        return newArray;
    }
}

// 使用方法
int[] iArr = { 0, 1, 2, 3, 4, 5, 6 };
iArr = (int[])iArr.ReSize(10);

强调一下主题: 在元素数量可变的情况下不应使用数组. 时间效率上要高100倍.

  • ResizeArray: 00:00:00.0001183
  • ResizeList: 00:00:00.0000016

建议17 : 多数情况下使用foreach进行循环遍历

关于集合的遍历, 假设存在一个数组,其遍历模式可能采用依据索引来进行遍历的方法. 又假设存在一个HashTable, 其遍历模式可能是按照键值来遍历. 无论哪个集合如果它们的遍历没有一个公共的接口, 那么客户端在进行调用的时候, 相当于是对具体类型进行编码. 这样一来,当需求变化时, 就必须修改我们的代码. 并且由于客户端代码过多地关注了集合内部的实现实现, 代码的可移植性会变得很差. 于是迭代器模式就诞生了.

自己实现一个迭代器, 用来理解该模式:

// 要求所有的迭代器全部实现该接口
interface IMyEnumerator
{
    bool MoveNext();
    object Current { get; }
}

// 要求所有的集合实现该接口
// 这样一来,客户端就可以针对该接口编码,
// 而无须关注具体的实现
interface IMyEnumerable
{
    IMyEnumerator GetEnumerator();
    int Count { get; }
}

// MyEnumerator就是一个迭代器的实现
//如果迭代需求有变化, 可以重新开发一个迭代器
class MyEnumerator : IMyEnumerator
{
   int index = 0;
   MyList myList;
   public MyEnumerator(MyList myList)
   {
       this.myList = myList;
   }

   public bool MoveNext()
   {
       if (index + 1 > myList.Count)
       {
           index = 1;
           return false;
       }
       else
       {
           index++;
           return true;
       }
   }

   public object Current
   {
       get { return myList[index - 1]; }
   }
}

// 模拟一个集合类, 继承IMyEnumerable接口, 这样客户端进行调用的时候
// 可以直接使用IMyEnumerable来声明变量
// 例如 IMyEnumerable list = new MyList();
class MyList : IMyEnumerable
{
    object[] items = new object[10];
    IMyEnumerator myEnumerator;

    public object this[int i]
    {
        get { return items[i]; }
        set { this.items[i] = value; }
    }

    public int Count
    {
        get { return items.Length; }
    }

    public IMyEnumerator GetEnumerator()
    {
        if (myEnumerator == null)
        {
            myEnumerator = new MyEnumerator(this);
        }
        return myEnumerator;
    }
}

如果未来我们新增了其他的集合类, 那么针对List的编码即使不做修改也能运行良好. 以下用法都没有针对MyList编码,而是实现了对迭代器编码.

//使用接口IMyEnumerable代替MyList
IMyEnumerable list = new MyList();
//得到迭代器,在循环中针对迭代器编码,而不是集合MyList
IMyEnumerator enumerator = list.GetEnumerator();

// for遍历
for (int i = 0; i < list.Count; i++)
{
    object current = enumerator.Current;
    enumerator.MoveNext();
}

// while遍历
while (enumerator.MoveNext())
{
    object current = enumerator.Current;
}

// foreach
foreach(var current in list)
{
   // 省略了object current = enumerator.Current;
}

可以看到foreach最大限度地简化了代码. 它用于遍历一个继承了IEmuerableIEmuerable<T>接口的集合元素.

通过观察foreach的IL代码看出:

  • 运行时会调用集合类型的get_Current和MoveNext方法
  • 在调用完MoveNext方法后, 如果结果是true, 跳转到循环开始处
    • 实际上,foreach循环和while是一样的
  • 自动将代码置入try-finally块
  • 若类型实现了IDispose接口, 它会在循环结束后自动调用Dispose方法.

如果用Reflector进行反编译,则对应的C#代码如下所示:

List<object> list = new List<object>();
using(List<object>.Enumerator CS$5$000 = list.GetEnumerator())
{
   while(CS$5$000.MoveNext())
   {
      object current = CS$5$000.Current;
   }
}

using是try-finally的语法糖, 等同于try{ 循环 } finally { CS$5$000.Dispose(); }

建议18 : foreach不能代替for

foreach两个优点:

  • 语法更简化
  • 默认调用Dispose方法

但是不一定适合所有场景. 存在的一个问题是: 它不支持循环时对集合进行增删操作. 取而代之应该使用for循环.

因为foreach循环使用了迭代器进行集合的遍历, 它在FCL提供的迭代器内部维护了一个堆集合版本的控制. 什么是集合版本呢? 简单的说, 其实它就是一个整型的变量, 任何对集合的增删操作都会使版本号加一. foreach循环会调用MoveNext方法来遍历元素, 在MoveNext方法内部会进行版本号检测,一旦检测到版本号有变动, 就会抛出InvalidOperationException异常.

for就不会有这个问题, for直接使用索引器, 它不对集合版本号进行判断, 所以不存在因为集合的变动而带来的异常(当然, 超出索引长度这种情况除外).

由于for循环和foreach循环在实现上有所不同(前者索引器, 后者迭代器), 两者性能上一直存在争议, 尤其针对泛型集合时, 两者的损耗(时间内存)是在同一数量级别上的.

因为版本检测的缘故, foreach循环并不能代替for循环.

19 使用更有效的对象和集合初始化

对象和集合初始化机制: 对象和集合初始化设定项.

初始化设定项实际相当于编译器在对象生成后对属性进行了赋值.

Person p = new Person(){ Name = "A", Age = 20}

使用集合的初始化设定项, 编译器会在集合对象创建完毕后对集合调用Add方法.

初始化设定项更重要的作用是为LINQ查询中的匿名类型进行属性的初始化, 由于LINQ查询返回的集合中匿名类型的属性都是只读的, 如果需要为匿名类型属性赋值, 或增加属性, 只能通过初始化设定项来进行, 还能为属性使用表达式.

// 集合初始化设定项, 会在对象创建完毕后对集合调用Add方法
List<Person> personList2 = new List<Person>()
{
    new Person() { Name = "Rose", Age = 19 },
    new Person() { Name = "Steve", Age = 45 },
    new Person() { Name = "Jessica", Age = 20 },
};

// 创建一个新的匿名类型, 该类型含有属性Name和AgeScope, 而AgeScope需要通过计算Person的Age属性得到
var pTemp = from p in personList2 select new { p.Name, AgeScope = p.Age > 20 ? "Old" : "Young" };
foreach (var item in pTemp)
{
    Console.WriteLine(string.Format("{0}:{1}", item.Name, item.AgeScope));
}

建议20 : 使用泛型集合代替非泛型集合

概括来讲, 如果大型集合进行循环访问,转型或拆箱装箱操作, 使用ArrayList这一的传统集合对效率影响会非常大. 鉴于此, 微软提供了对泛型的支持.

下列示例比较了非泛型集合和泛型集合在运行中的效率:

// 垃圾回收相关测试写法
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("开始测试ArrayList:");
        TestBegin();
        TestArrayList();
        TestEnd();
        Console.WriteLine("开始测试List<T>:");
        TestBegin();
        TestGenericList();
        TestEnd();
    }
    static int collectionCount = 0;
    static Stopwatch watch = null;
    static int testCount = 10000000;
    static void TestBegin()
    {
        GC.Collect();   //强制对所有代码进行即时垃圾回收
        GC.WaitForPendingFinalizers();  //挂起线程,执行终结器队列中的终结器(即析构方法)
        GC.Collect();   //再次对所有代码进行垃圾回收,主要包括从终结器队列中出来的对象
        collectionCount = GC.CollectionCount(0);    //返回在0代码中执行的垃圾回收次数
        watch = new Stopwatch();
        watch.Start();
    }

    static void TestEnd()
    {
        watch.Stop();
        Console.WriteLine("耗时:" + watch.ElapsedMilliseconds.ToString());
        Console.WriteLine("垃圾回收次数:" + (GC.CollectionCount(0) - collectionCount));
    }

    static void TestArrayList()
    {
        ArrayList al = new ArrayList();
        int temp = 0;
        for (int i = 0; i < testCount; i++)
        {
            al.Add(i);
            temp = (int)al[i];
        }
        al = null;
    }

    static void TestGenericList()
    {
        List<int> listT = new List<int>();
        int temp = 0;
        for (int i = 0; i < testCount; i++)
        {
            listT.Add(i);
            temp = listT[i];
        }
        listT = null;
    }

}

// 开始测试ArrayList:
// 耗时:788
// 垃圾回收次数:25
// 开始测试List<T>:
// 耗时:81
// 垃圾回收次数:3

建议21 : 选择正确的集合

要选择正确的集合, 首先要了解数据结构的知识, 所谓数据结构,就是相互之间存在一种或多种特定关系的数据元素的集合.

集合总体上分为线性和非线性集合.

  • 线性集合: 指元素具有唯一的前驱和后驱的数据结构类型.
  • 非线性集合: 指具有多个前驱和后驱的数据结构类型.

线性集合按存储方式又分直接存储和顺序存储.

  • 直接存储: 是指该类型的集合数据元素可以直接通过下标来访问
    • Array(包括数组和List<T>)
    • string
    • struct
    • 优点: 向数据结构中添加元素时很高效的, 直接放在数据末尾的第一个空位上就可以了.
    • 缺点: 向集合插入元素将会变得低效,它需要给插入的元素腾出位置并顺序移动后面的元素.

直接存储的数据结构中, 需要区分的是数组和List<T>的选择: 如果集合的数目固定且不涉及转型,使用数组效率高, 否则就是用List<T>.

  • 顺序存储结构即线性表. 线性表可动态地扩大和缩小, 它在一片连续的区域中存储数据元素. 线性表不能按照索引进行查找, 它是通过对地址的引用来搜索元素的, 为了找到某个元素, 它必须遍历所有元素
    • 优点: 插入和删除效率高
    • 缺点: 查找效率相对低一点

线性表又可以分为队列,栈及索引集群.

  • Queue<T>,Stack<T>
    • 队列: 先进先出, 可以用来处理并发命令等场景: 先让所有客户端的命令入队,然后由专门的工作先出来执行队列的命令, 在分布式中的消息队列就是一个典型的队列应用实例.
    • 栈: 先入后出
  • 索引集群进一步泛化为: 字典类型Dictionary<TKey,TValue>和双向链表LinkedList<T>
    • 字典: 值基于键的Hash码基础上进行存储. 字典类对象由包含集合元素的存储桶组成, 每一个存储桶与基于该元素的键的Hash码关联. 根据键的值进行查找,使用字典将会使搜索和检索更快捷.
    • 双向链表: LinkedList<T>是一个类型为LinkedListNode的元素对象的集合. 当我们觉得在集合中插入和删除数据很慢时,可以考虑使用链表. 此类型没有其他集合普遍的Add方法,取而代之的是AddAfter,AddBefore,AddFirst,AddLast等方法.

非线性集合分为层次集合和组集合.

  • 层次集合: 树, 在FCL中没有实现.
  • 组集合: 分为集和图. 集在FCL中实现为HashSet<T>, 图则没有对应实现.
    • 集的概念本意是指存放在集合中的元素是无序的且不能重复的

还有几个需要掌握的集合类型, 它们是实际应用中发展而来的对以上基础类型的扩展, 作用是将无序排列变成有序排列:

  • SortedList<T> 对应 List<T>
  • SortedDictionary<TKey,TValue> 对应 Dictionary<TKey,TValue>
  • SortSet<T> 对应 HashSet<T>

以及多线程集合类: 在命名空间System.Collections.Concurrent下, 主要是:

  • ConcurrentBag<T> 对应 List<T>
  • ConcurrentDictionary<TKey,TValue> 对应 Dictionary<TKey,TValue>
  • ConcurrentQueue<T> 对应 Queue<T>
  • ConcurrentStack<T> 对应 Stack<T>

如果集合被应用于多线程应用中, 可以使用这几个集合类型

FCL集合类图:

建议22 : 确保集合的线程安全

前面提到foreach不能替代for的一个原因是在迭代过程中对集合本身进行了增删操作. 这个场景移植到多线程场景中, 要确保集合的线程安全. 集合线程安全: 是指在多个线程上添加或删除元素时, 线程之间必须保持同步.

以下模拟另一个线程对集合的元素进行删除:

class Program
{
    static List<Person> list = new List<Person>()
        {
            new Person() { Name = "Rose", Age = 19 },
            new Person() { Name = "Steve", Age = 45 },
            new Person() { Name = "Jessica", Age = 20 },
        };
    // System.Threading命名空间提供了一个抽象基类WaitHandle。这个简单的类唯一的作用就是包装一个Windows内核对象句柄。
    // 类层次继承结构如下:
    //        WaitHandle
    //            EventWaitHandle
    //        AutoResetEvent
    //            ManualResetEvent
    //        Semaphore (信号量)
    //        Mutex (互斥体)
    // EventHandle(Event构造),事件实际上就是由内核维护的Boolean变量。
    // 事件为false在事件上等待的线程就阻塞, 为true就解除阻塞。
    // 有两种事件,即自动重置事件(AutoResetEvent)和手动重置事件(ManualResetEvent)
    // 区别就在于是否在解除第一个线程的阻塞后,将事件自动重置为false, 造成其余线程继续阻塞.
    // 而当手动重置事件为true时, 它解除正在等待它的所有线程的阻塞, 因为内核不将事件自动重置回false, 需要在代码中将事件手动重置回false
    static AutoResetEvent autoSet = new AutoResetEvent(false);

    static void Main(string[] args)
    {
        Thread t1 = new Thread(() =>
        {
            //确保等待t2开始之后才运行下面的代码
            autoSet.WaitOne();
            foreach (var item in list)
            {
                Console.WriteLine("t1:" + item.Name);
                Thread.Sleep(1000);
            }
        });
        t1.Start();
        Thread t2 = new Thread(() =>
        {
            //通知t1可以执行代码
            autoSet.Set();
            //沉睡1秒是为了确保删除操作在t1的迭代过程中
            Thread.Sleep(1000);
            list.RemoveAt(2); // 此处会报System.InvalidOperationException: 集合已修改;可能无法执行枚举操作。
        });
        t2.Start();
    }

}

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

早在泛型集合出现之前, 非泛型集合一般会提供一个SyncRoot属性, 要保证非泛型集合的线程安全, 通过锁定该属性来实现. 其线程安全则应该在迭代和删除的时候都加上lock.

lock (list.SyncRoot)
{
   foreach (Person item in list)
   {
       Console.WriteLine("t1:" + item.Name);
       Thread.Sleep(1000);
   }
}

//省略部分代码

//通知t1可以执行代码
autoSet.Set();
//沉睡1秒是为了确保删除操作在t1的迭代过程中
Thread.Sleep(1000);
lock (list.SyncRoot)
{
   list.RemoveAt(2);
   Console.WriteLine("删除成功");
}

上述代码不会抛出异常, 因为锁的互斥的机制保证了同一时刻只能有一个线程操作集合元素. 我们发现泛型集合没有这样的属性. 必须要自己创建一个锁对象来完成同步任务.

static object sycObj = new object();
//确保等待t2开始之后才运行下面的代码
autoSet.WaitOne();
lock (sycObj)
{
   foreach (Person item in list)
   {
       Console.WriteLine("t1:" + item.Name);
       Thread.Sleep(1000);
   }
}
// 省略部分代码

//通知t1可以执行代码
autoSet.Set();
//沉睡1秒是为了确保删除操作在t1的迭代过程中
Thread.Sleep(1000);
lock (sycObj)
{
    list.RemoveAt(2);
    Console.WriteLine("删除成功");
}

在建议21中,Concurrent命名空间下,有实现了多线程环境的线程安全的集合类, 可以根据需求选择这些集合类型.

避免将泛型List作为自定义集合类的基类

要实现一个自定义的集合类不应该以FCL集合类为基类, 而是应该扩展想要的泛型接口, FCL集合类应该以组合的形式包含至自定义的集合类. 需要扩展的泛型接口通常是IEnumerable<T>ICollection<T>(或者它的子接口例如: IList<T>).

IEnumerable<T>规范了集合类的迭代功能, ICollection<T>规范了一个集合通常会有的操作.

// 以下2个实现的集合类都能完成默认的需求
class Employees1 : List<Employees> {}
class Employees2 : IEnumerable<Employees>, `ICollection<Employees>`{}

遗憾的是, List<T>基本上没有提供可供子类使用的protected成员(从object继承来的Finalize方法和MemberwiseClone方法除外), 也就是说,继承List<T>并没有带来任何继承上的优势, 反而丧失了面向接口编程带来的灵活性. 而且容易出bug.

以Employees1为例, 如果要在Add方法中加入某些变化, 比如为名字后面添加一个”Changed”后缀,

static void Main(string[] args)
{
    Employees1 employees1 = new Employees1()
    {
        new Employee(){ Name = "Mike" },
        new Employee(){ Name = "Rose" }
    };
    // 出错的地方, 使用了IList里的Add方法导致输出没有达到预期
    IList<Employee> employees = employees1;
    employees.Add(new Employee() { Name = "Steve" });
    foreach (var item in employees1)
    {
        Console.WriteLine(item.Name);
        // Mike Changed
        // Rose Changed
        // Steve
    }
}

class Employee
{
    public string Name { get; set; }
}

class Employees1 : List<Employee>
{
    public new void Add(Employee item)
    {
        item.Name += " Changed!";
        base.Add(item);
    }
}

以上输出没有达到预期. 要正确使用应该如下所示:

static void Main(string[] args)
{
   Employees2 employees2 = new Employees2()
   {
       new Employee(){ Name = "Mike" },
       new Employee(){ Name = "Rose" }
   };
   ICollection<Employee> employees = employees2;
   employees.Add(new Employee() { Name = "Steve" });
   foreach (var item in employees2)
   {
       Console.WriteLine(item.Name);
   }
}

class Employees2 : IEnumerable<Employee>, ICollection<Employee>
{
   List<Employee> items = new List<Employee>();

   #region IEnumerable<Employee> 成员

   public IEnumerator<Employee> GetEnumerator()
   {
       return items.GetEnumerator();
   }

   #endregion

   #region ICollection<Employee> 成员

   public void Add(Employee item)
   {
       item.Name += " Changed!";
       items.Add(item);
   }

   //省略

   #endregion
}

建议24 : 迭代器应该是只读的

FCL中的迭代器只有GetEnumerator没有SetEnumerator方法. 原因有二:

  • 违背了设计模式中的开闭原则. 被设置到集合中的迭代器可能会直接导致集合的行为发生异常或变动. 一旦需要新的迭代需求,完全可以创建一个新的迭代器来满足需求,而不是设置该迭代器.
  • 现在, 我们有了LINQ使用LINQ可以不用创建任何新的类型就能满足任何迭代需求.(建议30)

如果允许了SetEnumerator方法,会出现业务B中设置新的迭代器,在没通知A的情况下对业务A进行了干扰.

所以迭代器模式的原则是, 不要为迭代器设置可写属性.

建议25 : 谨慎集合属性的可写操作

如果类型的属性中有集合属性, 那么应该保证属性对象是由类型本身产生的. 如果将属性设置为可写, 则会增加抛出异常的几率. 一般情况下, 如果集合属性没有值, 则它返回的Count等于,而不是集合属性的值为null. 以下代码将产生一个空指针异常:

// 因为是引用类型, 2个线程都有针对该属性对象的引用
// 导致任何一个线程修改都会影响到所有引用
static List<Student> listStudent = new List<Student>()
{
    new Student(){ Name = "Mike", Age = 1},
    new Student(){ Name = "Rose", Age = 2}
};

static void Main(string[] args)
{
    StudentTeamA teamA = new StudentTeamA();

    // 线程t1模拟对类型StudentTeamA的Students属性进行复制
    // Students这是一个可读/可写的属性
    Thread t1 = new Thread(() =>
    {
        teamA.Students = listStudent;
        Thread.Sleep(3000);
        Console.WriteLine(listStudent.Count); //模拟对集合属性进行一些运算
    });
    t1.Start();

    // 线程t2是另外一个程序员写的,
    // 对listStudent修改会影响到t1线程中teamA.Students的引用
    Thread t2 = new Thread(() =>
    {
        listStudent = null;   //模拟在别的地方对list1而不是属性本身赋值为null
    });
    t2.Start();
}
}

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}
// 问题版本:
class StudentTeamA
{
    public List<Student> Students { get; set; }
}

对StudentTeamA改进:

// 完善版本
class StudentTeamA
{
   // 设置为只读
   public List<Student> Students { get; private set; }

   public StudentTeamA()
   {
       Students = new List<Student>();
   }
   // 使用迭代器接口
   public StudentTeamA(IEnumerable<Student> studentList) : this()
   {
       Students.AddRange(studentList);
   }
}

// 用法如下:
 StudentTeamA teamA2 = new StudentTeamA();
 teamA2.Students.Add(new Student() { Name = "Steve", Age = 3 });
 teamA2.Students.AddRange(listStudent);
 Console.WriteLine(teamA2.Students.Count);
 //也可以像下面这样实现
 StudentTeamA teamA3 = new StudentTeamA(listStudent);

建议26 : 使用匿名类型存储LINQ查询结果

匿名类型 由var,赋值运算符和一个非空初始值(或者以new开头的初始化项)组成.

以下基本特性:

  • 支持简单类型也支持复杂类型.
    • 简单类型必须是一个非空初始值
    • 复杂类型则是一个以new开头的初始化项;
  • 匿名类型的属性是只读的,没有属性设置器, 一旦被初始化就不可更改
  • 如果两个匿名类型的属性值相同, 那么就认为两个匿名类型相等
  • 匿名类型可以在循环中用作初始化器
  • 匿名类型支持智能感知
  • 不常用: 匿名类型确实也可以拥有方法

将一些数据关联成一个新的类型. 临时需求需要创建很多临时类型, 如果全部使用普通的自定义类型, 代码将会膨胀而变得难以维护. 这个时候匿名类型就派上用场了.

在类型仅仅被当前的代码使用,或者被用于存储某种查询结果的场景中,匿名类型将大有用途。匿名类型的这个特点使它成为保存LINQ查询结果的最佳搭档。

例如数据库存储一下表格:

Company表
CompanyID|Name
:—:|:—:
0|Micro
1|Sun

Person表
Name|CompanyID
:—:|:—:
Mike|1
Rose|0
Steve|1

按照传统的做法,Company记录表和Person记录表在应用程序中都有对应的实体类。我们要创建的临时对象包括PersonName和CompanyName,满足该需求的一个示例:

var personWithCompanyList = from person in personList
                            join company in companyList
                            on person.CompanyID equals company.ComanyID
                            select new
                            {
                              PersonName = person.Name,
                              CompanyName = company.Name
                            };
foreach (var item in personWithCompanyList)
{
    Console.WriteLine(string.Format("{0}\t:{1}", item.PersonName, item.CompanyName));
}

new之前的代码是LINQ关键字,new 之后的代码就是匿名类型的初始化项。

该匿名类型包含两个属性:PersonName和CompanyName。将匿名类型中对应的属性重命名在有些时候是件很有必要的事情。本例中,如果不将Person中的Name重命名为PersonName,以及将Company中的Name重命名为CompanyName,会给我们的实际编码造成一定的困扰。

匿名类型也派生自Object,查看上面代码的IL代码我们知道,匿名类型会在IL中发出一个类(也称为一个投影),

非匿名类型包括的Equals、GetHashcode、 ToString 等方法匿名类型都有。并且,编译器为我们重载了ToString方法,它返回的是类型的属性及对应的值。如果为上面的循环输出部分增加一条语句:

Console.WriteLine(item.ToString());
//则输出一下内容
// Mike    :Sun
// { PersonName = Mike, CompanyName = Sun }
// Rose    :Micro
// { PersonName = Rose, CompanyName = Micro }
// Steve   :Sun
// { PersonName = Steve, CompanyName = Sun }

建议27 : 在查询中使用Lambda表达式

LINQ实际上是基于扩展方法和Lambda表达式的,理解了这一点就不难理解LINQ。任何LINQ查询都能通过调用扩展方法的方式来替代.

var personWithCompanyList = from person in personList
                            select new
                            {
                              PersonName = person.Name,
                              CompanyName = person.ComanyID == 0 ? "Micro" : "Sun"
                            };

这个查询语句中, 通过命名初始化的方式投影出了一个新的类型, 它包含属性PersonName和CompanyName.

另外一种用法, 直接调用扩展方法select,并且为select方法传入一个Lambda表达式.

foreach(var item in personList.Select(person =>
        new {
                PersonName = person.Name,
                CompanyName = person.ComanyID == 0 ? "Micro" : "Sun"
            }))
{
      Console.WriteLine(...);        
}

针对LINQ设计的扩展方法大多数应用了委托泛型. System命名空间定义了泛型委托:

  • Action
    • 执行一个操作, 没有返回值
  • Func
    • 执行一个操作并返回一个值
  • Predicate
    • 用于定义一组条件并判断参数是否符合条件

Select扩展方法接收的就是一个Func委托. Lambda表达式就是一个简洁的委托.”=>” 左边是方法的参数, 右边是方法体.

调用Where扩展方法的例子:

foreach(var item in personWithCompanyList.Where(p => p.CompanyName == "Sun"))
{
   Console.WriteLine(item.PersonName);
   // 输出:
   // Mike
   // Steve
}

OrderByDescending扩展方法:

foreach(var item in personWithCompanyList.OrderByDescending(p => p.Name))
{
   Console.WriteLine(item.PersonName);
   // 输出:
   // Steve
   // Rose
   // Mike
}

理解延迟求值和主动求值之间的区别

要理解延迟求值(lazy evaluation)主动求值(eager evaluation),首先来看一个例子:

List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 只是定义了查询, 而且不是立刻执行.
var temp1 = from c in list where c > 5 select c;
// 对查询调用ToList, ToArray方法会立刻执行.
// 因此, 之后的list[0]修改对temp2无效
var temp2 = (from c in list where c > 5 select c).ToList<int>();
list[0] = 11;
Console.Write("temp1: ");
foreach (var item in temp1)
{
   Console.Write(item.ToString() + " ");// 11 6 7 8 9
}
Console.Write("\ntemp2: ");
foreach (var item in temp2)
{
   Console.Write(item.ToString() + " ");// 6 7 8 9
}

延迟求值: 不会立刻执行, 在使用LINQ to SQL时, 能够带来显著的性能提升. 例如: 如果定义了两个查询, 而且采用延迟求值,CLR则会合并两次并生成一个最终查询(第二次查询在第一次查询基础上的前提):

// 延迟求值
var temp1 = from p in persons where p.Age > 20 select p;
// 但是因为temp1是延迟求值, CLR则会合并两次查询并生成一个最终SQL语句查询
var temp2 = from p in temp1 where p.Name.IndexOf('e') > 0 select p;

// 主动求值, 立即进行查询, 生成SQL语句
var temp1 = (from p in persons where p.Age > 20 select p).ToList<Person>();
// 从temp1结果中查找, 不会再生成下句代码的SQL查询
var temp2 = from p in temp1 where p.Name.IndexOf('e') > 0 select p;

事实上,应该仔细体会延迟求值和主动求值之间的区别,体会两者在应用中会带来什么样的输出结果:否则,很有可能会出现一些 我们意想不到的Bug。

##建议29 : 区别LINQ查询中的IEnumerable和Queryable

在System.Linq的命名空间下右两个静态类Enumerable和Queryable. 稍加观察我们会发现,接口IQueryable<T>实际也是继承了IEnumerable<T>接口的,所以,致使这两个接口的方法在很大程度上是一致的。那么,微软为什么要设计出两套扩展方法呢?

LINQ查询从功能上来讲实际可分为三类:

  • LINQ to object
    • 使用Enumerable中的扩展方法对本地集合进行排序和查询等操作.
  • LINQ to SQL
    • 使用Queryable中的扩展方法, 它接收的参数时Expression<>, 用于包装Func<>,引擎最终会将表达式树转化为响应的SQL语句,然后在数据库中执行.
  • LINQ to XML

设计两套接口的原因正是为了区别对待LINQ to Object和LINQ to SQL, 两者对于查询的处理在内部使用的是完全不同的机制.

到底什么时候使用IQueryable<T>,什么时候使用IEnumerable<T>呢? 简单的表示为:

  • 本地数据源使用IEnumerable<T>
  • 远程数据源使用IQueryable<T>
  • 在LINQ to SQL的查询中尽量使用IQueryable<T>

在使用IQueryable<T>IEnumerable<T>的时候还需要注意一点,IEnumerable<T>查询的逻辑可以直接用我们自己所定义的方法,而IQueryable<T>则不能使用自定义的方法,它必须先生成表达式树,查询由LINQ to SQL引擎处理。在使用IQueryable<T>查询的时候,如果使用自定义的方法,则会抛出异常。

但是如果将查询换成一个IEnumerable<T>查询,这种模式是支持的.

List<int> list = new List<int>() {19,20,21,22};
var temp = from c in list where OlderThan20(c) select;

建议30 : 使用LINQ取代集合中的比较器和迭代器

在建议10中为了满足BaseSalary的排序要求, 让类继承了IComparable<T>接口,实现了一个默认的CompareTo方法, 然后又接到第二个需求, 为Bonus排序, 由于IComparable<T>只提供一个CompareTo方法,因此需要创建一个比较器,在Sort排序时传入比较器.

以上方式实现的排序至少存在2个问题:

  • 可扩展性太低, 有新的排序要求就需要实现新的比较器
  • 对代码的侵入太高,为类型继续接口,增加新的方法

用LINQ就能实现即使类型只存在自动实现的属性,也能满足多方面的排序要求. LINQ提供了类似于SQL的语法来实现遍历,筛选与投影集合的功能.

tatic void Main(string[] args)
{
    List<Salary> companySalary = new List<Salary>()
        {
            new Salary() { Name = "Mike", BaseSalary = 3000, Bonus = 1000 },
            new Salary() { Name = "Rose", BaseSalary = 2000, Bonus = 4000 },
            new Salary() { Name = "Jeffry", BaseSalary = 1000, Bonus = 6000 },
            new Salary() { Name = "Steve", BaseSalary = 4000, Bonus = 3000 }
        };
    //-----------------------------------------------    
    Console.WriteLine("默认排序:");
    foreach (Salary item in companySalary)
    {
        Console.WriteLine(string.Format("Name:{0} \tBaseSalary:{1} \tBonus:{2}", item.Name, item.BaseSalary, item.Bonus));
    }
    //-----------------------------------------------
    Console.WriteLine("BaseSalary排序:");
    var orderByBaseSalary = from s in companySalary orderby s.BaseSalary select s;
    foreach (Salary item in orderByBaseSalary)
    {
        Console.WriteLine(string.Format("Name:{0} \tBaseSalary:{1} \tBonus:{2}", item.Name, item.BaseSalary, item.Bonus));
    }
    //-----------------------------------------------
    Console.WriteLine("Bonus排序:");
    var orderByBonus = from s in companySalary orderby s.Bonus select s;
    foreach (Salary item in orderByBonus)
    {
        Console.WriteLine(string.Format("Name:{0} \tBaseSalary:{1} \tBonus:{2}", item.Name, item.BaseSalary, item.Bonus));
    }
}

foreach实际隐含调用的是集合对象orderByBaseSalary里的迭代器, 以往如果我们要绕开集合的Sort方法对集合元素按照一定的顺序进行迭代, 则需要让类型继承IEnumerable接口, 实现一个或多个迭代器,现在从LINQ查询生成匿名类型来看, 相当于可以无限为集合增加迭代需求.

LINQ相当于封装了FCL泛型集合的比较器,迭代器,索引器. 因此强烈建议你利用LINQ所带来的便利性,但我们仍需掌握比较器,迭代器,索引器的原理.

建议31 : LINQ查询中避免不必要的迭代

无论SQL查询还是LINQ查询,搜索到的结果立刻返回总比搜索完所有的结果再将结果返回的效率要高.

// 第一种
from c in list where c.Age == 20 select c ;
// 第二种
from (c in list where c.Age >= 20 select c).First() ;

通常我们会认为第一种效率会高一些, 似乎返回的就是等于20的那两个元素, 第二种模式则需要查询所有大于20的元素. 实际情况不是这样的. 第一种查询集合迭代了5次, 第二种查询集合仅迭代了1次. 这是因为第二种查询时因为20正好放在List的首位. First方法实际完成的工作是: 搜索到满足条件的第一个元素就从集合中返回,如果没有符合条件的元素,也会遍历整个集合.

与First方法类型的还有Take方法接收一个整型参数,然后为我们返回该参数指定的元素个数, 满足条件后会立即返回.

如果一个集合包含了很多元素, 那么这种查询会为我们带来可观的时间效率.

在实际的编码过程中, 要充分运用First和Take等方法,这样才能带来高效性, 不让时间浪费在一些无效的迭代中.