1. 程式人生 > >C#復習筆記(4)--C#3:革新寫代碼的方式(查詢表達式和LINQ to object(下))

C#復習筆記(4)--C#3:革新寫代碼的方式(查詢表達式和LINQ to object(下))

標識 all 麻煩 linq with write mar sel img

查詢表達式和LINQ to object(下)

接下來我們要研究的大部分都會涉及到透明標識符

let子句和透明標識符

let子句不過是引入了一個新的範圍變量。他的值是基於其他範圍變量的。let 標識符=表達式;

首先展示一個不適用let操作符來使用的按用戶名稱長度來排序:

...
 var queryWithoutLet = from user in SampleData.AllUsers
                                  orderby user.Name.Length
                                  select user;
            
foreach (User user in queryWithoutLet) { Console.WriteLine($"{user.Name}‘s length is {user.Name.Length}"); } ...

可以看得出為了按名稱排序被迫使用了兩次Name.Lengthl來進行查詢。這是相當耗費性能的。所以,需要有一種手段來避免這種冗余的計算方式,這就引出了let操作:它對一個表達式進行求值, 並引入一個新的範圍變量。

...
  var query = from user in SampleData.AllUsers
                        let length 
= user.Name.Length orderby length select new { length = length, Name = user.Name }; foreach (var item in query) { Console.WriteLine($"{item.Name}‘s length is {item.length}"); } ...

上述代碼產生的結果都相同,只不過只計算了一次Length操作。代碼清單引入了一個新的範圍變量:length,它包含了用戶名的長度(針對原始序列中的當前用戶)。我們接著把新的範圍變量用於排序和最後的投影。 你發現問題了嗎? 我們需要使用兩個範圍變量, 但 Lambda表達式只會給Select傳遞一個參數(具體原因在後面)! 這就該透明標識符出場了。

我們在最後的投影中使用了兩個範圍變量, 不過Select方法只對單個序列起作用。 如何把範圍變量合並在一起呢? 答案是,創建一個匿名類型來包含兩個變量,不過需要進行一個巧妙的轉換, 以便看起來就像在select和orderby子句中實際應用了兩個參數。

下圖展示了這個過程:

技術分享圖片

上述代碼清單的執行過程,其中let子句引入了length範圍變量。

下面是轉譯後的代碼:

...
var translatedQuery = SampleData.Users
                .Select(user => new {user, length = user.Name.Length})
                .OrderBy(z => z.length)
                .Select(z => new {Name = z.user.Name, Length = z.length});
...

查詢的每個部分都進行了適當的調整:對於原始的查詢表達式直接引用user或length的地方,如果引用發生在let子句之後, 就用z.user或 z.length來代替。這裏z這個名稱是隨機選擇的——一切都被編譯器隱藏起來。

需要進行說明的是,匿名類型只是一種實現的方式,因為在C#規範上面沒有嚴格規定透明標識符的轉移過程,C#規範只是描述了透明標識符應該以怎樣的形式去表現。C#的現有編譯器是通過匿名類型來實現的,以後不知道會怎樣。

聯結

LINQ中的聯結與Sql上面的聯結的概念相似,只不過LINQ上面的聯結操作的序列。LINQ有三種各類型的聯結,但並不是都是用join關鍵字,首先來看與sql中的內聯結相似的join聯結。

關於聯結,我準備先說一個最重要的結論:聯結的左邊會進行流式傳輸,而右邊會進行緩沖傳輸,所以,在聯結兩個序列時,應該盡可能的將較小的那個序列放到聯結的右側。這個結論很重要,所以我準備在章節中多次提及。

MSDN文檔在描述計算內聯結的jon方法時,將相關的序列稱作inner和outer(可以查看IEnumerable<T>.Join()方法。)。這個只是用來區分兩個序列的叫法而已,不是真的在指內聯結和外聯結。對於IEnumerable<T>.Join()來說,outer是指Join的左邊,inner是指Join的右邊。

首先看一下join的語法:

技術分享圖片

left-key-selector的類型必須要與right-key-selector的類型匹配(能夠進行合理的轉換也是有效的),意義上面來說也要相等,我們不能吧一個人的出生日期和一個城市的人口做關聯。

聯結的符號是”equals“而不是“=”或者“==”。

我們也完全有可能用匿名類型來作為鍵, 因為匿名類型實現了適當的相等性和散列。 如果想創建一個多列的鍵, 就可以使用匿名類型。

實例:

