.NET面试题系列[13] - LINQ to Object

.NET面试题系列目录

名言警句

"C# 3.0全部特性的提出都是更好地为LINQ服务的" - Learning Hardhtml

LINQ是Language Integrated Query(语言集成查询)的缩写,读音和单词link相同。不要读成“lin-Q”。面试

LINQ to Object将查询语句转换为委托。LINQ to Entity将查询语句转换为表达式树,而后再转换为SQL。sql

LINQ的好处:强类型,相比SQL语句它更面向对象,对于全部的数据库给出了统一的操做方式。shell

LINQ的一些问题:要时刻关注转换的SQL来保持性能,另外,某些操做不能转换为SQL语句,以及很难替代存储过程。数据库

在面试时,大部分面试官都不会让你手写LINQ查询,至少就我来讲,写不写得出LINQ的Join并没所谓,反正查了书确定能够写得出来。但面试官会对你是否理解了LINQ的原理很感兴趣。实际上自有了委托起,LINQ就等于出现了,后面的特性均可以当作是语法糖。若是你能够不用LINQ而用原始的委托实现一个相似LINQ中的where,select的功能,那么你对LINQ to Object应该理解的不错了。数组

Enumerable是什么?

Enumerable是一个静态类型,其中包含了许多方法,绝大部分都是扩展方法(它也有本身的方法例如Range),返回IEnumerable (由于IEnumerable是延迟加载的,每次访问的时候才取值),并且绝大部分扩展的是IEnumerable<T>。缓存

Enumerable是一个静态类型,不能建立Enumerable类型的实例。性能优化

Enumerable是LINQ to Object的基础。由于LINQ to Object绝大多数时候都是和IEnumerable<T>以及它的派生类打交道,扩展了IEnumerable<T>的Enumerable类,赋予IEnumerable<T>强大的查询能力。函数

序列 (Sequence)

序列就像数据项的传送带,你每次只能获取一个,直到你不想获取或者序列没有数据为止。序列多是无限的(例如你能够写一个随机数的无限序列),当你从序列读取数据的时候,一般不知道还有多少数据项等待读取。工具

LINQ的查询就是得到序列,而后一般在中间过程会转换为其余序列,或者和额外的序列链接在一块儿。

延迟执行 (Lazy Loading)

大部分LINQ语句是在最终结果的第一个元素被访问的时候(即在foreach中调用MoveNext方法)才真正开始运算的,这个特色称为延迟执行。通常来讲,返回另一个序列(一般为IEnumerable<T>或IQueryable<T>)的操做,使用延迟执行,而返回单一值的运算,使用当即执行。

例以下面的例子:实际上,当这两行代码运行完时,ToUpper根本没有运行过。

或者下面更极端的例子,虽然语句不少,但其实在你打算遍历结果以前,这一段语句根本不会占用任什么时候间:

那么若是咱们这样写,会不会有任何东西打印出来呢?

 

答案是不会。问题的关键是,IEnumerable<T>是延迟执行的,当没有触发执行时,就不会进行任何运算。Select方法不会触发LINQ的执行。一些触发的方式是:

  • foreach循环
  • ToList,ToArray,ToDictionary方法等

例以下面的代码:

 

它的输出是:

注意全部名字都打印出来了,而所有大写的名字,只会打印长度大于3的。为何会交替打印?这是由于在开始foreach枚举时,uppercase的成员还没肯定,咱们在每次foreach枚举时,都先运行select,打印原名,而后筛选,若是长度大于3,才在foreach中打印,因此结果是大写和原名交替的。

利用ToList强制执行LINQ语句

下面的代码和上面的区别在于咱们增长了一个ToList方法。思考会输出什么?

 

ToList方法强制执行了全部LINQ语句。因此uppercase在Foreach循环以前就肯定了。其将仅仅包含三个成员:Lily,Joel和Annie(都是大写的)。故将先打印5个名字,再打印uppercase中的三个成员,打印的结果是:

 

LINQPad

LINQPad工具是一个很好的LINQ查询可视化工具。它由Threading in C#和C# in a Nutshell的做者Albahari编写,彻底免费。它的下载地址是http://www.linqpad.net/

进入界面后,LINQPad能够链接到已经存在的数据库(不过就仅限微软的SQL Server系,若是要链接到其余类型的数据库则须要安装插件)。某种程度上能够代替SQL Management Studio,是使用SQL Management Studio做为数据库管理软件的码农的强力工具,能够用于调试和性能优化(经过改善编译后的SQL规模)。

 

你可使用Northwind演示数据库进行LINQ的学习。Northwind演示数据库的下载地址是https://www.microsoft.com/en-us/download/details.aspx?id=23654。链接到数据库以后,LINQPad支持使用SQL或C#语句(点标记或查询表达式)进行查询。你也能够经过点击橙色圈内的各类不一样格式,看到查询表达式的各类不一样表达方式:

  • Lambda:查询表达式的Lambda表达式版本
  • SQL:由编译器转化成的SQL,一般这是咱们最关心的部分
  • IL:IL语言

 

查询操做

假设咱们有一个类productinfo,并在主线程中创建了一个数组,其含有若干productinfo的成员。咱们在写查询以前,将传入对象Product,其类型为productinfo[]。

基本的选择语法

得到product中,全部的产品的全部信息(注意p是一个别名,能够随意命名):

From p in products

select p

SQL: select * from products

 

得到product中,全部的产品名称:

From p in products

select p.name

SQL: select name from products

 

Where子句

得到product中,全部的产品的全部信息,但必须numberofstock属性大于25:

From p in products

where p. numberofstock > 25

select p

SQL: select * from products where numberofstock > 25

Where子句中可使用任何合法的C#操做符,&&,||等,这等同于sql的and和or。

注意最后的select p实际上是没有意义的,能够去掉。若是select子句什么都不作,只是返回同给定的序列相同的序列,则编译器将会删除之。编译器将会把这个LINQ语句转译为product.Where(p => p. numberofstock > 25)。注意后面没有Select跟着了。

但若是将最后的select子句改成select p.Name,则编译器将会把这个LINQ语句转译为product.Where(p => p. numberofstock > 25).Select(p => p.Name)。

Orderby子句

得到product中,全部的产品名称,并正序(默认)排列:

From p in products

order by p.name

select p.name

SQL: select name from products order by name

ThenBy子句必须永远跟在Orderby以后。

Let子句

假设有一个以下的查询:

            var query = from car in myCarsEnum
                orderby car.PetName.Length
                        select car.PetName;

            foreach (var name in query)
            {
                Console.WriteLine("{0}: {1}", name.Length, name);
            }

咱们发现,对name.Length引用了两次。咱们是否能够引入一个临时变量呢?上面的查询将会被编译器改写为:

myCarsEnum.OrderBy(c => c.PetName.Length).Select(c => c.PetName)。

咱们可使用let子句引入一个临时变量:

            var query = from car in myCarsEnum
                let length = car.PetName.Length
                orderby length
                select new {Name = car.PetName, Length = length};

            foreach (var name in query)
            {
                Console.WriteLine("{0}: {1}", name.Length, name.Name);
            }

上面的查询将会被编译器改写为:

myCarsEnum

.Select(car => new {car, length = car.Length})

.OrderBy(c => c.Length)

.Select(c => new { Name = c.PetName, Length = c.Length})。

能够经过LINQPad得到编译器的改写结果。

在此处,咱们能够看到匿名类型在LINQ中发挥了做用。select new {Name = car.PetName, Length = length} (匿名类型)使咱们不费吹灰之力就获得了一个新的类型。

链接

考察下面两个表格:

表Defect:

表NotificationSubscription:

咱们发现这两个表都存在一个外码ProjectID。故咱们能够试着进行链接,看看会发生什么。

使用join子句的内链接

在进行内链接时,必需要指明基于哪一个列。若是咱们基于ProjectID进行内链接的话,能够预见的是,对于表Defect的ProjectID列,仅有1和2出现过,因此NotificationSubscription的第一和第四行将会在结果集中,而其余两行不在。

查询:

            from defect in Defects 
            join subscription in NotificationSubscriptions
                 on defect.ProjectID equals subscription.ProjectID
            select new { defect.Summary, subscription.EmailAddress }

若是咱们调转Join子句先后的表,结果的记录数将相同,仅是顺序不一样。LINQ将会对链接延迟执行。Join右边的序列被缓存起来,左边的则进行流处理:当开始执行时,LINQ会读取整个右边序列,而后就不须要再读取右边序列了,这时就开始迭代左边的序列。因此若是要链接一个巨大的表和一个极小的表时,请尽可能将小表放在右边。

编译器的转译为:

Defects.Join (
      NotificationSubscriptions, 
      defect => defect.ProjectID, 
      subscription => subscription.ProjectID, 
      (defect, subscription) => 
         new  
         {
            Summary = defect.Summary, 
            EmailAddress = subscription.EmailAddress
         }
   )

使用join into子句进行分组链接

查询:

