2016年9月23日 星期五

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

Part 4 C# 4.0 良好的交互性(Playing nicely with others)
  1. 簡化程式碼的微小修改
    •  可選參數(optional parameter)及命名參數(named argument),可選參數就是在方法的參數上可指派預設值達到可選的效果,不用像以前必須要做方法的覆寫,命名參數是在呼叫方法時傳入參數(arguments)時指定對應到方法參數(parameters)的名稱,這樣可不必對應參數位置
      • 可選參數的使用,看以下範例,y及z皆有預設值所以可當可選參數
      • static void Dump(int x, int y = 20, int z = 30)
        {
           Console.WriteLine("x={0} y={1} z={2}", x, y, z);
        }
        Dump(1, 2, 3);
        Dump(1, 2);
        Dump(1);
        
      • 原則上可選參數的值要為常數,可以為null的型別,可選參數一定要在最後面
      • 命名參數使用,看以下範例,有一些組合的方式
      • static void Dump(int x, int y, int z)
        {
           Console.WriteLine("x={0} y={1} z={2}", x, y, z);
        }
        Dump(1, 2, 3);
        Dump(x: 1, y: 2, z: 3);
        Dump(z: 3, y: 2, x: 1);
        Dump(1, y: 2, z: 3);
        Dump(1, z: 3, y: 2);
        
      • 命名參數要注意到順序,雖然他不必對應參數位置,但為了可讀性還是依照順序寫比較好
      • 前面兩種方法把它結合在一起的話呢,原則上沒太大的問題,主要要注意到使用時會不會造成參數模擬兩可的情形發生,或是型別上的問題,比如以下範例,第一個要注意如果都不帶參數則兩個都可執行,第二個範例要注意object與int的型別轉換
      • //範例1
        static void Foo(int x = 10) {}
        static void Foo(int x = 10, int y = 20) {}
        
        Foo();
        Foo(1);
        Foo(y: 2);
        Foo(1, 2);
        //
        //範例2
        void Method(int x, object y) { ... }
        void Method(object a, int b) { ... }
        
        Method(10, 10);
        //可用這兩種方法解決
        Method(10, (object) 10);
        Method(x: 10, y: 10);
        
    • 改善COM的互動,這部分較沒有涉獵到,主要也是參數的改變,書上以操作Word文件檔為例子,以往在產生一個Word文件檔時要傳入一些不管有沒有用到的參數,C#4.0因有可選參數及命名參數讓這部分的程式碼精簡很多而且可讀性也較高。
    • 介面及委派的泛型的可變性,也就是共變數與反變數
      • 共變數(covariance)是回傳一個方法的值,如下範例
      • interface IFactory<T>
        {
           T CreateInstance();
        }
        
      • 反變數(contravariance)是傳入值並處理這個值,如下範例
      • interface IPrettyPrinter<T>
        {
           void Print(T document);
        }
        
      • 不變數(invariance)是指非共變或反變,雙向傳遞值,如下範例是進行序列化及反序列化
      • interface IStorage<T>
        {
           byte[] Serialize(T value);
           T Deserialize(byte[] data);
        }
        
      • 介面的共變數的寫法如下範例,shapesByConcat將兩個類別項目結合在一起
      • //Circle及Square都實做ISharp介面
        List<Circle> circles = new List<Circle> {
            new Circle(new Point(0, 0), 15),
            new Circle(new Point(10, 5), 20),
        };
        List<Square> squares = new List<Square> {
            new Square(new Point(5, 10), 5),
            new Square(new Point(-10, 0), 2)
        };
        //
        List<ISharp> shapesByAdding = new List<ISharp>();
        shapesByAdding.AddRange(circles);
        shapesByAdding.AddRange(squares);
        List shapesByConcat = circles.Concat(squares).ToList();
        
      • 介面的反變數寫法如下範例,circles可以用ISharp的類別做排序
      • class AreaComparer : IComparer<ISharp>
        {
            public int Compare(IShape x, IShape y)
            {
                return x.Area.CompareTo(y.Area);
            }
        }
        IComparer<ISharp> areaComparer = new AreaComparer();
        circles.Sort(areaComparer);
        
      •  委派中的可變性的寫法如下範例
      • //共變數
        Func<Square> squareFactory = () => new Square(new Point(5, 5), 10);
        Func<IShape> shapeFactory = squareFactory;
        //反變數
        Action<IShape> shapePrinter = shape => Console.WriteLine(shape.Area);
        Action<Square> squarePrinter = shapePrinter;
        //
        squarePrinter(squareFactory());
        shapePrinter(shapeFactory());
        
      •  較複雜的情況是同時有共變數及反變數的情形下如何實做,來看Converter<TInput, TOutput>這個類別,這跟Func<T, Tresult>類似,來看以下範例
      • Converter<object, string> converter = x => x.ToString();
        Converter<string, string> contravariance = converter;
        Converter<object, object> covariance = converter;
        Converter<string, object> both = converter;
        
    •  這邊有提到Lock在C# 4.0有一些改良,主要是避免在執行中發生錯誤導致執行緒被咬住的情況。
  2. 靜態語言中的動態綁定
    • 動態類型(dynamic)是什麼、何時用、為何而用、如何用
      • 動態類型就是在執行時期才決定型別
      • 動態類型在不確定型別但有相同屬性時可以使用,例如Length屬性,不管是String、StringBuilder、Array、Stream都不重要,只要能取得Length就好
    •  動態型別的用法就是dynamic這個關鍵字,基本上他會自動做隱含轉換為CLR類型,來看幾個範例
    • //這邊valueToAdd在item相加時會隱含轉換成字串,結果就是First2,Second2,Third2
      dynamic items = new List<string> { "First", "Second", "Third" };
      dynamic valueToAdd = 2;
      foreach (dynamic item in items)
      {
         string result = item + valueToAdd;
         Console.WriteLine(result);
      }
      //這個範例在執行中會報Microsoft.CSharp.RuntimeBinder.RuntimeBinderException的錯誤,因為這並非預期中的轉換
      dynamic items = new List<int> { 1, 2, 3 };
      dynamic valueToAdd = 2;
      foreach (dynamic item in items)
      {
         string result = item + valueToAdd;
         Console.WriteLine(result);
      }
      //只要把string result = item + valueToAdd改成
      Console.WriteLine(item + valueToAdd);
      
    • 動態型別的一些範例,目前有幾個類別庫是使用動態型別做的,包含MassiveDapperJson.NET
      • ˋ這邊作者有用Office.Excel的元件做範例,在之前我們都會用靜態類別然後強制轉型,如下第一個範例,現在我們可以改用dynamic的方式如下第二個範例 
      • //before
        var app = new Application { Visible = true };
        app.Workbooks.Add();
        Worksheet worksheet = (Worksheet) app.ActiveSheet;
        Range start = (Range) worksheet.Cells[1, 1];
        Range end = (Range) worksheet.Cells[1, 20];
        worksheet.Range[start, end].Value = Enumerable.Range(1, 20).ToArray();
        //after
        var app = new Application { Visible = true }; app.Workbooks.Add();
        dynamic worksheet = app.ActiveSheet;
        dynamic start = worksheet.Cells[1, 1];
        dynamic end = worksheet.Cells[1, 20]; worksheet.Range[start, end].Value = Enumerable.Range(1, 20).ToArray();
        
      • 再來作者提到使用IronPython,他可以很方便的去引用Python,這部分比較沒有涉獵來看個範例知道一下
      • string python = @"
           text = 'hello'
           output = input + 1
           ";
        ScriptEngine engine = Python.CreateEngine(); 
        ScriptScope scope = engine.CreateScope(); 
        scope.SetVariable("input", 10); 
        engine.Execute(python, scope); 
        Console.WriteLine(scope.GetVariable("text")); 
        Console.WriteLine(scope.GetVariable("input")); 
        Console.WriteLine(scope.GetVariable("output"));
        
      • 執行時的類型判斷,如果你想要做的不是只有調用方法,罵麼最好將所有額外的工作包在一個泛型方法內,然後動態的調用該泛型方法,用靜態類型來編寫所有剩餘的程式碼,如下範例
      • private static bool AddConditionallyImpl<T>(IList<T> list, T item) {
        if (list.Count < 10)
           {
              list.Add(item);
              return true;
           }
           return false;
        }
        public static bool AddConditionally(dynamic list, dynamic item)
        {
           return AddConditionallyImpl(list, item);
        }
        object list = new List<string> { "x", "y" };
        object item = "z";
        AddConditionally(list, item);
        
      • 彌補泛型運算式的不足,如下範例,有兩點比較有趣的事用default(T)來初始化total,另一個是將相加的結果強制轉換回T
      • public static T DynamicSum<T>(this IEnumerable<T> source)
        {
           dynamic total = default(T);
           foreach (T element in source)
           {
              total = (T) (total + element);
           }
           return total;
        }
        byte[] bytes = new byte[] { 1, 2, 3 };
        Console.WriteLine(bytes.DynamicSum());
        //印出6
        
      • 鴨子類型(Duck Typing),我們知道在執行時可以使用某個具有特殊名稱的成員,但無法確切告訴編譯器這個成員,因為這將取決於具體的類型,鴨子類型允許我們在訪問Count時不必執行類型檢查,如下範例
      • static void PrintCount(IEnumerable collection)
        {
           dynamic d = collection;
           int count = d.Count;
           Console.WriteLine(count);
        }
        ...
        PrintCount(new BitArray(10));
        PrintCount(new HashSet<int> { 3, 5 });
        PrintCount(new List<int> { 1, 2, 3 });
        
      • 多重分發,靜態類型是單一分發(single dispatch)的,多重分法會根據執行時參數的類型找出最適合的方法實現,如下範例
      • private static int CountImpl<T>(ICollection<T> collection)
        {
            return collection.Count;
        }
        private static int CountImpl(ICollection collection)
        {
            return collection.Count;
        }
        private static int CountImpl(string text)
        {
            return text.Length;
        }
        private static int CountImpl(IEnumerable collection)
        {
            int count = 0;
            foreach (object item in collection)
            {
                count++; 
            }
            return count;
        }
        public static void PrintCount(IEnumerable collection)
        {
            dynamic d = collection;
            int count = CountImpl(d);
            Console.WriteLine(count);
        }
        PrintCount(new BitArray(5));
        PrintCount(new HashSet<int> { 1, 2 });
        PrintCount("ABC");
        PrintCount("ABCDEF".Where(c => c > 'B'));
        
    • 背後的原理
      • DLR本身只是一個類別庫,他與CLR是不在同一個層級,另一個重要的部分是他有多層級暫存,這攸關到它的效能
      • DLR的核心概念
        1. 調用點-引用他的地方
        2. 接收器與綁定器-它需要其他訊息來判斷程式碼的含義及如何執行,接收器就是執行時接收引用的變數,綁定器(Binder)取決於語言,這邊C#是引用Microsoft.CSharp.RuntimeBinder.Binder
        3. 規則和緩存-規則就是調用所做出的決策,規則也關係到優化,再來會把規則儲存在緩存中
      • C#編譯器如何處理動態
        1. 如果使用了動態,他就是動態,這不是廢話嗎,他想表達的是當宣告為dynamic就一定會找尋符合dynamic對應的方法,如下範例
        2. static void Execute(string x)
          {
             Console.WriteLine("String overload");
          }
          static void Execute(dynamic x)
          {
             Console.WriteLine("Dynamic overload");
          }
          dynamic text = "text";
          Execute(text);  //執行string 參數的方法
          dynamic number = 10;
          Execute(number);  //執行dynamic 參數的方法
          
        3. CLR類型與動態類型之間的轉換,如果兩個類型(比如說dynamic與string)可以進行雙向的隱含轉換,情況會變得很糟糕,來看下面範例,array應該是什麼類別,他是dynamic[]而不是string[],編譯器可以將string轉換為dynamic但反過來就不行
        4. dynamic d = 0;
          string x = "text";
          var array = new[] { d, x };
          
        5. 動態運算式不一定都是動態求值,如下範例可以使用as來轉換型別
        6. dynamic d = GetValueDynamically();
          string x = d as string;
          
        7. 動態運算式不一定都是動態類型,如下範例
        8. dynamic d = GetValueDynamically();
          SomeType x = new SomeType(d);
          
      • 更加智能的C#編譯器,裡面提到了像多載裡有宣告dynamic時會對應到哪個多載的方法,這部分有些情況編譯器會在執行前就檢測出有問題的部分,其實這些在撰寫過程還是要注意型別轉換時是否會出現轉換失敗或對應不到相對應的方法。
      • 動態程式碼的約束
        1. 不能動態處理擴充方法,傳入的參數必須要轉型成靜態類型,如下範例
        2. dynamic size = 5;
          var numbers = Enumerable.Range(10, 10);
          var error = numbers.Take(size);  //這會編譯錯誤
          //下面這兩段可以編譯成功雖然有點醜
          var workaround1 = numbers.Take((int) size);
          var workaround2 = Enumerable.Take(numbers, size);
          
        3. 委派與動態類型之間的轉換限制,這邊展示了一些可以與不可以的用法
        4. //這邊都是允許的
          dynamic badMethodGroup = Console.WriteLine;
          dynamic goodMethodGroup = (Action<string>) Console.WriteLine;
          dynamic badLambda = y => y + 1;
          dynamic goodLambda = (Func<int, int>) (y => y + 1);
          dynamic veryDynamic = (Func<dynamic, dynamic>) (d => d.SomeMethod());
          //下面這段就編譯不過
          void Method(Action<string> action, string value)
          {
             action(value);
          }
          dynamic text = "error";
          Method(x => Console.WriteLine(x), text);
          
        5. 查詢動態元素的集合,下面範例會把number認定為int
        6. var list = new List<dynamic> { 50, 5m, 5d };
          var query = from number in list
                      where number > 4
                      select (number / 20) * 10;
          foreach (var item in query)
          {
             Console.WriteLine(item);
          }
          //這邊會印出20,2.50,2.5
          
        7. 類型指定和泛型類別參數,如下範例指出哪些可以哪些不行
        8. //下面四行是編譯不過的
          class BaseTypeOfDynamic : dynamic
          class DynamicTypeConstraint<T> where T : dynamic
          class DynamicTypeConstraint<T> where T : List<dynamic>
          class DynamicInterface : IEnumerable<dynamic>
          //下面兩行是允許的
          class GenericDynamicBaseClass : List<dynamic>
          IEnumerable<dynamic> variable;
          
      • 實現動態行為,這個部分日後有興趣再來看這部分。
Part 5 C# 5.0 讓非同步更加簡單
  1. 用async/await實現非同步
    • 介紹非同步的方法
      • 這邊用一個簡單的範例做一個介紹,使用HTTPClient的非同步方法,如果在之前可以使用WebClient來達到同樣效果但差別在於他不是非同步
      • class AsyncForm : Form
        {
           Label label;
           Button button;
           public AsyncForm()
           {
              label = new Label { Location = new Point(10, Text = "Length" };
              button = new Button { Location = new Point(10, 50), Text = "Click" };
              button.Click += DisplayWebSiteLength;
              AutoSize = true;
              Controls.Add(label);
              Controls.Add(button);
           }
           async void DisplayWebSiteLength(object sender, EventArgs e)
           {
              label.Text = "Fetching...";
              using (HttpClient client = new HttpClient())
              {
                 string text = await client.GetStringAsync("http://csharpindepth.com");
                 label.Text = text.Length.ToString();
              }
           }
        }
        
      • 再來改寫一下DisplayWebSiteLength方法,使用task類別來接值,如下範例,task在await回傳單純就是string
      • async void DisplayWebSiteLength(object sender, EventArgs e)
        {
            label.Text = "Fetching...";
            using (HttpClient client = new HttpClient())
            {
                Task<string> task = client.GetStringAsync("http://csharpindepth.com");
                string text = await task;
                label.Text = text.Length.ToString();
            }
        }
        
    • 非同步的思考方式
      • 說明了非同步在程式上的執行順序是什麼,這邊基本上就是思考有多執行緒在執行其他段落
      • 非同步的方法寫法,如下範例,GetPageLengthAsync是非同步方法,呼叫方法及非同步方法中間的分隔是Task<int>,非同步方法及非同步的運算式的分隔是Task<string>
      • static async Task<int> GetPageLengthAsync(string url)
        {
           using (HttpClient client = new HttpClient())
           {
              Task<string> fetchTextTask = client.GetStringAsync(url);
              int length = (await fetchTextTask).Length;
              return length;
           } 
        }
        static void PrintPageLength()
        {
           Task<int> lengthTask = GetPageLengthAsync("http://csharpindepth.com");
           Console.WriteLine(lengthTask.Result);
        }
        
    • 語法和語意
      • 宣告一個非同步方法時,只要在回傳型別前面任何地方加上async就可以,為了一致性統一放在回傳型別的前一個位置
      • async的回傳型別有3種
        1. void
        2. Task
        3. Task<TResult>
      • await基本上就是放在運算式前面
      • 流暢的await運算式
        1. 來看稍微複雜一點的await,下面兩個範例的結果是一樣的
        2. //不透過Task直接取值
          string pageText = await new HttpClient().GetStringAsync(url);
          //透過Task來暫存值
          Task task = new HttpClient().GetStringAsync(url); 
          string pageText = await task;
          
        3. 有一種狀況有潛在的效能問題,如下範例,這部分要考慮是否使用並行的方式來處理
        4. AddPayment(await employee.GetHourlyRateAsync() *
                     await timeSheet.GetHoursWorkedAsync(employee.Id));
          
      • 例外處理
        1. 例外的部分原則上會拋出AggregateException,要注意到的是如有多個非同步執行的例外,他只會返回一個,如要接回多個例外可寫擴充方法,如下範例
        2. public static AggregatedExceptionAwaitable WithAggregatedExceptions(this Task task)
          {
              return new AggregatedExceptionAwaitable(task);
          }
          
          // In AggregatedExceptionAwaitable
          public AggregatedExceptionAwaiter GetAwaiter()
          {
              return new AggregatedExceptionAwaiter(task);
          }
          
          // In AggregatedExceptionAwaiter
          public bool IsCompleted
          {
              get { return task.GetAwaiter().IsCompleted; }   //❶ 委托给任务awaiter
          }
          
          public void OnCompleted(Action continuation)
          {
              task.GetAwaiter().OnCompleted(continuation);   //❶ 委托给任务awaiter
          }
          
          public void GetResult()
          {
              task.Wait();   //❷ 发生错误时,直接抛出AggregateException
          }
          
          private async static Task CatchMultipleExceptions()
          {
              Task task1 = Task.Run(() => { throw new Exception("Message 1"); });
              Task task2 = Task.Run(() => { throw new Exception("Message 2"); });
              try
              {
                  await Task.WhenAll(task1, task2).WithAggregatedExceptions();
              }
              catch (AggregateException e)
              {
                  Console.WriteLine("Caught {0} exceptions: {1}",
                      e.InnerExceptions.Count,
                      string.Join(", ",
                          e.InnerExceptions.Select(x => x.Message)));
              }
          }
          
        3. 任務本身也可以做取消,任務本身有一個狀態的屬性可以查詢,參考下面範例,另外還有一些狀況在遇到問題時可再了解例外更進階的處理
        4. static async Task DelayFor30Seconds(CancellationToken token)
          {
              Console.WriteLine("Waiting for 30 seconds...");
              await Task.Delay(TimeSpan.FromSeconds(30), token);   //❶ 启动一个异步的延迟操作
          }
          ...
          var source = new CancellationTokenSource();
          var task = DelayFor30Seconds(source.Token);   //❷ 调用异步方法
          source.CancelAfter(TimeSpan.FromSeconds(1));
          Console.WriteLine("Initial status: {0}", task.Status);   //❸ 请求延迟的token取消操作
          try
          {
              task.Wait();   //❹ 等待完成(同步)
          }
          catch (AggregateException e)
          {
              Console.WriteLine("Caught {0}", e.InnerExceptions[0]);
          }
          Console.WriteLine("Final status: {0}", task.Status);   //❺ 显示任务状态
          //
          //結果如下
          Waiting for 30 seconds...
          Initial status: WaitingForActivation
          Caught System.Threading.Tasks.TaskCanceledException: A task was canceled.
          Final status: Canceled
          
    • 非同步的匿名函式,基本上是一樣的只要在前面加上async,直接看個範例就清楚了
    • Funcint<int, Task<int>> function = async x =>
      {
          Console.WriteLine("Starting... x={0}", x);
          await Task.Delay(x * 1000);
          Console.WriteLine("Finished... x={0}", x);
          return x * 2;
      };
      Task<int> first = function(5);
      Task<int> second = function(3);
      Console.WriteLine("First result: {0}", first.Result);
      Console.WriteLine("Second result: {0}", second.Result);
      //
      //會得到如下結果,要注意到的是Result會等到阻塞線程直到任務結束
      Starting... x=5
      Starting... x=3
      Finished... x=3
      Finished... x=5
      First result: 10
      Second result: 6
      
    •  編譯器的轉換,這部分牽扯到細部的講解,未來有意要深入了解時再回頭來看
    •  高效的使用async/await
      • 基於任務的非同步模式,在C#5 微軟為此定義一套標準TAP(Task base Asynchronous Pattern),完整的說明在這MSDN,作者建議要熟練的話把他看過一次會有更深的了解,以下他就說明他覺得最重要的部分
        1. 非同步方法命名的方式因怕跟原生的類別的方法衝突,建議方法名稱後綴加入TaskAsync,比如DownloadStringTaskAsync
        2. 一般來說非同步方法回傳的是Task或Task<T>,建立非同步方法時應考慮提供4種覆寫方法具有相同的基本參數,如下範例,IProgress<int>是用於進度報告
        3. //比如我們要建立下面這個方法
          Employee LoadEmployeeById(string id)
          Task<Employee> LoadEmployeeById(string id)
          Task<Employee> LoadEmployeeById(string id, CancellationToken cancellationToken)
          Task<Employee> LoadEmployeeById(string id, IProgress<int> progress)
          Task<Employee> LoadEmployeeById(string id,
              CancellationToken cancellationToken, IProgress<int> progress)
          
        4. 另外有一種方法可以不會去影響到Context,就是使用ConfigureAwait的屬性,他的參數只有一個continueOnCaptureContext(true/false),ture為正常等待會在同一個執行緒裡,false則忽略會在執行緒池裡,如以下範例是忽略上下文的部分
      • 組合非同步的操作
        1. 在單個調用中收集結果,如下範例,啟動多個請求,TPL提供了Task.WhenAll提供將各有一個結果的多個任務組合成一個包含多個結果的任務
        2. var tasks = urls.Select(async url =>
          {
              using (var client = new HttpClient())
              {
                  return await client.GetStringAsync(url);
              }
          }).ToList();
          //
          string[] results = await Task.WhenAll(tasks);
          
        3. 在全部完成時收集結果,看的不是很了他想表達什麼,這等我收集多一點的經驗及文章時再來了解
      • 對非同步程式碼寫單元測試,這部分有需要寫的時候再回頭看
  2. C# 5.0附加特性和結束語
    • foreach循環中捕捉變數的變化,在C# 3、4以下範例會輸出3個z,這總是讓人覺得很奇怪,但這也不是語言的錯誤而是它的特性,到了C# 5它會個別存變數值,所以會正常顯示x、y、z
      string[] values = { "x", "y", "z" };
      var actions = new ListAction();
      foreach (string value in values)
      {
         actions.Add(() = Console.WriteLine(value));
      }
      foreach (Action action in actions)
      {
         action(); 
      }
      
    • 調用者的訊息特性,這部分提到了3種方法來取得調用者的資訊,CallerFilePathAttribute、CallerLineNumberAttribute、CallerMemberNameAttribute,簡單看一下範例
      static void ShowInfo([CallerFilePath] string file = null,
                           [CallerLineNumber] int line = 0,
                           [CallerMemberName] string member = null)
      {
         Console.WriteLine("{0}:{1} - {2}", file, line, member);
      }
      ShowInfo();
      ShowInfo("LiesAndDamnedLies.java", -10);
      //結果
      c:\Users\Jon\Code\Chapter16\CallerInfoDemo.cs:21 - Main 
      LiesAndDamnedLies.java:-10 - Main