static void Main(string[] args)
        {
            var query = from defect in SampleData.AllDefects
                        join subscription in SampleData.AllSubscriptions
                            on defect.Project equals subscription.Project
                        select new { defect.Summary, subscription.EmailAddress };
            foreach (var item in query)
            {
                Console.WriteLine($"{item.EmailAddress}-{item.Summary}");
            }
            Console.ReadKey();
        }

我們可以將join兩邊的序列進行反轉,結果返回的內容相同,只是順序不同,在linq to object的實現中,返回條目的順序為:先使左邊序列中第1個元素的所有成對數據能被返回(按右邊序列的順序),接著返回使用左邊序列中第2個元素的所有成對數據,依次類推。右邊 序列被緩沖處理,不過左邊序列仍然進行流處理—— 所以,如果你打算把一個巨大的序列聯接到一個極小的序列上, 應盡可能把小序列作為右邊序列。這種操作仍然是延遲的:在訪問第1個數據對時,它才會開始執行,然後再從某個序列中讀取數據。這時,它會讀取整個右邊序列,來建立一個從鍵到生成這些鍵的值的映射。之後,它就不需要再次讀取右邊的序列了, 這時你可以叠代左邊的序列,生成適當的數據對。

下圖展示了上述代碼中用作數據源的兩個不同序列。(SampleData.AllDefects,缺陷和SampleData.AllSubscrption,訂閱)

技術分享圖片

我們通常需要對序列進行過濾,而在聯接前進行過濾比在聯接後過濾效率要高得多。

static void Main(string[] args)
        {
            var query = from defect in SampleData.AllDefects
                        where defect.Status==Status.Closed
                        join subscription in SampleData.AllSubscriptions
                            on defect.Project equals subscription.Project
                        select new { defect.Summary, subscription.EmailAddress };
            foreach (var item in query)
            {
                Console.WriteLine($"{item.EmailAddress}-{item.Summary}");
            }
            Console.ReadKey();
        }

我們也能在join右邊的序列上執行類似的查詢,不過稍微麻煩一些:

static void Main(string[] args)
        {
            var query = from subscription in SampleData.AllSubscriptions
                join defect in (from defect in SampleData.AllDefects
                        where defect.Status == Status.Closed
                        select defect) on
                    subscription.Project equals defect.Project
                select new {subscription.EmailAddress, defect.Summary};
            foreach (var item in query)
            {
                Console.WriteLine($"{item.EmailAddress}-{item.Summary}");
            }
            Console.ReadKey();
        }

說明   內聯接在LINQ to Objects中很有用嗎? SQL總是會使用內聯接。它們實際上是從某個實體導航到相關聯的實體上的一種方式, 通常是把某個表的外鍵和另外一個表的主鍵進行聯接。在面向對象模型中,我們傾向於通過引用來從某個對象導航到另外一個對象。 例如, 在SQL中, 要得到缺陷的概要和處理這個缺陷的用戶名稱(這裏的名詞都是針對書面代碼的對象的屬性),需要進行聯接—— 在C#中,我們則使用屬性鏈。如果在我們的模型中存在一個反向關聯,從Project對象到與之關聯的NotificationSubscription對象列表,我們 不必使用聯接也可以實現這個例子的目標。 這並不是說,在面向對象模型裏面,內聯沒有用——只是沒有在關系模型中出現得那麽頻繁而已。

內聯被編譯器轉譯後的結果如下:

技術分享圖片

用於LINQ to object的重載簽名如下:

技術分享圖片

由於剛才已經對inner和outer的含義做了說明,此處就略去了。

當聯接的後面不是select子句時,C#3編譯器就會引入透明標識符,這樣,用於兩個序列的範圍變量就能用於後面的子句,並且創建了一個匿名類型,簡化了對resultSelector參數使用的映射。然而,如果查詢表達式的下一 部分是select子句,那麽select子句的投影就直接 作為resultSelector參數—— 當你可以一步完成這些轉換的時候,創建元素對,然後調用Select是沒有意義的。你仍然可以把它看做是“select” 步驟所跟隨的“join” 步驟, 盡管兩者都被壓縮到了一個單獨的方法調用中。在我看來,這樣在思維模式上更能保持一致,而且這種 思維模式也容易理解。除非你打算研究生成的代碼,不然可以忽略編譯器為你完成的這些優化。令人高興的是,在學懂了內聯接的相關知識後,下一種聯接類型就很容易理解了。

C#復習筆記(4)--C#3:革新寫代碼的方式(查詢表達式和LINQ to object(下))