C# Notizen 12 查詢表達式

應用程序還需要操作存儲在其他數據源(如SQL數據庫或XML文件)中的數據,甚至通過Web服務訪問它們。傳統上,查詢這些數據源時需要使用不同的語法,且在編譯期間不進行類型檢查。

一、LINQ
在.NET Framework中,查詢表達式是一組統稱為語言集成查詢(LINQ)的技術的一部分,將查詢功能直接集成到了C#語言中。LINQ是.NET Framework 3.5新增的,它提供了適用于所有數據源(SQL數據庫、XML文檔、Web服務、ADO.NET數據庫以及任何支持接口IEnumberable或IEnumerable<T>的集合)的查詢語言,從而避免了操作數據和對象時經常出現的語言不匹配問題。
LINQ讓查詢變成了基本語言構造,就像算術運算和流程控制語句是C#基本概念一樣。LINQ 查詢將重點放在常用的操作而不是數據結構上,能夠以一致的方式從任何支持的數據源檢索數據,并對其進行轉換。
與SQL(Structured Query Language,結構化查詢語言)查詢相比,LINQ查詢的語法相同,使用的一些關鍵字相同,提供的很多優點也相同。您可隨便修改被查詢的底層數據的結構,而不會導致需要修改查詢。SQL只能用于操作關系型數據,而LINQ支持的數據結構要多得多。

如下代碼是一個用于Contact對象集合的查詢

class Contact
{    
    public int Id { get; set; }    
    public string Company { get; set; }    
    public string LastName { get; set; }    
    public string FirstName { get; set; }    
    public string Address { get; set; }    
    public string City { get; set; }    
    public string StateProvince { get; set; }
}
IEnumerable<Contact> contacts = GetContacts();
var result = from contact in contacts select contact.FirstName;

foreach(var name in result)
{    
    Console.WriteLine(name);
}

這個簡單的查詢演示了 C#語言支持的聲明性語法(declartive syntax,也叫 query comprehension syntax)。這種語法讓您能夠使用類似于SQL查詢的語法編寫查詢,靈活性和表達能力都極強。雖然查詢表達式中所有的變量都是強類型的,但是在大多數情況下,不需要顯式地指定類型,因為編譯器能夠推斷出來。

ps:LINQ查詢語法

如果您熟悉SQL,就不會對LINQ使用的查詢語法感到陌生。最明顯的差別是,from運算符位于select運算符的前面,而不像SQL中那樣位于后面。

1.1 選擇數據

雖然上述所示的代碼看起來可能很簡單,但是實際上涉及的內容很多。首先應注意到的是,使用了一個類型將隱式確定的變量result,其類型實際上是IEnumerable<string>。查詢表達式(賦值運算符右邊的代碼)的結果為查詢,而不是查詢的結果。select 子句返回一個對象,該對象表示對一個序列( contacts 列表)執行投影操作的結果(一系列contact.FirstName值)。由于結果是一系列字符串,因此result必然是由字符串組成的可枚舉集合。當前,并不會檢索數據,而只是返回一個可枚舉集合,以后再將數據取回。

這個查詢相當于說,從contacts指定的數據源中,選擇每個元素(contact)的FirstName字段。from子句中指定的變量contact類似于foreach語句中的迭代變量,它是一個只讀局部變量,作用域為查詢表達式。in子句指定了要從中查詢元素的數據源,而select子句指定在迭代期間只選擇每個元素的contact.FirstName字段。

選擇單個字段時,這種語法的效果很好,但通常需要選擇多個字段,甚至以某種方式對數據進行變換,如合并字段。所幸的是,LINQ通過類似的語法提供了這樣的支持。實際上,有多種方法執行這些類型的選擇。

第一種方法是在select子句中拼接這些字段,這樣將只返回一個字段,如下代碼所示:

var result = from contact in contacts select contact.FirstName + " " + contact.LastName;

foreach(var name in result)
{    
    Console.WriteLine(name);
}

顯然,這種選擇方式只適用于有限的情形。一種更靈活的方法是返回多個字段,即返回一個數據子集,如下所示:

var result = from contact in contacts             
                      select new             
                      {                 
                          Name = contact.LastName + ", " + contact.FirstName;                 
                          DateOfBirth = contact.DateOfBirth             
                      };

foreach(var contact in result)
{    
    Console.WriteLine("{0} born on {1}", contact.Name, contact.DateOfBirth);
}

這里返回的仍是IEnumberable,但其類型是什么呢?如果查看程序中的select子句,就會發現它返回了一種新類型,其中包含字段contact.FirstName和contact.LastName的值。這實際上是一個匿名類型(anonymous type),它包含屬性Name和DateOfBirth。這種類型之所以是匿名的,是因為它沒有名稱。無需顯式地聲明與返回值對應的新類型,編譯器會自動生成。

ps:匿名類型
以這種方式創建匿名類型是LINQ工作方式的核心,如果沒有var提供的引用類型,這根本不可能。

1.2 篩選數據
選擇數據很重要,但以這種方式選擇數據時,無法指定要返回哪些數據。SQL 提供了where子句,同樣,LINQ也提供了where子句,它返回一個可枚舉的集合,其中包含符合指定條件的元素。如下代碼再上一個示例的查詢中添加了一個where子句,將結果限定為StateProvince字段為FL的聯系人。

var result = from contact in contacts             
                        whrere contact.StateProvince == "FL"             
                        select new {customer.FirstName, customer.LastName};

foreach(var name in result)
{    
    Console.WriteLine(name.FirstName + " " + name.LastName);
}

首先執行where子句,再對得到的可枚舉集合執行select子句,其結果為一個包含屬性FirstName和LastName的匿名類型。

1.3 對數據進行分組和排序
為支持更復雜的情形,如對返回的數據進行排序或分組,LINQ 提供了 orderby 子句和group 子句。可將數據按升序(從最小到最大)或降序(從最大到最小)排列,由于升序是默認設置,因此不需要指定升序。如下程序將結果按字段LastName排序。

var result = from contact in contacts             
                        oderby contact.LastName             
                        select contact.FirstName;

foreach(var name in result)
{     
    Console.WriteLine(name);
}

可根據多個字段進行排序,并混合使用升序和降序,這將創建出非常復雜的 orderby 語句,如下代碼所示:

var result = from contact in contacts             
                    oderby                 
                        contact.LastName ascending,                
                        contact.FirstName descending             
                    select customer.FirstName;

foreach(var name in result)
{    
    Console.WriteLine(name);
}

將數據分組的方法與此類似,但將使用group子句替換select子句。對數據分組時的不同之處在于,返回的結果為由 IGrouping<TKey, TElement>對象組成的 IEnumerable,可將其視為由列表組成的列表。這要求使用兩條嵌套的foreach語句來訪問結果。
如下代碼是一個使用group子句的LINQ查詢

var result = from contact in contacts             
                   group contact by contact.LastName[0];

foreach(var group in result){    
    Console.WriteLine("Last names starting width {0}", group.key);    
    foreach(var name in result)    
    {        
        Console.WriteLine(name);    
    }    
    Console.WriteLine();
}

如果需要引用分組操作的結果,就可創建一個標識符,使用關鍵字into將查詢結果存儲到該標識符中,并對其做進一步查詢。這種組合方式稱為查詢延續(continuation)。
如下代碼演示了一個使用group和into的LINQ查詢:

var result = from contact in contacts             
                    group contact by contact.LastName[0] into namesGroup             
                    where namesGroup.Count() > 2             
                    select namesGroup;

foreach(var group in result)
{    
    Console.WriteLine("Last names starting width {0}", group.key);    
        foreach(var name in result)    
        {        
            Console.WriteLine(name);    
        }    
    Console.WriteLine();
}

1.4 聯接數據

