.NET中那些所謂的新語法之四:標准查詢運算符與LINQ


開篇:在上一篇中,我們了解了預定義委托與Lambda表達式等所謂的新語法,這一篇我們繼續征程,看看標准查詢運算符和LINQ。標准查詢運算符是定義在System.Linq.Enumerable類中的50多個為IEnumerable<T>准備的擴展方法,而LINQ則是一種類似於SQL風格的查詢表達式,它們可以大大方便我們的日常開發工作。因此,需要我們予以關注起來!

/* 新語法索引 */

9.標准查詢運算符 Standard Query Operator
10.LINQ查詢表達式

一、擴展方法哪家強?標准查詢運算符:[ C# 3.0/.NET 3.x 新增特性 ]

  標准查詢運算符提供了包括篩選、投影、聚合、排序等功能在內的查詢功能,其本質是定義在System.Linq.Enumerable類中的50多個為IEnumerable<T>准備的擴展方法

  從上圖可以看出,在Enumerable類中提供了很多的擴展方法,這里我們選擇其中幾個最常用的方法來作一點介紹,使我們能更好地利用它們。首先,我們需要一點數據來進行演示:

    public class Person
    {
        public int ID { get; set; }

        public string Name { get; set; }

        public int Age { get; set; }

        public bool Gender { get; set; }

        public override string ToString()
        {
            return string.Format("{0}-{1}-{2}-{3}", ID, Name, Age, 
                Gender == true ? "" : "");
        }
    }

    public class LitePerson
    {
        public string Name { get; set; }

        public override string ToString()
        {
            return Name;
        }
    }

    public class Children
    {
        public int ChildID { get; set; }
        public int ParentID { get; set; }
        public string ChildName { get; set; }

        public override string ToString()
        {
            return string.Format("{0}-{1}-{2}", ChildID, ChildName, ParentID);
        }
    }

        static List<Person> GetPersonList()
        {
            List<Person> personList = new List<Person>()
            {
                new Person(){ID=1,Name="Edison Chou",Age=25,Gender=true},
                new Person(){ID=2,Name="Edwin Chan",Age=20,Gender=true},
                new Person(){ID=3,Name="Jackie Chan",Age=40,Gender=true},
                new Person(){ID=4,Name="Andy Lau",Age=55,Gender=true},
                new Person(){ID=5,Name="Kelly Chan",Age=45,Gender=false}
            };
            return personList;
        }

        static List<Children> GetChildrenList()
        {
            List<Children> childrenList = new List<Children>()
            {
                new Children(){ChildID=1,ParentID=1,ChildName="Lucas"},
                new Children(){ChildID=2,ParentID=1,ChildName="Louise"},
                new Children(){ChildID=3,ParentID=3,ChildName="Edward"},
                new Children(){ChildID=4,ParentID=4,ChildName="Kevin"},
                new Children(){ChildID=5,ParentID=5,ChildName="Mike"}
            };
            return childrenList;
        }

        static List<Person> GetMorePersonList()
        {
            List<Person> personList = new List<Person>()
            {
                new Person(){ID=1,Name="愛迪生",Age=100,Gender=true},
                new Person(){ID=2,Name="瓦特",Age=120,Gender=true},
                new Person(){ID=3,Name="牛頓",Age =150,Gender=true},
                new Person(){ID=4,Name="圖靈",Age=145,Gender=true},
                new Person(){ID=5,Name="香農",Age=120,Gender=true},
                new Person(){ID=6,Name="居里夫人",Age=115,Gender=false},
                new Person(){ID=6,Name="居里夫人2",Age=115,Gender=false},
                new Person(){ID=7,Name="居里夫人3",Age=115,Gender=false},
                new Person(){ID=8,Name="居里夫人4",Age=115,Gender=false},
                new Person(){ID=9,Name="居里夫人5",Age=115,Gender=false},
                new Person(){ID=10,Name="居里夫人6",Age=115,Gender=false},
                new Person(){ID=11,Name="居里夫人7",Age=115,Gender=false},
                new Person(){ID=12,Name="居里夫人8",Age=115,Gender=false},
                new Person(){ID=13,Name="居里夫人9",Age=115,Gender=false},
                new Person(){ID=14,Name="居里夫人10",Age=115,Gender=false},
                new Person(){ID=15,Name="居里夫人11",Age=115,Gender=false},
                new Person(){ID=16,Name="居里夫人12",Age=115,Gender=false},
                new Person(){ID=17,Name="居里夫人13",Age=115,Gender=false},
                new Person(){ID=18,Name="居里夫人14",Age=115,Gender=false}
            };

            return personList;
        }
View Code

1.1 篩選高手Where方法

  Where方法提供了我們對於一個集合的篩選功能,但需要提供一個帶bool返回值的“篩選器”(匿名方法、委托、Lambda表達式均可),從而表明集合中某個元素是否應該被返回。這里,我們以上面的數據為例,篩選出集合中所有性別為男,年齡大於20歲的子集合,借助Where方法實現如下:

        static void SQOWhereDemo()
        {
            List<Person> personList = GetPersonList();

            List<Person> maleList = personList.Where(p =>
                p.Gender == true && p.Age > 20).ToList();
            maleList.ForEach(m => Console.WriteLine(m.ToString()));
        }

  (1)運行結果如下圖所示:

  (2)由本系列文章的第二篇可知,擴展方法的本質是在運行時調用擴展類的靜態方法,而我們寫的Lambda表達式在編譯時又會被轉為匿名方法(准確地說應該是預定義泛型委托實例)作為方法參數傳入擴展方法中,最后調用執行該擴展方法生成一個新的List集合返回。

1.2 投影大牛Select方法

  Select方法可以查詢投射,返回新對象集合。這里,假設我們先篩選出所有男性集合,再根據男性集合中所有項的姓名生成子集合(這是一個不同於原類型的類型),就可以借助Select方法來實現。

        static void SQOSelectDemo()
        {
            List<Person> personList = GetPersonList();

            List<LitePerson> liteList = personList.Where(p =>
                p.Gender == true).Select(
                p => new LitePerson() { Name = p.Name }).ToList();
            liteList.ForEach(p => Console.WriteLine(p.ToString()));
        }

  (1)運行結果如下圖所示:

  (2)這里也可以采用匿名類,可以省去事先聲明LitePerson類的步湊,但需要配合var使用:

var annoyList = personList.Where(p =>
    p.Gender == true).Select(
    p => new { Name = p.Name }).ToList();

  (3)這里因為實現LitePerson類重寫了ToString()方法,所以這里直接調用了ToString()方法。

1.3 排序小生OrderBy方法

  說到排序,我們馬上想起了SQL中的order by語句,而標准查詢運算符中也為我們提供了OrderBy這個方法,值得一提的就是我們可以進行多條件的排序,因為OrderBy方法返回的仍然是一個IEnumerable<T>的類型,仍然可以繼續使用擴展方法。但要注意的是,第二次應該使用ThenBy方法。

        static void SQOOrderByDemo()
        {
            List<Person> personList = GetPersonList();
            // 單條件升序排序
            Console.WriteLine("Order by Age ascending:");
            List<Person> orderedList = personList.OrderBy(p => p.Age).ToList();
            orderedList.ForEach(p => Console.WriteLine(p.ToString()));
            // 單條件降序排序
            Console.WriteLine("Order by Age descending:");
            orderedList = personList.OrderByDescending(p => p.Age).ToList();
            orderedList.ForEach(p => Console.WriteLine(p.ToString()));
            // 多條件綜合排序
            Console.WriteLine("Order by Age ascending and ID descending:");
            orderedList = personList.OrderBy(p => p.Age)
                .ThenByDescending(p => p.ID).ToList();
            orderedList.ForEach(p => Console.WriteLine(p.ToString()));
        }

  運行結果如下圖所示:

1.4 連接道士Join方法

  在數據庫中,我們對兩個表或多個表進行連接查詢時往往會用到join語句,然后指定兩個表之間的關聯關系(例如: a.bid = b.aid)。在標准查詢運算符中,細心的.NET基類庫也為我們提供了Join方法。現在,假設我們有兩個類:Person和Children,其中每個Children對象都有一個ParentID,對應Person對象的ID,現需要打印出所有Person和Children的信息,可以借助Join方法來實現。

        static void SQOJoinDemo()
        {
            List<Person> personList = GetPersonList();
            List<Children> childrenList = GetChildrenList();

            // 連接查詢
            var joinedList = personList.Join(childrenList,
                p => p.ID, c => c.ParentID, (p, c) => new
                {
                    ParentID = p.ID,
                    ChildID = c.ChildID,
                    ParentName = p.Name,
                    ChildName = c.ChildName
                }).ToList();
            joinedList.ForEach(c => Console.WriteLine(c.ToString()));
        }

  運行結果如下圖所示:

1.5 分組老師GroupBy方法

  在數據庫中,我們要對查詢結果進行分組會用到 group by 語句,在標准查詢運算符中,我們也有對應的GroupBy方法。這里,假設我們對Person數據集按照性別進行分類,該怎么來寫代碼呢?

        static void SQOGroupByDemo()
        {
            List<Person> personList = GetPersonList();

            IEnumerable<IGrouping<bool, Person>> groups =
                personList.GroupBy(p => p.Gender);
            IList<IGrouping<bool, Person>> groupList = groups.ToList();

            foreach (IGrouping<bool, Person> group in groupList)
            {
                Console.WriteLine("Group:{0}", group.Key ? "" : "");
                foreach (Person p in group)
                {
                    Console.WriteLine(p.ToString());
                }
            }
        }

  (1)這里需要注意的是:通過GroupBy方法后返回的是一個IEnumerable<IGrouping<TKey, TSource>>類型,其中TKey是分組依據的類型,這里是根據Gender來分組的,而Gender又是bool類型,所以TKey這里為bool類型。TSource則是分組之后各個元素的類型,這里是將List<Person>集合進行分組,因此分完組后每個元素都存儲的是Person類型,所以TSource這里為Person類型,Do you understand now?

  (2)運行結果如下圖所示:

  (3)可能有人會說我咋記得住GroupBy返回的那個類型,太長了,我也不想記。怎么辦呢?不怕,我們可以使用var關鍵字嘛:

            var annoyGroups = personList.GroupBy(p => p.Name).ToList();
            foreach (var group in annoyGroups)
            {
                Console.WriteLine("Group:{0}", group.Key);
                foreach (var p in group)
                {
                    Console.WriteLine(p.ToString());
                }
            }

1.6 分頁實戰Skip與Take方法

  相信很多人都使用過標准查詢運算符進行分頁操作,這里我們再次來看看如何借助Skip與Take方法來實現分頁操作。還是以PersonList集合為例,假如頁面上的表格每頁顯示5條數據,該怎么來寫代碼呢?

        static void SQOPagedDemo()
        {
            // 這里假設每頁5行數據
            // 第一頁
            Console.WriteLine("First Page:");
            var firstPageData = GetPagedListByIndex(1, 5);
            firstPageData.ForEach(d => Console.WriteLine(d.ToString()));
            // 第二頁
            Console.WriteLine("Second Page:");
            var secondPageData = GetPagedListByIndex(2, 5);
            secondPageData.ForEach(d => Console.WriteLine(d.ToString()));
            // 第三頁
            Console.WriteLine("Third Page:");
            var thirdPageData = GetPagedListByIndex(3, 5);
            thirdPageData.ForEach(d => Console.WriteLine(d.ToString()));
        }

        static List<Person> GetPagedListByIndex(int pageIndex, int pageSize)
        {
            List<Person> dataList = GetMorePersonList();
            return dataList.Skip((pageIndex - 1) * pageSize)
                .Take(pageSize).ToList();
        }

  運行結果如下圖所示:

1.7 淺談延遲加載與即時加載

  (1)延遲加載(Lazy Loading):只有在我們需要數據的時候才去數據庫讀取加載它。

  在標准查詢運算符中,Where方法就是一個典型的延遲加載案例。在實際的開發中,我們往往會使用一些ORM框架例如EF去操作數據庫,Where方法的使用則是每次調用都只是在后續生成SQL語句時增加一個查詢條件,EF無法確定本次查詢是否已經添加結束,所以沒有辦法木有辦法在每個Where方法執行的時候確定最終的SQL語句,只能返回一個DbQuery對象,當使用到這個DbQuery對象的時候,才會根據所有條件生成最終的SQL語句去查詢數據庫。

    var searchResult = personList.Where(p =>
          p.Gender == false).Where(p => p.Age > 20)
          .Where(p=>p.Name.Contains("奶茶"));

  (2)即時加載(Eager Loading):加載數據時就把該對象相關聯的其它表的數據一起加載到內存對象中去。

  在標准查詢運算符中,FindAll方法就是一個典型的即時加載案例。與延遲加載相對應,在開發中如果使用FindAll方法,EF會根據方法中的條件自動生成SQL語句,然后立即與數據庫進行交互獲取查詢結果,並加載到內存中去。

        var searchResult = personList.FindAll(p=>p.Gender == false
                && p.Name.Contains("奶茶"));

二、查詢方式誰更快?LINQ:[ C# 3.0/.NET 3.x 新增特性 ]

2.1 初識LINQ:類似SQL風格的代碼

  LINQ又稱語言集成查詢,它是C# 3.0的新語法。在更多的人看來,它是一種方便的查詢表達式,或者說是和SQL風格接近的代碼

    var maleList = from p in personList
                   where p.Gender == true
                   select p;

  (1)LINQ表達式以"from"開始,以"select 或 group by子句"結尾;

  (2)LINQ表達式的輸出是一個 IEnumerable<T> 或 IQueryable<T> 集合;(注:T 的類型 由 select 或 group by 推斷出來)

2.2 LINQ使用:實現除Skip和Take外的標准查詢運算符的功能

  (1)基本條件查詢:

            List<Person> personList = GetPersonList();
            List<Children> childList = GetChildrenList();
            // 基本條件查詢
            Console.WriteLine("Basic Query:");
            var maleList = from p in personList
                           where p.Gender == true
                           select p;
            maleList.ToList().ForEach(m =>
                Console.WriteLine(m.ToString()));
View Code

  (2)排序條件查詢:

            // 排序條件查詢
            Console.WriteLine("Order Query:");
            var orderedList = from p in personList
                              orderby p.Age descending
                              orderby p.Name ascending
                              select p;
            orderedList.ToList().ForEach(m =>
                Console.WriteLine(m.ToString()));
View Code

  (3)連接查詢:

            // Join連接查詢
            Console.WriteLine("Join Query:");
            var joinedList = from p in personList
                             join c in childList
                             on p.ID equals c.ParentID
                             select new
                             {
                                 Person = p,
                                 Child = c
                             };
            foreach (var item in joinedList)
            {
                Console.WriteLine(item.ToString());
            }
View Code

  (4)分組查詢:

            // 分組條件查詢
            Console.WriteLine("Group Query:");
            var groupList = from p in personList
                            group p by p.Gender;
            foreach (var group in groupList)
            {
                Console.WriteLine("Group:{0}", 
                    group.Key? "":"");
                foreach(var item in group)
                {
                    Console.WriteLine(item.ToString());
                }
            }
View Code

  運行結果請參考上一節標准查詢運算符中相關的運行結果,或下載附件運行查看,這里不再貼圖。

2.3 LINQ本質:生成對應的標准查詢運算符

  作為一個細心的.Net碼農,我們不由得對LINQ表達式為我們做了哪些工作而好奇?於是,我們又想起了我們的“滑板鞋”—Reflector或ILSpy,去看看編譯器為我們做了什么事!

  (1)以上述的基本條件查詢代碼為例,我們看到原來編譯器將LINQ生成了對應的標准查詢運算符,即Where擴展方法:

  (2)再來看看排序條件查詢的代碼,也是生成了對應的標准查詢運算符,即OrderBy擴展方法:

  (3)總結:LINQ編譯后會生成對應的標准查詢運算符(查詢->Where,排序->OrderBy,連接->Join,分組->GroupBy),所以LINQ表達式其實就是類似於SQL風格的一種更加友好語法糖而已。其本質還是擴展方法、泛型委托等“舊酒”,被一個“新瓶子”所包裝了起來,就變得高大上了。

系列總結

  轉眼之間,四篇文章的介紹就到此結束了,其實本系列介紹的都是不算新語法,其實也可以說成是老語法了。說它們新,只不過是相對於.NET老版本而言,而且平時開發中大家有可能沒有注意到的一些細節,本系列做了一個簡單的介紹。這幾天看到很多園子里的童鞋開始關注C# 6.0的新特性了,粗略看了看,語法糖居多,相信經過了這一系列的探秘,對於新的語法糖,我們可以站在一個比較高的高度去看待它們。最后,謝謝各位園友的瀏覽,以及給我的一些鼓勵,再次感謝!

參考文章

  (1)MSDN,《標准查詢運算符概述》:http://msdn.microsoft.com/zh-cn/library/bb397896.aspx

  (2)MSDN,《LINQ(語言繼承查詢)》:http://msdn.microsoft.com/library/bb397926.aspx

  (3)jake強,《為提高EF性能需要注意哪些事情?》:http://www.cnblogs.com/jake1/archive/2013/04/25/3043664.html

附件下載

  NewGrammerDemo-v1.3:http://pan.baidu.com/s/1pJnyKtX

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM