Linq研究

微软在.NET 3.5中加入了LINQ技术,作为配套改进了C#语言,加入了Lambda表达式,扩展方法,匿名类型等新特性用以支持LINQ。微软同时提出了要使用声明式编程,即描述计算规则,而不是描述计算过程。

使用LINQ技术能很好地做到声明式编程,写出的代码表意能力强,可读性高,避免了以往或其他语言的代码中充斥大量表意不明的for循环甚至多层循环的问题。不要小看for循环和Where,Select,OrderBy等扩展方法的区别,可以不通过注释一眼就能看出代码意图真的很重要。当看到Java代码中一大堆的for循环,包括多层循环,又没有注释,必须仔细看才能了解代码作用时,真的很头大。个人认为LINQ是C#语言区别于其他语言的最显著的特性,也是最大的优势之一。

当然现在大多数主流语言都加入了Lambda表达式,从而可以使用类似于LINQ的技术,达到声明式编程。比如Java语言在Java 8中加入了和C#几乎一样的Lambda表达式语法,并加入了Stream API,以达到类似于LINQ的用法。

如此可见,声明式编程是发展趋势,既然使用C#,就要多用LINQ,用好LINQ,用对LINQ。不要再写一堆一堆的for循环了!

要用好LINQ,就要学好LINQ,理解其原理,机制和用法。推荐一个学习和研究LINQ的好工具LINQPad,下面是官网和官网上的截图。

http://www.linqpad.net/

下面针对几个关键点,对LINQ进行一些初步研究。有些问题可能是使用LINQ多年的人都理解得不对的。

首先看下面的程序。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LinqResearch
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<int> { 2, 1, 6, 4, 3, 5, 7, 8, 10, 9 };
            Console.WriteLine("list");
            var list1 = list.Select(i =>
            {
                Console.WriteLine("In select {0}", i);
                return i * i;
            });
            Console.WriteLine("list1");
            var list2 = list1.Where(i =>
            {
                Console.WriteLine("In where>10 {0}", i);
                return i > 10;
            });
            Console.WriteLine("list2");
            var list3 = list2.Where(i =>
            {
                Console.WriteLine("In where<60 {0}", i);
                return i < 60;
            });
            Console.WriteLine("list3");
            var list4 = list3.OrderBy(i =>
            {
                Console.WriteLine("In orderby {0}", i);
                return i;
            });
            Console.WriteLine("list4");
            var list5 = list4.ToList();
            Console.WriteLine("list5");
            foreach (var i in list5)
            {
                Console.WriteLine(i);
            }
        }
    }
}

先不要看下面的运行结果,想想打印出的是什么,然后再看结果,看看和想的一样吗?

list
list1
list2
list3
list4
In select 2
In where>10 4
In select 1
In where>10 1
In select 6
In where>10 36
In where<60 36
In select 4
In where>10 16
In where<60 16
In select 3
In where>10 9
In select 5
In where>10 25
In where<60 25
In select 7
In where>10 49
In where<60 49
In select 8
In where>10 64
In where<60 64
In select 10
In where>10 100
In where<60 100
In select 9
In where>10 81
In where<60 81
In orderby 36
In orderby 16
In orderby 25
In orderby 49
list5
16
25
36
49

为什么先打印出list 到 list4,而没有进到Lambda里面?

这是因为LINQ是延时计算的,即只有foreach或ToList时才去做真正计算,前面的Select,Where等语句只是声明了计算规则,而不进行计算。

这点很重要,如果不明白这点,就会写出有BUG的代码,如下面的程序,打印出的是1和2,而不是1。

            var a = 2;
            var list = new List<int> { 1, 2, 3 };
            var list1 = list.Where(i => i < a);
            a = 3;
            foreach (var i in list1)
            {
                Console.WriteLine(i);
            }

后面打印出的为什么先是select和where交错,然后是orderby,而不是先select再where,最后orderby?

这时因为Select,Where等这些扩展方法,在声明计算规则时是有优化的(内部可能通过表达式树等方法实现),它并不是傻傻的按照原始定义的规则,顺序执行,而是以一种优化的方法计算并获得结果。所以使用 LINQ一般会比自己写的原始的一大堆for循环性能还高,除非花大量时间优化自己的逻辑(一般不会有这个时间)。

可以看到针对元素2和1,并没有打印出In where<60 的行,这说明针对这两个元素,第二个Where里的代码并没有执行,因为第一个Where都没有通过。在进行完投影(Select)和筛选(Where)后,最后进行排序(OrderBy),只针对筛选后留下的元素执行OrderBy里面的计算逻辑,一点也不浪费。

上面的程序有人可能会写成这样。

            var list = new List<int> { 2, 1, 6, 4, 3, 5, 7, 8, 10, 9 };
            Console.WriteLine("list");
            var list1 = list.Select(i =>
            {
                Console.WriteLine("In select {0}", i);
                return i * i;
            }).ToList();
            Console.WriteLine("list1");
            var list2 = list1.Where(i =>
            {
                Console.WriteLine("In where>10 {0}", i);
                return i > 10;
            }).ToList();
            Console.WriteLine("list2");
            var list3 = list2.Where(i =>
            {
                Console.WriteLine("In where<60 {0}", i);
                return i < 60;
            }).ToList();
            Console.WriteLine("list3");
            var list4 = list3.OrderBy(i =>
            {
                Console.WriteLine("In orderby {0}", i);
                return i;
            }).ToList();
            Console.WriteLine("list4");
            var list5 = list4.ToList();
            Console.WriteLine("list5");
            foreach (var i in list5)
            {
                Console.WriteLine(i);
            }

这样写打印出的结果为,

list
In select 2
In select 1
In select 6
In select 4
In select 3
In select 5
In select 7
In select 8
In select 10
In select 9
list1
In where>10 4
In where>10 1
In where>10 36
In where>10 16
In where>10 9
In where>10 25
In where>10 49
In where>10 64
In where>10 100
In where>10 81
list2
In where<60 36
In where<60 16
In where<60 25
In where<60 49
In where<60 64
In where<60 100
In where<60 81
list3
In orderby 36
In orderby 16
In orderby 25
In orderby 49
list4
list5
16
25
36
49

虽然也能得到正确的结果,但是却是不合理的。因为这样写每步都执行计算,并放到集合中,会有很大的性能损耗,失去了使用LINQ的优势。

何时进行真正计算是个值得思考的问题,多了会增加中间集合的数量,性能不好,少了有可能会有多次重复计算,性能也不好。下文会有说明。

如果使用Resharper插件,会提示出重复迭代(可能会有多次重复计算)的地方,这个功能很好,便于大家分析是否存在问题。

使用Max和Min要小心,Max和Min等聚合运算需要集合中存在值,否则会抛出异常,笔者多次遇到这个问题产生的BUG。

当前面有Where筛选时,后面使用Max或Min不一定是安全的,如下面的代码会抛出异常。

            var a = 0;
            var list = new List<int> { 1, 2, 3 };
            var min = list.Where(i => i < a).Min();
            Console.WriteLine(min);

如果a来源于外部值,又有大段的逻辑,这样的BUG不易发现。

解决方法有多种,我们来分析一下,一种方法是可以先调一下Any,再使用Min,代码如下,

            var a = 0;
            var list = new List<int> { 1, 2, 3 };
            var list2 = list.Where(i => i < a);
            var min = 0;
            if (list2.Any())
            {
                min = list2.Min();
            }
            Console.WriteLine(min);

把代码改为如下,

            var a = 3;
            var list = new List<int> { 1, 2, 3 };
            var list2 = list.Where(i =>
            {
                Console.WriteLine("In where {0}", i);
                return i < a;
            });
            var min = 0;
            if (list2.Any(i =>
            {
                Console.WriteLine("In any {0}", i);
                return true;
            }))
            {
                min = list2.Min();
            }
            Console.WriteLine(min);

打印结果为,

In where 1
In any 1
In where 1
In where 2
In where 3
1

这样做有可能对性能影响不大,也有可能较大,取决于where(或前面的其他逻辑)中逻辑的多少和集合中前面不满足where条件的元素的数量。因为Any确定有就不会继续执行,但仍有部分重复计算发生。

第二种方法的代码如下,

            var a = 3;
            var list = new List<int> { 1, 2, 3 };
            var list2 = list.Where(i => i < a).ToList();
            var min = 0;
            if (list2.Any())
            {
                min = list2.Min();
            }
            Console.WriteLine(min);

这种方法不会有重复计算的开销,但会有数据导入集合的开销,和第一种比较哪种性能更高值得考虑。

第三种方法的代码如下,

            var a = 0;
            var list = new List<int> { 1, 2, 3 };
            var list2 = list.Where(i => i < a);
            var min = 0;
            try
            {
                min = list2.Min();
            }
            catch (Exception)
            {
            }
            Console.WriteLine(min);

直接吃掉异常,数据量大时,前面过滤条件计算复杂时,可能这种方法性能最高。

总之,C#开发者,学好LINQ,用好LINQ,你会发现真的很爽的!

时间: 2024-10-16 06:38:12

Linq研究的相关文章

FCL研究-LINQ-System.Linq Enumerable

.net 里面集合操作极为方便,尤其是实现了IEnumerable接口的集合,一直在使用,系统的研究一下集合的操作也是极好的. 类型 操作符名称 投影操作符 Select,SelectMany 限制操作符 Where 排序操作符 OrderBy,OrderByDescending,ThenBy,ThenByDescending,Reverse 联接操作符 Join,GroupJoin 分组操作符 GroupBy 串联操作符 Concat 聚合操作符 Aggregate,Average,Count

.NET深入解析LINQ框架(五:IQueryable、IQueryProvider接口详解)

阅读目录: 1.环路执行对象模型.碎片化执行模型(假递归式调用) 2.N层对象执行模型(纵横向对比链式扩展方法) 3.LINQ查询表达式和链式查询方法其实都是空壳子 4.详细的对象结构图(对象的执行原理) 5.IQueryable<T>与IQueryProvider一对一的关系能否改成一对多的关系 6.完整的自定义查询 1]. 环路执行对象模型.碎片化执行模型(假递归式调用) 这个主题扯的可能有点远,但是它关系着整个LINQ框架的设计结构,至少在我还没有搞懂LINQ的本意之前,在我脑海里一直频

.NET深入解析LINQ框架(四:IQueryable、IQueryProvider接口详解)

阅读目录: 1.开篇介绍 2.扩展Linq to Object (应用框架具有查询功能) 2.1.通过添加IEnumerable<T>对象的扩展方法 2.2.通过继承IEnumerable<T>接口 2.3.详细的对象结构图 3.实现IQueryable<T> .IQueryProvider接口 3.1.延迟加载IEnumertor<T>对象(提高系统性能) 3.2.扩展方法的扩展对象之奥秘(this IQueryable<TSource> so

.NET深入解析LINQ框架(六:LINQ执行表达式)

阅读目录: 1.LINQ执行表达式 在看本篇文章之前我假设您已经具备我之前分析的一些原理知识,因为这章所要讲的内容是建立在之前的一系列知识点之上的,为了保证您的阅读顺利建议您先阅读本人的LINQ系列文章的前几篇或者您已经具备比较深入的LINQ原理知识体系,防止耽误您的宝贵时间. 到目前为止我们对LINQ的执行原理已经很清楚了,从它的前期构想到它真正为我们所用都有足够的证据,但是似乎问题并没有我们想的那么简单,问题总是在我们使用中频频出现尤其是新技术的使用,当然有问题才能有进步. 一:LINQ执行

从LINQ开始之LINQ to Objects(下)

前言 上一篇<从LINQ开始之LINQ to Objects(上)>主要介绍了LINQ的体系结构.基本语法以及LINQ to Objects中标准查询操作符的使用方法.本篇则主要讨论LINQ to Objects中的扩展方法以及延迟加载等方面的内容. 扩展方法 扩展方法简介 扩展方法能够向现有类型"添加"方法,而无需创建新的派生类型.重新编译或其他方式修改原始类型.扩展方法是静态方法,它是类的一部分,但实际没有放在类的源代码当中.下面,我们来看一个简单示例,为上一篇中定义的

db4o发布7.2,出现.NET 3.5版本,支持LINQ

db4o发布7.2,出现.NET 3.5版本,支持LINQ Db4Object刚刚发布了db4o的7.2beta,除了以前支持如下的平台:.NET 1.1,.NET 2.0,Mono外,现在还支持.NET 3.5了.当然支持.NET 3.5,最主要的时候要来支持LINQ. 关于LINQ,我稍后再讲.现在讲讲7.2中最大的新特性--Transparent Activation(透明激活).关于7.0版本的其他新特性,可以参看我在InfoQ上的文章<Db4Objects发布Db4o 7.0,支持透明

XML读写利器XElement(Linq to xml)

年初公司绩效改革,在等最后通知,不知我有没理解错,感觉新版绩效最高会比原先最高拿到的奖金整整少一半... 还好同时也有调工资,加了一点.去年好像是年中整体调过一次,不知公司是一年调两次还是从今年开始改成年初调. 晚上去加班,处理一个数据交换,本想XML和实体直接互相转换,但XML结构太复杂,自动转换不理想,改用手工处理. 原先其它项目是用原始的XmlDocument,感觉不好用,写法也很不美观 网上说还有种XMLTextReader,像DataReader一样,向前只读的,从来没用过 以前有用过

LINQ中的陷阱--TakeWhile&amp;SkipWhile

在用TakeWhile,SkipWhile设置陷阱之前,我们先来看一看他们的兄弟Take和Skip: public static IEnumerable<T> Take<T>(IEnumerable<T> source, int count) public static IEnumerable<T> Skip<T>(IEnumerable<T> source, int count) 这两个操作符从字面上看就能理解其含义.Take将枚举

.NET深入实战系列—Linq to Sql进阶

最近在写代码的过程中用到了Linq查询,在查找资料的过程中发现网上的资料千奇百怪,于是自己整理了一些关于Linq中容易让人困惑的地方. 本文全部代码基于:UserInfo与Class两个表,其中Class中的UserId与UserInfo中的Id对应 本文唯一访问地址:http://www.cnblogs.com/yubaolee/p/BestLinqQuery.html linq联合查询 内联查询 内联是一个实际使用频率很高的查询,它查询两个表共有的且都不为空的部分 from user in