LINQ 還能夠合并多個數據源,這是通過利用一個或多個都有的字段將它們聯接起來實現的。查詢多個沒有直接關系的數據源時,聯接數據很重要。SQL支持使用很多運算符進行聯接,但LINQ聯接基于相等性。
前面的示例只使用了Contact類,要執行聯接操作,至少需要兩個類。程序清單12.9再次列出了Contact類,還列出了新增的JournalEntry類。繼續假設通過調用GetContacts填充了contacts列表,并通過調用GetJournalEntries填充了journal列表。
如下代碼演示了Contact和JournalEntry類

class Contact
{    
    public int Id { get; set; }    
    public string Company { get; set; }    
    public string LastName { get; set; }    
    public string FirstName { get; set; }    
    public string Address { get; set; }    
    public string City { get; set; }    
    public string StateProvince { get; set; }
}

class JournalEntry
{    
    public int Id { get; set; }    
    public int ContactId { get; set; }    
    public string Description { get; set; }    
    public string EntryType { get; set; }    
    public DateTime Date { get; set; }
}

IEnumberable<Contact> contacts = GetContacts();
IEnumberable<JournalEntry> journal = GetJournalEntries();

在LINQ中,最簡單的聯接查詢與SQL內聯接等效,這種查詢使用join子句。SQL聯接可使用很多不同的運算符,而LINQ聯接只能使用相等運算符,稱之為相等聯接(equijoin)。
如下示例的查詢使用Contact.ID和JournalEntry.ContactId作為聯接鍵,將一個由Contact對象組成的列表和一個由JournalEntry對象組成的列表聯接起來:

var result =    
        from contact in contacts    
        join journalEntry in journal    
        on contact.Id equals journalEntry.ContactId    
        select new    
        {        
            contact.FirstName,        
            contact.LastName,        
            journalEntry.Date,        
            journalEntry.EntryType,        
            journalEntry.Description    
        };

如上所示的 join子句創建了一個名為 journalEntry的范圍變量(range ariable),其類型為JournalEntry;然后使用equals運算符將兩個數據源聯接起來。
LINQ還支持分組聯接概念,而SQL沒有與之對應的查詢。分組聯接使用關鍵字into,其結果為層次結構。就像使用group子句時那樣,需要使用嵌套foreach語句來訪問結果。
ps:順序很重要
使用LINQ聯接時,順序很重要。被聯接的數據源必須位于equals運算符左邊,而聯接數據源必須位于右邊。在這個示例中,contacts是被聯接的數據源,而journal是聯接數據源。
所幸的是,順序不正確時,編譯器能夠捕獲并生成編譯錯誤。如果交換join子句中的參數,將出現下面的編譯錯誤:
名稱“Journalentry”不在“equals”左側的范圍內。請考慮交換“equals”兩側的表達式。
需要注意的另一個重點是,join子句使用運算符equals,它與相等運算符(==)不完全相同。
如下代碼的查詢聯接contacts和journal,并將結果按聯系人姓名分組。在返回的結果集中,每個元素都包含一個由 JournalEntry 組成的可枚舉集合,該集合由返回的匿名類型的JournalEntries屬性表示。

var result =    
        from contact in contacts    
        join journalEntry in journal    
        on contact.Id equals journalEntry.ContactId              
        into journalGroups    
        select new    
        {        
            Name = contact.LastName + "," +contact.FirstName,        
            JournalEntries = journalGroups    
        };

1.5 數據平坦化
雖然選擇和聯接數據時,返回的數據是合適的,但層次型數據使用起來比較繁瑣。LINQ能夠創建返回平坦化數據的查詢,就像查詢SQL數據源一樣。
假設對Contact和JournalEntry類進行了修改:在Contact類中添加了一個Journal字段,其類型為List<JournalEntries>,并刪除了JournalEntry類的屬性ContactId,如下所示

class Contact
{    
    public int Id { get; set; }    
    public string Company { get; set; }    
    public string LastName { get; set; }    
    public string FirstName { get; set; }    
    public string Address { get; set; }    
    public string City { get; set; }    
    public string StateProvince { get; set; }    
    public List<JournalEntries> Journal;
}

