.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
            });
相關文章
相關標籤/搜索