2016年8月3日 星期三

深入理解C#(C# in Depth) 讀書心得 Part 2

Part 3 C# 3.0 資料存取重大改變(Revolutionizing data access)
  1. 使用聰明的編譯器防止錯誤(Cutting fluff with a smart compiler)
    • 自動產生屬性對應的程式碼(Automatically implemented properties),就是簡化properry的撰寫,編譯器會自動產生相對應的程式碼,範例如下,這樣可以替代一些寫在class裡的區域變數
    • public string Name { get; set; }
      //編譯成
      private string k__BackingField;
      public string Name
      {
          get { return k__BackingField; }
          set { k__BackingField = value; }
      }
      
    •  宣告隱含型別的變數(Implicityping of local variables),就是可以在局部區塊用var 宣告一個變數,並指定初始值
      • 使用的幾個原則
        1. 一定要給初始值
        2. 不可指定匿名方法或方法群組 
        3. 不可指定null
      • 作者提到一些優缺點,基本上不必因為是新的寫法就拼命用,要評估程式碼的適用情形,最大的考慮點就是可讀性的好壞,比如說宣告一個Dictionary<string, List<Person>>如果沒使用var,程式碼會看起來又臭又長,但有個好處是明確定義變數的類型,這部分可以斟酌使用,並沒有絕對的好與壞。
    •  簡單的初始化(Simplified initialization)C# 3.0在類別的初始化,增加了高可讀性及簡化了程式碼的撰寫
      • 看個範例程式碼就知道改了甚麼
      • //建立一個簡單的類別
        public class Person
        {
            public int Age { get; set; }
            public string Name { get; set; }
            List<Person> friends = new List<Person>();
            public List<Person> Friends { get { return friends; } }
            Location home = new Location();
            public Location Home { get { return home; } }
            public Person() { }
            public Person(string name)
            {
                Name = name;
            }
        }
        public class Location
        {
            public string Country { get; set; }
            public string Town { get; set; }
        }
        
        //C# 2.0的寫法
        Person tom1 = new Person();
        tom1.Name = "Tom";
        tom1.Age = 9;
        Person tom2 = new Person("Tom");
        tom2.Age = 9;
        
        //C# 3.0增加的語法糖 
        Person tom3 = new Person() { Name = "Tom", Age = 9 };
        Person tom4 = new Person { Name = "Tom", Age = 9 };
        Person tom5 = new Person("Tom") { Age = 9 };
        
        //還可以這樣
        Person[] family = new Person[]
        {
            new Person { Name = "Holly", Age = 36 },
            new Person { Name = "Jon", Age = 36 },
            new Person { Name = "Tom", Age = 9 },
            new Person { Name = "William", Age = 6 },
            new Person { Name = "Robin", Age = 6 }
        };
        
    • 隱含類型的陣列(Implicitly typed arrays),更便利的方式來做陣列的宣告及使用方式,看個範例
    • //C#1.0 2.0會這樣寫
      string[] names = {"Holly", "Jon", "Tom", "Robin", "William"};
      MyMethod(names);
      //C#3.0可以這樣寫
      MyMethod(new string[] {"Holly", "Jon", "Tom", "Robin", "William"});
      //或
      MyMethod(new[] {"Holly", "Jon", "Tom", "Robin", "William"});
      
    • 匿名型別(Anonymous types),這一般來說都會結合LINQ一起使用,這邊先就他的特性稍微瞭解一下
      • 就前面的Person的範例來看,我們不先宣告Person的類別,直接使用
      • //名叫Tom 9歲
        var tom= new { Name = "Tom", Age = 9 };
        //也可以用在陣列裡
        var family = new[]
        {
            new { Name = "Holly", Age = 36 },
            new { Name = "Jon", Age = 36 },
        };
        
      • 複製屬性,可以用更簡潔的寫法
      • //原本的寫法
        new { Name = person.Name, IsAdult = (person.Age >= 18) }
        //可以省略指定屬性名,直接複製名稱及值
        new { person.Name, IsAdult = (person.Age >= 18) }
        //來看個碗整一點的範例
        List<Person> family = new List<Person>
                {
                   new Person { Name = "Holly", Age = 36 },
                   new Person { Name = "Jon", Age = 36 },
                   new Person { Name = "Tom", Age = 9 },
                   new Person { Name = "Robin", Age = 6 },
                   new Person { Name = "William", Age = 6 }
                };
                var converted = family.ConvertAll(delegate(Person person)
                   { return new { person.Name, IsAdult = (person.Age >= 18) }; }
                );
               foreach (var person in converted)
               {
                   Console.WriteLine("{0} is an adult? {1}",
                                    person.Name, person.IsAdult);
        }
        
      • 基本上這些用法大部分的會配合著LINQ來使用,可以讓程式碼更好寫更易懂,後續看到LINQ時還會在使用到,而且用的會很頻繁.
  2. Lambda運算式及運算式樹狀結構(Lambda expressions and expression trees)
    • 委派的Lambda運算式(Lambda expressions as delegates),Lambda提供更容易讀的更簡潔的程式碼
      • 介紹Func<...>委派類型,在.NET3.5有提供了Func<>()的參數最多到四個,.NET4.0提供到了17個,這個Func<>()除了提供參數進去最後會回傳一個直回來,定義如下
      • TResult Func<TResult>()
        TResult Func<T,TResult>(T arg)
        TResult Func<T1,T2,TResult>(T1 arg1, T2 arg2)
        TResult Func<T1,T2,T3,TResult>(T1 arg1, T2 arg2, T3 arg3)
        TResult Func<T1,T2,T3,T4,TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
        //Example
        Func<string,double,int>
        //等同
        public delegate int SomeDelegate(string arg1, double arg2)
        
      • 另外如要實現void的方式,可以使用Action<...>系列的委派
    • 使用List<T>和事件的簡單範例(Simple examples using List<T> and events),這邊用幾個簡單常見的例子看一下Lambda運算式的寫法 
      • 用一個Lambda運算式來處理一個電影列表
        class Film {
           public string Name { get; set; }
           public int Year { get; set; }
        }
        var films = new List<Film>
        {
           new Film { Name = "Jaws", Year = 1975 },
           new Film { Name = "Singing in the Rain", Year = 1952 },
           new Film { Name = "Some like it Hot", Year = 1959 }
        };
        //建立重複使用的委派方法
        Action<Film> print = film => 
            Console.WriteLine("Name={0}, Year={1}", film.Name, film.Year);
        //執行print方法
        films.ForEach(print);
        //找出年度大於1960的並列出來
        films.FindAll(film => film.Year < 1960)
             .ForEach(print);
        //排序後並列出來
        films.Sort((f1, f2) => f1.Name.CompareTo(f2.Name));
        films.ForEach(print);
        
      • FindAll的實際方法在編譯器看起來應該是這樣
      • private static bool SomeAutoGeneratedName(Film film) {
           return film.Year < 1960;
        }
        films.FindAll(new Predicate<Film>(SomeAutoGeneratedName));
        
      • 再來看一下事件是如何實作的
      • static void Log(string title, object sender, EventArgs e)
        {
           Console.WriteLine("Event: {0}", title);
           Console.WriteLine(" Sender: {0}", sender);
           Console.WriteLine(" Arguments: {0}", e.GetType());
           foreach (PropertyDescriptor prop in
                    TypeDescriptor.GetProperties(e))
           {
              string name = prop.DisplayName;
              object value = prop.GetValue(e);
              Console.WriteLine("    {0}={1}", name, value);
           } 
        }
        Button button = new Button { Text = "Click me" };
        button.Click      += (src, e) => Log("Click", src, e);
        button.KeyPress   += (src, e) => Log("KeyPress", src, e);
        button.MouseClick += (src, e) => Log("MouseClick", src, e);
        
    • 表達樹(運算式樹狀結構)(Expression trees),.NET 3.5提供了一種抽象的方式將一些代碼表示成一個對象樹,Expression trees對LINQ有這很重要的關聯
      • Expression包含了兩種屬性
        1. Type-就是返回的類型,如果要反回string.length,返回的就是int.
        2. NodeType-返回所代表的表達式種類.相關詳細的描述在MSDN有完整的類別說明,原則上就是要執行什麼樣的expression就用相對應的類別.
      • 我們來建立一個簡單的Expression trees,只是單純的兩個數字相加,要注意到的是由下而上來建立運算式的,這是因為運算式是不可變的(immutable)
      • Expression firstArg = Expression.Constant(2);
        Expression secondArg = Expression.Constant(3);
        Expression add = Expression.Add(firstArg, secondArg);
        Console.WriteLine(add);
        //這邊會輸出(2 + 3)
        
      • LambdaExpression 是從Expression衍生的類別,Expression<TDelegate>又是LambdaExpression的衍生類別,Expression<TDelegate>以靜態類型的方式指定了他是哪種運算式,確定了要返回的類型和參數,所以他可以跟<Func<int>>一起使用,我們可以用Expression.Lambda來完成這件事,這邊LambdaExpression有提供Compile的方法建立一個委派來執行運算式,延續上面的範例來執行結果.
      • FuncFunc<int> compiled = Expression.Lambda<Func<int>>(add).Compile(); 
        Console.WriteLine(compiled());
        //這邊結果當然就是5
        
      • 接下來我們再把LambdaExpression轉成Expression trees,透過下面的方式來作轉換
      • Expression<Func<int>> return5 = () => 2 + 3; 
        Func<int> compiled = return5.Compile(); 
        Console.WriteLine(compiled());
        
      • 再來看一個較為複雜的例子,我們引用了stirng.StartsWith()的方法來實作
      • Expression<Func<string, string, bool>> expression =
           (x, y) => x.StartsWith(y);
        var compiled = expression.Compile();
        Console.WriteLine(compiled("First", "Second"));
        Console.WriteLine(compiled("First", "Fir"));
        //上面是實際Lambda的做法,我們把他背後做的事情拆開來看
        //宣告幾個運算式必要的條件
        //宣告方法運用到MethodInfo
        MethodInfo method = typeof(string).GetMethod
           ("StartsWith", new[] { typeof(string) });
        //宣告兩個參數
        var target = Expression.Parameter(typeof(string), "x");
        var methodArg = Expression.Parameter(typeof(string), "y");
        //宣告傳入的參數到陣列
        Expression[] methodArgs = new[] { methodArg };
        //建構一個運算式,將前面宣告的方法及參數傳入
        Expression call = Expression.Call(target, method, methodArgs);
        //再來就把它轉換成Lambda運算式
        var lambdaParameters = new[] { target, methodArg };
        var lambda = Expression.Lambda<Func<string, string, bool>>
            (call, lambdaParameters);
        //
        var compiled = lambda.Compile();
        Console.WriteLine(compiled("First", "Second"));
        Console.WriteLine(compiled("First", "Fir"));
        
      • 如果想看Lambda運算式的實際解析的程式碼,可以利用VS的visualizer來查看。
      • 前面講這麼多運算式主要的目的就是LINQ,LINQ就是Lambda Expression + Expression trees + extension methods。
      • LINQ的中心思想就是從一個熟悉的語言生成一個Expression trees,將作為一個中間層,再將轉換成目標平台上的語言,像SQL。
      • Expression trees除了LINQ以外,可以做到3件事,基本上我還不是很瞭書中想表達的這3件事
        1. 優化動態語言運行
        2. 放心的對成員進行重構
        3. 更簡單的反射
    • 改變型別推斷及多載解析
      • 改變的原因:精簡泛型方法的呼叫,這邊利用不指定參數的類型作泛型方法的呼叫,範例如下,
      • static void PrintConvertedValue<TInput,TOutput>
            (TInput input, Converter<TInput,TOutput> converter)
        {
            Console.WriteLine(converter(input));
        }
        //在C#2.0 我們可能就要這樣寫,必須指定傳入、傳出的參數型別
        PrintConvertedValue<string int>("I'm a string", x => x.Length);
        //C# 3.0可直接忽略型別指定,它會自行推斷
        PrintConvertedValue("I'm a string", x => x.Length);
        
      • 匿名方法回傳的類型如有多種則會判斷每個回傳類型是否允許隱含轉換,如下範例會回傳object
      • delegate T MyFunc<T>();
        static void WriteResult<T>(MyFunc<T> function)
        {
            Console.WriteLine(function());
        }
        WriteResult(delegate
        {
            if (DateTime.Now.Hour < 12)
            {
                return 10;
            }
            else
            {
                return new object();
            }
        });
        
      • 兩階段的型別推斷,如有兩個以上的參數型別需要判斷會有兩階段的判斷模式,我們用一個範例來解說
      • static void PrintConvertedValue<TInput,TOutput>
           (TInput input, Converter<TInput,TOutput> converter)
        {
           Console.WriteLine(converter(input));
        }
        PrintConvertedValue("I'm a string", x => x.Length);
        
        1. 第一階段開始
        2. 第一個參數類型(TInput)及參數是string類型,我們推斷string到TInput肯定存在隱含轉換
        3. 第二個參數類型是Convert<TInput, TOutput>類型, 參數是隱含轉換的Lambda運算式,此時不做任何的推斷
        4. 第二階段開始
        5. TInput不依賴任何非固定的參數,所以它被確定為string
        6. 現在第二個參數有一個固定的輸入類型,但有一個非固定的輸出類型,我們可以把它視為(string x) => x.Length,並推斷出返回的類型是int,從int到TOutput會發生一次隱含轉換
        7. 重複第二階段
        8. TOutput不依賴任何非固定參數,所以它被確定為int
        9. 現在沒有非固定的類型參數,推斷成功
      • 選擇正確的多載方法,基本上會去比對最合適的參數型別及參數選擇適當的方法,比較有點讓人模糊的例子我們來看一下
      • static void Execute(Func<int> action)
        {
           Console.WriteLine("action returns an int: " + action());
        }
        static void Execute(Func<double> action)
        {
           Console.WriteLine("action returns a double: " + action());
        }
        Execute(() => 1);
        
        這個例子會選擇第一個方法,因為int to int 跟int to double雖然都合理,但是最合適的還是int to int,這邊有個規則是如果一個匿名函數能轉換成參數列表,但返回類型不同的兩個委派類別,就根據“推斷的返回類型”到“委派的返回類型”的轉換來判定哪個委派轉換更好
  3. 擴充方法(extension method)
    • 在還沒使用擴充方法之前,比如我們要將string或int做處理時,我們會很習慣的寫一個function傳入變數再回傳處理完的值,原則上這也不是錯,只是有點不好看
    • 擴充方法的語法
      • 宣告的方法有幾個要點如下
        1. 必須是靜態類別(static)
        2. 至少要有一個傳入的參數
        3. 第一個傳入的參數必須帶有前綴詞 this
        4. 第一個傳入的參數不得帶有修飾詞(out, ref)
        5. 第一個傳入參數的型別不可以是指標型別
      • 使用擴充方法,來看個範例連同宣告及使用,這是使用到Stream來做複製的動作
      • public static class StreamUtil
        {
           const int BufferSize = 8192;
           public static void CopyTo(this Stream input, Stream output) {
              byte[] buffer = new byte[BufferSize];
              int read;
              while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
              {
                 output.Write(buffer, 0, read);
              }
           }
           public static byte[] ReadFully(this Stream input) {
              using (MemoryStream tempStream = new MemoryStream())
              {
                 CopyTo(input, tempStream);
                 return tempStream.ToArray();
              }
           }
        }
        WebRequest request = WebRequest.Create("http://manning.com"); 
        using (WebResponse response = request.GetResponse())
        using (Stream responseStream = response.GetResponseStream()) 
        using (FileStream output = File.Create("response.dat"))
        {
           responseStream.CopyTo(output);
        }
        
      • 使用擴充方法要小心名稱相同的問題,像上述的範例在.NET 4.0就會失效,因為Stream已有原生的擴充方法叫CopyTo,這種情形下會優先使用Stream.CopyTo,但因為做的事相同,在編譯時也不會有錯誤
      • 如果使用擴充方法的變數是null reference的情況下會怎麼樣呢,之前我們用instance method(執行個體方法)時是不允許用null的,但擴充方法會把他視為一個參數傳入,來看下面範例,第一個回傳的會是false,第二個回傳的會是true,這樣的做法可讀性更佳更清楚
      • using System;
        public static class NullUtil
        {
           public static bool IsNull(this object x)
           {
              return x == null;
           }
        }
        public class Test
        {
           static void Main()
           {
              object y = null;
              Console.WriteLine(y.IsNull());
              y = new object();
              Console.WriteLine(y.IsNull());
           } 
        }
        
    • .NET 3.5的擴充方法,有兩個最常用的類別就是Enumable跟Queryable,這兩個類別擴充了IEnumable跟IQueryable,這邊我們先看Enumable
      • 這邊先用簡單的數字區間來作為範例,使用Range及Reverse兩個方法,這邊會使用到yield的方式來回傳值,有個效能考量在實做一些複雜的功能時要注意的是像範例中我們先用Range取回數字區間再用Reverse做反轉,這時在Reverse裡一定會將所有的值傳回來做反轉的動作再回傳到引用的地方,這時就會使用記憶體空間暫存,當功能複雜資料量大的時候記憶體空間使用量就一定會大,類似Dataset與DataReader,所以在實做時要注意
        var collection = Enumerable.Range(0, 10).Reverse();
        foreach (var element in collection)
        {
           Console.WriteLine(element);
        }
        //得到的結果9,8,7...0
        
      • 使用Where來篩選資料,延續上個範例,再鏈結裡再加一個Where做條件篩選,這邊也有個效能考量,就是在串聯鏈結的時候的順序,如果把Where放在Reverse後面,會變成需要把所有數字都反轉在做篩選,這樣會需要較多的空間及運算,雖然這個小範例沒太大影響,但如果資料量堂大時就要注意了
      • var collection = Enumerable.Range(0, 10).Where(x => x % 2 != 0).Reverse();
        foreach (var element in collection)
        {
           Console.WriteLine(element);
        }
        //得到的結果9,7,5,3,1
        
      • 再來利用Select來取得匿名型別,延續前面的範例,再多加Select來取回原始數字及平方根
      • var collection = Enumerable.Range(0, 10).Where(x => x % 2 != 0).Reverse()
           .Select(x => new { Original = x, SquareRoot = Math.Sqrt(x) } );
        foreach (var element in collection)
        {
           Console.WriteLine("sqrt({0})={1}",
           element.Original,
           element.SquareRoot);
        }
        //得到的結果
        //sqrt(9)=3, sqrt(7)=2.64575131106459, sqrt(5)=2.23606797749979, sqrt(3)=1.73205080756888, sqrt(1)=1
        
      • 接下來看OrderBy、OrderByDescending、ThenBy、ThenByDescending,顧名思義就是排序,OrderBy在前,如有超過一個以上的元素後面叫接ThenBy,繼續之前的範例
      • var collection = Enumerable.Range(-5, 11).Select(x => new { Original = x, Square = x * x })
           .OrderBy(x => x.Square)
           .ThenBy(x => x.Original);
        foreach (var element in collection)
        {
           Console.WriteLine(element);
        }
        //得到的結果
        //{ Original = 0, Square = 0 }, { Original = -1, Square = 1 }, { Original = 1, Square = 1 }, { Original = -2, Square = 4 }
        //{ Original = 2, Square = 4 }, { Original = -3, Square = 9 }, { Original = 3, Square = 9 }, { Original = -4, Square = 16 }
        //{ Original = 4, Square = 16 }, { Original = -5, Square = 25 }, { Original = 5, Square = 25 }
        
      • 至於運用到商業邏輯的部分就實作一些功能實再來看其他相關的功能特性,像Sum、GroupBy等
    •  使用的思路和原則
      • 作者提到extending the world(擴展世界)、fluent interfaces(流暢的介面)、理智的使用擴充方法,主要是提到擴充方法的好處,但也不是所有情況都適用,必須要視情形、環境、團隊來決定是否使用。
  4. 查詢運算式和LINQ to Object
    • 介紹一下LINQ是甚麼
      • LINQ最重要的一個部分就是序列(sequence),這邊利用一個簡單的例子來看一下,adultNames最後會得到一個IEnumerable<string>裡面包含人的年紀大於18歲的人,他的查詢運算式的處理順序在第一行會先取得people裡的所有物件,第二行再依據第一行的結果做篩選年紀大於18的有那些,最後一行則依據前面篩選完的物件取回人的名字
      • var adultNames = from person in people
                         where person.Age >= 18
                         select person.Name;
        
      • LINQ還有另一個特色是延遲處理,前面範例宣告時只是在記憶體裡儲存運算式,直到讀取第一筆資料時才真正執行運算式,他實際執行順序,讀取第一筆時,他會先提取select的部分需要哪些,再來提取where條件,接著就是people集合,這時他會先丟一筆資料給where看條件是否符合,符合救回給select,不符合則繼續取下一筆,直到取到完,不管它的筆數有多少,一次只會傳回一筆
    • 這邊說明一下LINQ的語法在編譯器會轉化成查詢運算式,像以下的範例
    • //一段LINQ的查詢語法
      var query = from user in SampleData.AllUsers
                  select user;
      //編譯器轉換成
      var query = SampleData.AllUsers.Select(user => user);
      
    • 過濾及排序
      • 運用where方法來做條件過濾,一樣他在編譯時期會轉換成查詢運算式
      • User tim = SampleData.Users.TesterTim;
        var query = from defect in SampleData.AllDefects
                    where defect.Status != Status.Closed 
                    where defect.AssignedTo == tim 
                    select defect.Summary;
        //編譯器轉換成
        SampleData.AllDefects.Where(defect => defect.Status != Status.Closed)
                             .Where(defect => defect.AssignedTo == tim)
                             .Select(defect => defect.Summary)
        
      • 運用orderby方法來做條件排序,延續前面的範例
      • var query = from defect in SampleData.AllDefects
                    where defect.Status != Status.Closed
                    where defect.AssignedTo == tim
                    orderby defect.Severity descending, 
                            defect.LastModified 
                    select defect;
        //編譯器轉換成
        SampleData.AllDefects.Where(defect => defect.Status != Status.Closed)
                             .Where(defect => defect.AssignedTo == tim)
                             .OrderByDescending(defect => defect.Severity)
                             .ThenBy(defect => defect.LastModified)
        
        
      • 這邊可能會看到一個奇怪的現象,就是前面orderby、thenby後的select不見了,作者說這是退化查詢運算式(Degenerate query expressions),其實就是在select出來的內容如果沒有特俵要選哪幾個元素時他會省略掉這個步驟,因為多此一舉,得到的結果還是一樣
    • let子句及透明標示符(transparent identifiers)
      • 用let來做中間的計算,let就是引入另一個運算式指定到變數中,看下面範例,length另外去存取名稱的長度,直接在運算式中做引用
      • var query = from user in SampleData.AllUsers
                    let length = user.Name.Length
                    orderby length
                    select new { Name = user.Name, Length = length };
        
      • 透明標示符(transparent identifiers),上面使用let的方法就是透明標示符,作者的解釋是let另外調用了一個方法來讓select做使用,因為最終要查詢出指定的項目髓以會變成兩層的select,編譯器會自動模擬出第二層,而範例中的z就是編譯器隨機產生出來的,前面範例經由編譯器轉換會變成如下所示
      • SampleData.AllUsers
                  .Select(user => new { user, length = user.Name.Length })
                  .OrderBy(z => z.length)
                  .Select(z => new { Name = z.user.Name, Length = z.length })
        
    • 聯接,join子句
      • join的寫法我們直接看範例,運用到join、on、equal
      • var query = from defect in SampleData.AllDefects
                    join subscription in SampleData.AllSubscriptions
                    on defect.Project equals subscription.Project
                    select new { defect.Summary, subscription.EmailAddress };
        
      • 如果有需要過濾條件,可以評估是否可在聯接前先做過濾,增加效能,如下範例有兩種方式,第一種方式較直覺
      • //方法1
        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 }
        //方法2
        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 { defect.Summary, subscription.EmailAddress }
        
      • 使用join into的方式做分組聯接,可將一個聯接的整個集合寫到一個變數中,如下範例
      • var query = from defect in SampleData.AllDefects
                    join subscription in SampleData.AllSubscriptions
                       on defect.Project equals subscription.Project
                       into groupedSubscriptions
                    select new { Defect = defect,
                                 Subscriptions = groupedSubscriptions };
        
      • 再來看一個模擬sql left join 的做法,如以下範例
      • var dates = new DateTimeRange(SampleData.Start, SampleData.End);
        var query = from date in dates
                    join defect in SampleData.AllDefects
                       on date equals defect.Created.Date
                       into joined
                    select new { Date = date, Count = joined.Count() };
        
      • 多個from子句做交叉聯接和合併,可以把兩個完全不相關的集合合併成一個查詢結果,如下範例
      • var query = from user in SampleData.AllUsers
                    from project in SampleData.AllProjects
                    select new { User = user, Project = project };
        
      • 另外來看交叉的部分可以怎麼做,如下範例,可依據第一行的查詢結果帶入第二行的查詢當作參數
      • var query = from left in Enumerable.Range(1, 4)
                    from right in Enumerable.Range(11, left)
                    select new { Left = left, Right = right };
        //這段依據編譯器轉換會運用到SelectMany的擴充方法
        Enumerable.Range(1, 4)
                  .SelectMany(left => Enumerable.Range(11, left),
                             (left, right) => new {Left = left, Right = right})
        
        //
        //另一個比較實用的範例可用來讀取檔案
        var query = from file in Directory.GetFiles(logDirectory, "*.log") 
                    from line in ReadLines(file)
                    let entry = new LogEntry(line)
                    where entry.Type == EntryType.Error
                    select entry;
        
    • 分組和延續(Groupings and continuations)
      • group by子句來做分組資料,看以下範例
      • var query = from defect in SampleData.AllDefects
                              where defect.AssignedTo != null
                              group defect by defect.AssignedTo;
        //讀取的時候要用key取出分組的資料
        foreach (var entry in query)
        {
            Console.WriteLine(entry.Key.Name);
            foreach (var defect in entry)
            {
                Console.WriteLine(" ({0}) {1}",
                defect.Severity, defect.Summary);
            }
            Console.WriteLine();
        }
        
      • 延續的意思就是第一段查詢出來的結果放到第二段繼續處理,這邊也用了into來把結果暫存到變數中,如以下範例,同時把名字及數量讀出來
      • var query = from defect in SampleData.AllDefects
                    where defect.AssignedTo != null
                    group defect by defect.AssignedTo into grouped 
                    select new { Assignee = grouped.Key,
                                 Count = grouped.Count() };
        
      • 再來看看超過兩個延續的做法,延續前面的範例再加上排序在讀出結果,如下範例
      • var query = from defect in SampleData.AllDefects
                    where defect.AssignedTo != null
                    group defect by defect.AssignedTo into grouped
                    select new { Assignee = grouped.Key,
                                 Count = grouped.Count() } into result 
                    orderby result.Count descending
                    select result;
        
    • 這邊要思考的一件事就是查詢運算式及點標記(擴充方法的方式)的優缺,其實兩種方式都好沒有哪種方法比較好或對錯,取決於個人的習慣或團隊的習慣,基本上以程式碼可讀性高為原則。
  5.  超越集合的LINQ(LINQ beyond collections)
    • LINQ to SQL,這邊介紹了簡單的資料模型做範例,主要就是依據上一章節所描述的LINQ最終轉化成SQL的語法,這邊範例程式碼就不多做解釋了
    • 用IQueryable與IQueryProvider進行轉換
      • IQueryable<T>和介面的介紹
        1. IQueryable<T>繼承了IEnumable<T>、IEnumable、IQueryable
        2. IQueryable只有3個屬性,QueryProvider(IQueryProvider類別)、ElementType、Expression
      • 模擬介面來實現紀錄調用
      • 把運算式黏在一起,Queryable的擴充方法,這邊提到幾件事還蠻重要的
        1. LINQ to SQL能夠完成工作的主要原因有4個,都是C#3.0的特性,Lambda運算式、查詢運算式到使用Lambda運算式的普通運算式轉換、擴充方法及運算式樹。
        2. Enumable使用委派當作參數,Queryable使用運算式樹當作參數
        3. 這邊提到了Enumable與Queryable的差異性,Enumable原則上會直接去執行比如說條件篩選等,Queryable他實際上是產生運算式樹,直到執行excute時才真正的運作。 
    • LINQ to XML,這邊提到了一些跟XML文檔有關的寫入及讀取,用LINQ來操作XML相當方便
      • LINQ to XML中的核心類型都在System.Xml.Linq,主要的類型如下
        1. XName-表示元素和特性的名稱
        2. XNamespace-表示XML的命名空間
        3. XObject-XNode、XAttribute的父類別
        4. XNode-XML的節點
        5. XAttribute-包含名/值的特性
        6. XContainer-XML中可以包含子內容的節點
        7. XText-文本節點
        8. XElement-元素
        9. XDocument-文檔
      •  這邊來看一下簡單的例子瞭解他是如何實做的
      • var users = new XElement("users",
            SampleData.AllUsers.Select(user => new XElement("user",
               new XAttribute("name", user.Name),
               new XAttribute("type", user.UserType)))
        );
        Console.WriteLine(users);
        // Output
        <users>
          <user name="Tim Trotter" type="Tester" />
          <user name="Tara Tutu" type="Tester" />
          <user name="Deborah Denton" type="Developer" />
          <user name="Darren Dahlia" type="Developer" />
          <user name="Mary Malcop" type="Manager" />
          <user name="Colin Carton" type="Customer" />
        </users>
        
      • 查詢單個節點,查詢節點有幾個類別方法可用,這些方法的功能都跟字面上的意思一樣
        1. Ancestors
        2. AncestorsAndSelf
        3. Annotations
        4. Atributes
        5. Descendants
        6. DescendantAndSelf
        7. DescendantNodes
        8. DescendantNodesAndSelf
        9. Elements
        10. ElementAfterSelf
        11. ElementsBeforeSelf
        12. Nodes 
    • 平行LINQ,平行LINQ是LINQ to Object的平行(parallel)實做,簡稱PLINQ,一般我們寫的LINQ都是使用並行(concurrency)執行,也就是單執行緒,遇到需要大量運算的時候效能會比較低落,所以運用到平行運作,也就是多執行緒來執行,核心數越多效果就越高
      • 需要運用到這種技巧來寫的通常是有兩個from做交叉查詢時,這邊我們簡單看個小範例
      • var DictTwo = from i in Enumerable.Range(0, 999999).AsParallel().AsOrdered()
                      where isPrime(i)
                      select i;
        private static bool isPrime(int number)
        {
            if (number < 2) 
            {
                return false;
            }
            int limite = (int)Math.Sqrt(number);
            for (int i = 2; i <= limite; i++)
            {
                if (number % i == 0) 
                {
                    return false;
                }
            }
            return true;
        }
        
      • 另外還有一些可改變查詢行為的方法如下
        1. AsUnordered-讓有序查詢便無序
        2. WithCancellation-取消標記
        3. WithDegreeOfParallelism-指定執行查詢的最大開發任務數
        4. WithExecutionMode-強制查詢按並行方式執行
        5. WithMergeOptions-可以改變對結果的緩衝方式
    • LINQ to Rx,這是非同步的作法,這如以後有需要用到再回來看,本書這部分提到的也不多,他的全名是Reactive Extensions,如要使用要再另外下載SDK,還有大大寫的教學文可供參考。
    • 擴充LINQ to Objects的方法,這邊主要是依據自己的需求去擴充方法,有幾點設計及實作應該注意的問題
      • 單元測試-避免有空序列或無效參數
      • 檢查參數-在調用方法的同時執行參數檢查
      • 優化-使用ICollection
      • 文檔-在文檔中指名程式碼對輸入的處理和運算式的預期性能很重要
      • 盡量迭代一次-盡量不要跑兩次
      • 釋放迭代器-可以對迭代器用using,平常都是使用foreach自動幫我們釋放了
      • 自定義比較器-自行覆寫比較的IEquality<T>和IComparer<T>
      • 這邊看個範例對照前面的注意事項,除了沒有自定義比較器外都有符合
      • public static T RandomElement<T>(this IEnumerable<T> source, Random random)
        {
           if (source == null)
           {
              throw new ArgumentNullException("source");
           }
           if (random == null)
           {
              throw new ArgumentNullException("random");
           }
           ICollection collection = source as ICollection;
           if (collection != null)
           {
              int count = collection.Count;
              if (count == 0)
              {
                  throw new InvalidOperationException("Sequence was empty.");
              }
              int index = random.Next(count);
              return source.ElementAt(index);
           }
        
           using (IEnumerator iterator = source.GetEnumerator())
           {
              if (!iterator.MoveNext())
              {
                 throw new InvalidOperationException("Sequence was empty.");
              }
              int countSoFar = 1;
              T current = iterator.Current;
              while (iterator.MoveNext())
              {
                 countSoFar++;
                 if (random.Next(countSoFar) == 0)
                 {
                    current = iterator.Current;
                 }
              }
              return current;
           }
        }
        

沒有留言 :

張貼留言