class JournalEntry
{    
    public int Id { get; set; }    
    public string Description { get; set; }    
    public string EntryType { get; set; }    
    public DateTime Date { get; set; }
}

IEnumerable<Contact> contacts = GetContacts();

在這種情況下,可使用查詢檢索特定聯系人的JournalEntry列表,如下所示:

var result =    
        from contact in contacts    
        where contact.id == 1    
        select contact.Journal;

foreach(var item in result)
{    
    foreach(var journalEntry in item)    
    {        
        Console.WriteLine(journalEntry);    
    }
}

雖然這樣可行,也返回了所需的結果,但是仍需使用嵌套 foreach 語句來訪問結果。所幸的是,LINQ 支持從多個數據源選擇數據,從而提供了一種返回平坦化數據的查詢語法。如下程序演示了這種查詢語法,它使用多個from子句,使得訪問數據時只需一條foreach語句。

var result =    
        from contact in contacts    
        from journalEntry in contact.Journal    
        where contact.id == 1    
        select contact.Journal;

foreach(var item in result)
{    
    Console.WriteLine(journalEntry);
}

二、標準查詢運算符方法

前面介紹的所有查詢都使用聲明性查詢語法,但也可使用標準查詢運算符方法來編寫這些查詢。標準查詢運算符方法實際上是命名空間System.Linq中定義的Enumerable類的擴展方法。對于使用聲明性語法的查詢表達式,編譯器將其轉換為等價的查詢運算符方法調用。
使用using語句包含命名空間System.Linq后,對于任何實現了接口IEnumberable<T>的類,智能感知列表都可包含標準查詢運算符方法。
雖然聲明性查詢語法幾乎支持所有的查詢操作,但是也有一些操作(如Count和Max)沒有對應的查詢語法,必須使用方法調用來表示。由于每個方法調用都返回IEnumerable,因此通過串接方法調用,可編寫出復雜的查詢。編譯聲明性查詢表達式時,編譯器就是這樣做的。

ps:使用聲明性語法還是方法語法
使用聲明性語法還是方法語法因人而異,這取決于個人認為哪種語法更容易理解。無論使用哪種語法,執行查詢得到的結果都相同。

如下示例演示了使用方法語法的LINQ查詢

var result = contacts.     
        Where(contact => contact.StateProvince == "FL").     
        Select(contact => new { contact.FirstName, contact.LastName });

foreach(var name in result)
{    
    Console.WriteLine(name.FirstName + " " + name.LastName);
}

三、Lambda

上述示例中,傳遞給方法Where和Select的參數看起來與以前使用過的參數不同。這些參數實際上包含的是代碼,而不是數據。之前介紹過委托和匿名方法,委托能夠將一個方法作為參數傳遞給另一個方法,而匿名方法能夠編寫未命名的內聯語句塊,這些語句塊將在調用委托時執行。
Lambda 結合使用了這兩個概念,它是可包含表達式和語句的匿名函數。通過使用Lambda,可以更方便、更簡潔的方式編寫這樣的代碼,即正常情況下需要使用匿名方法或泛型委托進行編寫。
ps:Lambda和委托
由于Lambda是編寫委托的更簡潔方式,因此可在通常需要使用委托的任何地方使用它們。所以,Lambda的形參類型必須與相應的委托類型完全相同,返回類型也必須隱式地轉換為委托的返回類型。
雖然 Lambda 沒有類型,但是它們可隱式地轉換為任何兼容的委托類型。正是這種隱式轉換讓您無需顯式賦值就能夠傳遞它們。
在C#中,Lambda使用Lambda運算符(=>)。在方法調用中,該運算符左邊指定了形參列表,而該運算符右邊為方法體。匿名方法的所有限制也適用于Lambda。

在上述示例中,實參 contact => contact.StateProvince == "FL"的意思為,這是一個以 contact為參數的函數,其返回值為表達式 contact.StateProvince == "FL"的結果。
ps:捕獲的變量和定義的變量
Lambda還能捕獲變量,這可以是Lambda所屬方法的局部變量或參數。這使得可在Lambda體內通過名稱訪問捕獲的變量。如果捕獲是局部變量,那么必須賦值后才能在Lambda中使用它。ref或out參數無法捕獲。
然而,需要注意的是,對于Lambda捕獲的變量,在引用它的委托超出作用域前,垃圾收集器將不會收集它們。
Lambda中聲明的變量在Lambda所屬方法內不可見,輸入參數也如此,因此可在多個Lambda中使用同一個標識符。

表達式Lambda
在Lambda中,如果運算符右邊為表達式,該Lambda就為表達式Lambda,它返回該表達式的結果。表達式Lambda的基本格式如下:

(input parameters) => expressions

如果只有一個輸入參數,那么括號是可選擇的;否則(包括沒有參數時),括號將是必不可少的。
就像泛型方法可推斷其類型參數的類型一樣,Lambda 也能推斷其輸入參數的類型。如果編譯器無法推斷出類型,您就可以顯式地指定類型。

如果將表達式 Lambda 的表達式部分視為方法體,那么表達式 Lambda 包含一條隱式的return語句,它返回表達式的結果。
ps:包含方法調用的表達式Lambda
大部分示例都在右邊使用了方法,但是如果創建的Lambda將用于其他域,如SQL Server,就不應使用方法調用,因為它們在.NET Framework公共語言運行時外面沒有意義。

語句Lambda
在 Lambda 的右邊,可使用一條或多條用大括號括起的語句,這種 Lambda 稱為語句Lambda。語句Lambda的基本形式如下:
(input parameters) => { statement; }
與表達式 Lambda 一樣,如果只有一個輸入參數,那么括號是可選的;否則,括號就必不可少。語句Lambda也遵循同樣的類型推斷規則。
雖然表達式Lambda包含一條隱式的return語句,但是語句Lambda沒有,您必須在語句Lambda中顯式地指定return語句。return語句只導致從Lambda表示的隱式方法返回,而不會導致從Lambda所屬的方法返回。
語句Lambda不能包含這樣的goto、break和continue語句,即其跳轉目標在Lambda外。同樣,作用域規則禁止從嵌套Lambda分支到外部Lambda。
預定義的委托
雖然 Lambda 是 LINQ 的有機組成部分,但是可將其用于任何可使用委托的地方。因此,.NET Framework提供了很多預定義的委托,可將其作為方法參數進行傳遞,而無需首先聲明顯式的委托類型。
由于返回Boolean值的委托很常見,因此.NET Framework定義了一個Predicate<in T>委托,Array和List<T>類的很多方法都使用它。
Predicate<T>定義了一個總是返回Boolean值的委托,而Func系列委托封裝了有指定返回值,且接受0~16個輸入參數的方法。
Predicate<T>和 Func 系列委托都有返回值,但是 Action 系列委托表示返回類型為 void的方法。就像Func系列委托一樣,Action系列委托也接受0~16個輸入參數。
四、延遲執行
不同于眾多傳統的數據查詢技術,LINQ 查詢要等到實際迭代其結果時才執行,稱之為延遲執行(lazy evaluation)。其優點之一是,在指定查詢和檢索查詢指定的數據之間,可修改原始集合中的數據。這意味著您獲得的數據總是最新的。
雖然LINQ首選延遲執行,但是使用了任何聚合函數的查詢都必須先迭代所有元素。這些函數(如Count、Max、Average和First)都返回一個值,并且無需使用顯式foreach語句就能執行。
ps:延遲執行和串接查詢
延遲執行的另一個優點是,讓您能夠串接查詢,從而提供編碼效率。由于查詢對象表示的是查詢,而不是查詢的結果,因此可輕松地串接或重用它們,而不會導致開銷高昂的數據取回操作。
也可強制查詢立刻執行,這有時稱為貪婪執行(greedy evaluation)。為此,可在查詢表達式后面,緊接著放置一條foreach語句,也可調用方法ToList 或ToArray。方法ToList 和ToArray還可用于將數據緩存到一個集合對象中。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容