2017年9月28日 星期四

C# : 使用多執行緒 Thread 讀取多網站資料

當你進行 C# 程式開發的時候,同一個時間,只會有一個執行緒來幫助您執行程式,例如,當我們想要讀取四個網址的內容的時候,我們會寫類似如下的程式碼;因此,當我們執行方法 SequenceThreads() 的時候,就會依序執行方法內的程式碼,所以,當第一個 GetWebContent 方法執行完成之後,才會繼續執行下一個 GetWebContent 方法,直到四個網站資料都讀取完成。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [HttpClient Class] 的使用方式



在進行抓取網站資料的時候,我們會呼叫方法 GetWebContent,這裡會使用 Thread.CurrentThread.ManagedThreadId 顯示出當時正在執行的受管理的執行緒ID代碼,讓你清楚現在的是使用哪個執行緒來執行;所以,再進行非多工模式的抓取網頁資料的測試程式碼的時候,所看到的受管理的執行緒ID,都是一樣的。
雖然,我們使用 HttpClient 類別的 client.GetStringAsync(url.ToString()).Result 表示式來取得特定網頁上的資料,不過,因為使用了 Result 屬性,所以,這行表示式是採用同步的方式執行的,不是非同步的方式。
    static string[] Urls = new string[]
    {
        "https://www.microsoft.com",
        "https://tw.yahoo.com/",
        "http://www.msn.com/zh-tw/",
        "https://world.taobao.com/"
    };

    private static void SequenceThreads()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        GetWebContent(Urls[0]);
        GetWebContent(Urls[1]);
        GetWebContent(Urls[2]);
        GetWebContent(Urls[3]);
        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void GetWebContent(object url)
    {
        Console.WriteLine($"{url} 執行緒 ID {Thread.CurrentThread.ManagedThreadId}");

        Stopwatch sw = new Stopwatch();
        sw.Start();
        using (var client = new HttpClient())
        {
            string content = client.GetStringAsync(url.ToString()).Result;
            Console.WriteLine($"{url} 的內容大小為 {content.Length} bytes");
        }
        sw.Stop();
        Console.WriteLine($"{url} 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
    }
我們從這個測試過程的輸出內容,可以看到全部抓取四個網頁完成所需要的時間,為這四個抓取網頁的時間總和。
底下是執行結果輸出內容
https://www.microsoft.com 執行緒 ID 1
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 271.7432 ms
https://tw.yahoo.com/ 執行緒 ID 1
https://tw.yahoo.com/ 的內容大小為 194903 bytes
https://tw.yahoo.com/ 花費時間為 141.2671 ms
http://www.msn.com/zh-tw/ 執行緒 ID 1
http://www.msn.com/zh-tw/ 的內容大小為 45733 bytes
http://www.msn.com/zh-tw/ 花費時間為 161.3392 ms
https://world.taobao.com/ 執行緒 ID 1
https://world.taobao.com/ 的內容大小為 220949 bytes
https://world.taobao.com/ 花費時間為 314.1775 ms
抓取四個網站內容 花費時間為 921.3975 ms
Press any key for continuing...
現在,讓我們使用多執行緒的方式來同時抓取這些網頁的資料,在此,我們建立了四個執行緒 Thread 物件,並且在建構函是傳遞了 ParameterizedThreadStart 類別的物件;而這個 ParameterizedThreadStart型別其實是個委派型別 public delegate void ParameterizedThreadStart(object obj);,也就是說,我們要建立的執行緒物件,需要一個委派方法,如此,這個執行緒將會多工執行這個方法。
在這裡,我們在要開始進行多執行緒運作的時候,使用 Thread.CurrentThread.ManagedThreadId 屬性,顯示出這個應用程式的主執行緒 ID。
為了要能夠量測出同時抓取四個網頁的總共花費時間,我們使用了執行緒的 Join 方法,這是微軟官方的說明 封鎖呼叫執行緒,直到此執行個體所代表的執行緒終止為止
    private static void MultiThreads()
    {
        Thread thread1 = new Thread(GetWebContent);
        Thread thread2 = new Thread(GetWebContent);
        Thread thread3 = new Thread(GetWebContent);
        Thread thread4 = new Thread(GetWebContent);

        Console.WriteLine($"主執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"啟動執行緒");
        Stopwatch sw = new Stopwatch();
        sw.Start();
        thread1.Start(Urls[0]);
        thread2.Start(Urls[1]);
        thread3.Start(Urls[2]);
        thread4.Start(Urls[3]);

        thread1.Join();
        thread2.Join();
        thread3.Join();
        thread4.Join();

        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }
從執行結果可以看的出來,同時抓取四個網頁所需要花費的總共時間,大約為抓取某個網頁花費最多的時間,而不是累計總和時間。
底下是執行結果輸出內容
主執行緒 ID 1
啟動執行緒
https://world.taobao.com/ 執行緒 ID 13
https://tw.yahoo.com/ 執行緒 ID 11
http://www.msn.com/zh-tw/ 執行緒 ID 12
https://www.microsoft.com 執行緒 ID 10
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 47.7937 ms
https://world.taobao.com/ 的內容大小為 220949 bytes
https://world.taobao.com/ 花費時間為 77.1401 ms
http://www.msn.com/zh-tw/ 的內容大小為 45597 bytes
http://www.msn.com/zh-tw/ 花費時間為 126.1487 ms
https://tw.yahoo.com/ 的內容大小為 195623 bytes
https://tw.yahoo.com/ 花費時間為 946.1321 ms
抓取四個網站內容 花費時間為 973.0811 ms
Press any key for continuing...
最後,我們一樣要進行多工的抓取網頁工作,只不過,我們在這裡使用 執行緒的集區 Thread Pool 來處理這樣需求,微軟官方對於 執行緒的集區 的定義為:提供執行緒的集區,可用來執行工作、張貼工作項目、處理非同步 I/O、代表其他執行緒等候,以及處理計時器,ThreadPool 類別為您的應用程式提供了受到系統管理的背景工作執行緒集區,讓您專注於應用程式工作上,而不是執行緒的管理。 如果您有需要在背景處理的簡短工作,Managed 執行緒集區是利用多重執行緒的一個簡單方式。
ThreadPool類別的 QueueUserWorkItem 靜態方法,可以接受兩個引數,第一個為委派類型的 WaitCallback (他的委派宣告為 public delegate void WaitCallback(Object state);),另外一個是要傳入到 WaitCallback 委派函式內的方法引數。
執行緒集區執行緒為背景執行緒
    private static void MultiThreadPool()
    {
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[0]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[1]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[2]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[3]);
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }
底下是執行結果輸出內容
https://www.microsoft.com 執行緒 ID 16
https://world.taobao.com/ 執行緒 ID 22
http://www.msn.com/zh-tw/ 執行緒 ID 18
https://tw.yahoo.com/ 執行緒 ID 15
https://www.microsoft.com 的內容大小為 1020 bytes
https://www.microsoft.com 花費時間為 151.1066 ms
http://www.msn.com/zh-tw/ 的內容大小為 46023 bytes
http://www.msn.com/zh-tw/ 花費時間為 160.9714 ms
https://world.taobao.com/ 的內容大小為 220949 bytes
https://world.taobao.com/ 花費時間為 225.9449 ms
https://tw.yahoo.com/ 的內容大小為 191718 bytes
https://tw.yahoo.com/ 花費時間為 816.9125 ms

底下是完整的測試程式碼
class Program
{
    static string[] Urls = new string[]
    {
        "https://www.microsoft.com",
        "https://tw.yahoo.com/",
        "http://www.msn.com/zh-tw/",
        "https://world.taobao.com/"
    };
    static void Main(string[] args)
    {
        SequenceThreads();

        MultiThreads();

        MultiThreadPool();
    }

    private static void MultiThreadPool()
    {
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[0]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[1]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[2]);
        ThreadPool.QueueUserWorkItem(GetWebContent, Urls[3]);
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void SequenceThreads()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        GetWebContent(Urls[0]);
        GetWebContent(Urls[1]);
        GetWebContent(Urls[2]);
        GetWebContent(Urls[3]);
        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void MultiThreads()
    {
        Thread thread1 = new Thread(GetWebContent);
        Thread thread2 = new Thread(GetWebContent);
        Thread thread3 = new Thread(GetWebContent);
        Thread thread4 = new Thread(GetWebContent);

        Console.WriteLine($"主執行緒 ID {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine($"啟動執行緒");
        Stopwatch sw = new Stopwatch();
        sw.Start();
        thread1.Start(Urls[0]);
        thread2.Start(Urls[1]);
        thread3.Start(Urls[2]);
        thread4.Start(Urls[3]);

        thread1.Join();
        thread2.Join();
        thread3.Join();
        thread4.Join();

        sw.Stop();
        Console.WriteLine($"抓取四個網站內容 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
        Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
        Console.ReadKey();
    }

    private static void GetWebContent(object url)
    {
        Console.WriteLine($"{url} 執行緒 ID {Thread.CurrentThread.ManagedThreadId}");

        Stopwatch sw = new Stopwatch();
        sw.Start();
        using (var client = new HttpClient())
        {
            string content = client.GetStringAsync(url.ToString()).Result;
            Console.WriteLine($"{url} 的內容大小為 {content.Length} bytes");
        }
        sw.Stop();
        Console.WriteLine($"{url} 花費時間為 {sw.Elapsed.TotalMilliseconds} ms");
    }
}

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [HttpClient Class] 的使用方式






2017年9月27日 星期三

C# : using 陳述式使用練習

根據微軟官方的文件說明
提供方便的語法,以確保正確使用 IDisposable 物件
對於有使用到非受管理記憶體或者資源的類別,他們通常都會實作 IDisposable 介面,當我們不再需要使用到這些類別所產生的物件的時候,建議手動執行這些物件的 Dispose() 方法,讓這些非受到管理的記憶體與資源可以受到釋放,並且也縮短這個物件停留在受管理執行階段的記憶體存留時間。
對於像是有實作 IDisposable 介面的物件,我們可以使用 using 陳述式,這個語法糖,幫助我們簡化與確實確認在該物件不再使用的時候(不論在執行階段是否有例外異常發生),都會執行 Dispose() 方法。
在底下的程式碼,我們可以知道,HttpClient 有實作 IDisposable 介面,因此,在建立這個物件的當時,我們就使用 using 陳述式將其包起來;當這個區塊程式碼結束執行之後,就會立即執行 client.Dispose() 方法。
+

using (var client = new HttpClient())
{
    var content = client.GetStringAsync("http://www.google.com").Result;
    Console.WriteLine(content);
}

Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();

C# : 以明確方式實作介面 (Explicit Interface Implementation)練習

在這個練習中,我們使用不同的命名空間,區隔出同樣的名稱的兩個介面和要實作出這兩個介面類別的定義,在底下的程式碼,是沒有使用明介介面實作,而是使用了實作介面這個功能。

了解更多關於 [明確介面實作的使用方式
了解更多關於 [C# 程式設計手冊 



實作介面

當 Visual Studio 提示 IContron 介面沒有實作的提示時候,將游標移動到燈泡提示上,請選擇 實作介面,此時,在 SampleClass 類別內,就會自動幫我們產生出這個介面需要提供的方法定義;另外,因為,我們在這個類別中需要繼承兩個介面,不過,這兩個介面都只需要實作出同一個方法,因此,當我們在類別中實作出 public void Paint() {...} 方法之後,就同時滿足了這兩個介面的需求。
實作介面
namespace 沒有明確實作介面
{ 
    interface IControl
    {
        void Paint();
    }
    interface ISurface
    {
        void Paint();
    }
    class SampleClass : IControl, ISurface
    {
        // Both ISurface.Paint and IControl.Paint call this method. 
        public void Paint()
        {
            Console.WriteLine("要執行的是 SampleClass 實作的 Paint 方法");
        }
    }
}

執行測試

我們在這裡建立一個型別為 沒有明確實作介面.SampleClass 的物件,指定給型別為 沒有明確實作介面.SampleClass 的物件變數內;接著分別將這個物件,轉型為兩個型別為 沒有明確實作介面.IControl 與 沒有明確實作介面.ISurface 的介面類型物件變數。
當我們執行後,我們發現到,不論是透過物件變數的型別為類型或者是介面的物件變數,所執行的 Paint()的方法都是指向這個類別中的實作方法。
沒有明確實作介面.SampleClass sc = new 沒有明確實作介面.SampleClass();
沒有明確實作介面.IControl ctrl = (沒有明確實作介面.IControl)sc;
沒有明確實作介面.ISurface srfc = (沒有明確實作介面.ISurface)sc;

sc.Paint();
ctrl.Paint();
srfc.Paint();
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();
這是執行後的輸出內容
要執行的是 SampleClass 實作的 Paint 方法
要執行的是 SampleClass 實作的 Paint 方法
要執行的是 SampleClass 實作的 Paint 方法
Press any key for continuing...

以明確方式實作介面

這是微軟官方文件,對於明確介面實作的定義
如果類別實作了兩個包含相同簽章之成員的介面,則在類別上實作該成員會導致兩個介面都將該成員當做實作 (Implementation) 使用
當 Visual Studio 提示 IContron 介面沒有實作的提示時候,將游標移動到燈泡提示上,請選擇 以明確方式實作介面
明確介面實作
底下為我們要測試的介面與類別宣告的程式原始碼,我們在類別 SampleClass內看到,這裡分別定義了這三個介面需要實作的方法
  • public void Paint()
  • void IControl.Paint()
  • void ISurface.Paint()
這三個實作方法將會輸出不同的文字,現在,讓我們透過實際測試的程式碼,看看這三個方法要如何使用呢?
namespace 有明確實作介面
{
    interface IControl
    {
        void Paint();
    }
    interface ISurface
    {
        void Paint();
    }
    class SampleClass : IControl, ISurface
    {
        public void Paint()
        {
            Console.WriteLine("要執行的是 SampleClass 實作的 Paint 方法");
        }
        void IControl.Paint()
        {
            Console.WriteLine("要執行的是 IControl 介面明確實作的 Paint 方法");
        }

        void ISurface.Paint()
        {
            Console.WriteLine("要執行的是 ISurface 介面明確實作的 Paint 方法");
        }
    }
}

執行測試

我們在這裡建立一個型別為 有明確實作介面.SampleClass 的物件,指定給型別為 有明確實作介面.SampleClass 的物件變數內;接著分別將這個物件,轉型為兩個型別為 有明確實作介面.IControl 與 有明確實作介面.ISurface 的介面類型物件變數。
當我們執行後,我們發現到,若該物件變數的宣告型別為該類別,而且所指向的實際物件型別也是該類別,則呼叫 Paint() 的方法,將會執行在該類別中的 public void Paint() 法。
不過,當我們將 new 有明確實作介面.SampleClass() 所產生的物件,轉型成為其他介面的時候,並且執行這個介面的 Paint() 的方法,我們會發現,此時,會依據當時轉型成功的介面類型,執行 void IControl.Paint() 或者 void ISurface.Paint() 方法。
有明確實作介面.SampleClass sc2 = new 有明確實作介面.SampleClass();
有明確實作介面.IControl ctrl2 = (有明確實作介面.IControl)sc2;
有明確實作介面.ISurface srfc2 = (有明確實作介面.ISurface)sc2;

sc2.Paint();
ctrl2.Paint();
srfc2.Paint();
((有明確實作介面.IControl)sc2).Paint();
((有明確實作介面.ISurface)sc2).Paint();
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();

底下內容為執行後的輸出文字
要執行的是 SampleClass 實作的 Paint 方法
要執行的是 IControl 介面明確實作的 Paint 方法
要執行的是 ISurface 介面明確實作的 Paint 方法
要執行的是 IControl 介面明確實作的 Paint 方法
要執行的是 ISurface 介面明確實作的 Paint 方法
Press any key for continuing...

了解更多關於 [明確介面實作的使用方式
了解更多關於 [C# 程式設計手冊 






2017年9月26日 星期二

C# : 購物車多樣付款機制(使用介面 Interface 來抽象化需求)

在這裡,將說明一個大家經常會遇到的一個情境,那就是在開發專案的時候,隨著時間變化,需求也有了變化,因此,我們要如何針對需求來調整我們的類別設計呢?
例如,你開發了一個購物網站,僅僅提供了兩種付款機制,例如:銀行轉帳、信用卡付款,不過,此時,客戶希望能夠追加某家的第三方支付系統,到我們家的專案內,讓客戶進行付款的時候,有著更多樣性的選擇。
之前,公司同事面臨到這樣的情境,若想要追加第二家的第三方支付系統到我們家的專案內,這樣又該如何處理呢?
在這裡,我們需要將這些付款機制抽象化出來,使用介面 Interface 制定出這些付款機制需要用到的相關屬性與方法等成員 Member;之後,就可以在不同的具體類別中,實作這個介面,在這些具體實作類別中,會根據每個付款機制進行不同的處理流程。
當這個專案系統的使用者需要進行付款作業的時候,我們在程式裡面,僅需要針對這個介面物件變數進行處理,而實際的具體實作物件,會根據使用者選擇的機制,實際產生或者注入到我們的系統內。
現在,讓我們來看看這個練習的類別與介面宣告。我們宣告一個介面 I付款方法,他如同一個合約,每個要進行付款動作的類別,都需要實作出這個介面 I付款方法。而這個介面裡面,僅有宣告一個方法 void 付款(int Money);
另外,我們建立了三個類別,轉帳付款 / 信用卡付款 / 第三方支付付款,這三個類別都有實作出 I付款方法這個介面合約內容,也就是都有自行設計出每個付款機制的 付款 方法。
public interface I付款方法
{
    void 付款(int Money);
}

public class 轉帳付款 : I付款方法
{
    public void 付款(int Money)
    {
        Console.WriteLine($"正在使用 轉帳方式 進行付款 ${Money}");
    }
}

public class 信用卡付款 : I付款方法
{
    public void 付款(int Money)
    {
        Console.WriteLine($"正在使用 信用卡方式 進行付款 ${Money}");
    }
}

public class 第三方支付付款 : I付款方法
{
    public void 付款(int Money)
    {
        Console.WriteLine($"正在使用 第三方支付方式 進行付款 ${Money}");
    }
}

進行測試

現在讓我們來進行完成類似購物車的付款機制,在實際進行專案設計的時候,我們需要宣告一個物件變數,他的型別為 I付款方法,而我們針對這個物件變數會依據當時的需求,實例化所需要型別的執行個體;這裡,你可以選擇使用不同的設計模式 (Design Pattern) ,例如 Service, Factory, 或者 相依性注入 Dependency Injection等模式來幫助你有系統的設計這樣的需求。
不過,在這裡,我們簡化這樣的需求,我們直接建立這三個具體類別的物件到 I付款方法 型別變數內。
接著,我們就可以直接使用 I付款方法 物件變數的 付款 方法,進行付款動作。
由執行結果,我們可以看到,雖然物件變數的型別都是 I付款方法,不過,實際執行付款的方法,卻是相對應的具體實作的類別內定義的方法。
I付款方法 foo轉帳付款付款 = new 轉帳付款();
I付款方法 foo信用卡付款 = new 信用卡付款();
I付款方法 foo第三方支付付款 = new 第三方支付付款();

foo轉帳付款付款.付款(111);
foo信用卡付款.付款(222);
foo第三方支付付款.付款(333);
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();
這是執行後的結果輸出內容
正在使用 轉帳方式 進行付款 $111
正在使用 信用卡方式 進行付款 $222
正在使用 第三方支付方式 進行付款 $333
Press any key for continuing...

2017年9月25日 星期一

.NET Framework / PCL 可攜式類別庫 / .NET Standard 標準類別庫 之中繼套件與類型轉送 深入探究

在這裡,我們透過 IL 中繼語言的反組譯工具 ILSpy 來查看這三個 .NET 生態環境的內容。

.NET Framework

我們使用 ILSpy 工具,打開 C:\Windows\Microsoft.NET\Framework\v4.0.30319 目錄,找到 mscorlib.dll這個檔案
接著展開其 mscorlib 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。

PCL 可攜式類別庫

在這裡,我們使用 PCL Profile259 這個版本
我們使用 ILSpy 工具,打開 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile259\ 目錄,這裡就是 Profile 259 會使用到的類別庫組件所在位置,找到 System.Collections.dll 這個檔案
這個位置,可以從 Visual Studio 中,打開任意一個 PCL 可攜式專案,點選 參考 > .NET 節點,從屬性視窗的 路徑 中,就可以查到,如下圖所示
接著展開其 System.Collections 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。
不過,在這裡,似乎你只看到了這個 List 類別的成員定義,而成員的方法似乎都沒有實做出來。
讓我們繼續使用 ILSpy 工具,打開 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile259\ 目錄,這裡就是 Profile 259 會使用到的類別庫組件所在位置,找到 mscorlib.dll 這個檔案
當你點選剛剛開啟的 mscorlib.dll 的 mscorlib 節點,從右邊分割視窗中,您會看到這個敘述 [assembly: TypeForwardedTo(typeof(List<>))] 這表示,在執行時期,若你的專案中有使用這個 List 的泛型型別,則真正實作的組件,將不會在這個組件內定義,而是使用了 Type Forwarding 類型轉送 技術,需要到其他的組件上來找到這個類別的實作定義。根據微軟官方文件上的描述:型別轉送可讓您將某種型別移到其他組件,而不需重新編譯使用原始組件的應用程式

.NET Standard 標準類別庫

在這裡,請先下載 NETStandard.Library NuGet 套件到本機上,使用 zip 解壓縮工具,就可以解開這個 netstandard.library.2.0.0.nupkg 壓縮檔案。
當你在 Visual Studio 內,打開任意 .NET Standard 標準類別庫,就會看到如下圖
.NET Standard 標準類別庫
我們使用 ILSpy 工具,打開解壓縮 (netstandard.library.2.0.0.nupkg 壓縮檔案) 後的目錄 netstandard.library.2.0.0.nupkg\build\netstandard2.0\ref ,這裡就是 NETStandard.Library 會使用到的類別庫組件所在位置,找到 netstandard.dll 這個檔案
接著展開其 netstandard 節點,接著再展開 System.Collections.Generic 節點,就會看到 List<T> 節點,點擊這個節點,就會看到這個類別的原始 C# 原始程式碼。
不過,在這裡,似乎你只看到了這個 List 類別的成員定義,而成員的方法似乎都沒有實做出來。
讓我們繼續使用 ILSpy 工具,打開解壓縮 (netstandard.library.2.0.0.nupkg 壓縮檔案) 後的目錄 netstandard.library.2.0.0.nupkg\build\netstandard2.0\ref,這裡就是 NETStandard.Library 會使用到的類別庫組件所在位置,找到 mscorlib.dll 這個檔案
當你點選剛剛開啟的 mscorlib.dll 的 mscorlib 節點,從右邊分割視窗中,您會看到這個敘述 [assembly: TypeForwardedTo(typeof(List))] 這表示,在執行時期,若你的專案中有使用這個 List 的泛型型別,則真正實作的組件,將不會在這個組件內定義,而是使用了 Type Forwarding 類型轉送 技術,需要到其他的組件上來找到這個類別的實作定義。根據微軟官方文件上的描述:型別轉送可讓您將某種型別移到其他組件,而不需重新編譯使用原始組件的應用程式

總結

從上面的檢測過程,我們可以知道,不論 PCL 或者 .NET Standard 這兩個,他們使用的核心技術原則上是相同的,只不過對於中繼套件的使用方式與可以使用那些平台的 API 的規劃設定方式不同。
在 .NET Standard,中繼套件 (原始程式碼) 描述定義 (部分) 一個或多個 .NET 標準程式庫版本的程式庫集合,並且,以 NuGet 套件散發並由 NETStandard.Library 中繼套件參考的參考組件;而 PCL 的中繼套件則是存在於本機上的某個目錄中,這是隨著你的 Visual Studio 安裝的同時,也就會安裝進去的。
+

關於最後真正平台要使用各 API 實作,則是使用 Type Forwarding 類型轉送 技術,在執行階段,動態的進行使用真正實作的組件。

參考資料