from defect in Defects
join subscription in NotificationSubscriptions
on defect.Project equals subscription.Project
into groupedSubscriptions
select new { Defect=defect, Subscriptions=groupedSubscriptions }

其结果将会是:

内链接和分组链接的一个重要区别是:分组链接的结果数必定和左边的表的记录数相同(例如本例中左边的表Defects有41笔记录,则分组链接的结果数必定是41),即便某些左边表内的记录在右边没有对应记录也无所谓。这相似SQL的左外链接。与内链接同样,分组链接缓存右边的序列,而对左边的序列进行流处理。

编译器的转译为简单的调用GroupJoin方法:

Defects.GroupJoin (
      NotificationSubscriptions, 
      defect => defect.Project, 
      subscription => subscription.Project, 
      (defect, groupedSubscriptions) => 
         new  
         {
            Defect = defect, 
            Subscriptions = groupedSubscriptions
         }
   )

使用多个from子句进行叉乘

查询:

from user in DefectUsers
from project in Projects
select new { User = user, Project = project }

在DefectUsers表中有6笔记录,在Projects表中有3笔记录,则结果将会是18笔:

 

编译器将会将其转译为方法SelectMany:

DefectUsers.SelectMany (
      user => Projects, 
      (user, project) => 
         new  
         {
            User = user, 
            Project = project
         }
   )

即便涉及两个表,SelectMany的作法彻底是流式的:一次只会处理每一个序列中的一个元素(在上面的例子中就是处理18次)。SelectMany不须要将右边的序列缓存,因此不会一次性向内存加载不少的内容。 

在查询表达式和点标记之间作出选择

不少人爱用点标记,点标记这里指的是用普通的C#调用LINQ查询操做符来代替查询表达式。点标记并不是官方名称。对这两种写法的优劣有不少说法:

  • 每一个查询表达式均可以被转换为点标记的形式,而反过来则不必定。不少LINQ操做符不存在等价的查询表达式,例如Reverse,Sort等等。
  • 既然点标记是查询表达式编译以后的形式,使用点标记能够省去编译的一步。
  • 点标记比查询表达式具备更高的可读性(并不是对全部人来讲,见仁见智)
  • 点标记体现了面向对象的性质,而在C#中插入一段SQL让人以为不三不四(见仁见智)
  • 点标记能够轻易的接续
  • Join时查询表达式更简单,看上去更像SQL,而点标记的Join很是难以理解

C# 3.0全部的特性的提出都是更好地为LINQ服务的

下面举例来使用普通的委托方式来实现一个where(o => o > 5):

public delegate bool PredicateDelegate(int i);

        public static void Main(string[] args)
        {
            var seq = Enumerable.Range(0, 9);

            var seqWhere = new List<int>();
            PredicateDelegate pd = new PredicateDelegate(Predicate);
            foreach (var i in seq)
            {
                if (pd(i))
                {
                    seqWhere.Add(i);
                }
            }
        }

        //The target predicate delegate
        public static bool Predicate(int input)
        {
            return input > 5;
        }

因为where是一个判断,它返回一个布尔值,因此咱们须要一个形如Func<int, bool>的委托,故咱们能够构造一个方法,它接受一个int,返回一个bool,在其中实现筛选的判断。最后,对整个数列进行迭代,并一一进行判断得到结果。若是使用LINQ,则整个过程将会简化为只剩一句话。

C# 2.0中匿名函数的提出使得咱们能够把Predicate方法内联进去。若是没有匿名函数,每个查询你都要写一个委托目标方法。

        public delegate bool PredicateDelegate(int i);

        public static void Main(string[] args)
        {
            var seq = Enumerable.Range(0, 9);

            var seqWhere = new List<int>();
            PredicateDelegate pd = delegate(int input)
            {
                return input > 5;
            };
            foreach (var i in seq)
            {
                if (pd(i))
                {
                    seqWhere.Add(i);
                }
            }
        }

C#是在Where方法中进行迭代的,因此咱们看不到foreach。因为Where是Enumerable的扩展方法,因此能够对seq对象使用Where方法。

有时候咱们须要从数据库中选择几列做为结果,此时匿名类型的存在使得咱们不须要为了这几列去辛辛苦苦的创建一个新的类型(除非它们常常被用到,此时你可能就须要一个ViewModel层)。隐式类型的存在使得咱们不须要思考经过查询语句得到的类型是何种类型(大部分时候,咱们也不关心它的类型),只须要简单的使用var就能够了。

var seq = Enumerable.Range(0, 9);
            var seq2 = seq.Select(o => new
            {
                a = o,
                b = o + 1
            });
相关文章
相关标签/搜索