.NET面試題系列目錄
名言警句
"C# 3.0所有特性的提出都是更好地為LINQ服務的" - Learning Hard
LINQ是Language Integrated Query(語言集成查詢)的縮寫,讀音和單詞link相同。不要讀成“lin-Q”。
LINQ to Object將查詢語句轉換為委托。LINQ to Entity將查詢語句轉換為表達式樹,然後再轉換為SQL。
LINQ的好處:強類型,相比SQL語句它更面向對象,對於所有的數據庫給出了統一的操作方式。
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 });
Tags: 名言警句 Object 面試官 數據庫 表達式
文章來源: