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 類型轉送 技術,在執行階段,動態的進行使用真正實作的組件。

參考資料

C# : is (C# 7.0 類型模式) 與 as 的使用練習

我們宣告了三個類別 ClassA 是最上層的基底類別,Class B 則是繼承了 ClassA,ClassC 繼承了 ClassC。
ClassA -> ClassB -> ClasssC
這三個類別,都有分別覆寫 ToString 這個方法,他們的建立程式碼如下所示。
class ClassA : ICloneable
{
    public object Clone()
    {
        return this.MemberwiseClone();
    }

    public override string ToString()
    {
        return "It is ClassA";
    }
}

class ClassB : ClassA
{
    public override string ToString()
    {
        return "It is ClassB";
    }
}

class ClassC : ClassB
{
    public override string ToString()
    {
        return "It is ClassC";
    }
}

as 的用法

從微軟官方的說明文件中,我們可以知道
使用 as 運算子,以在相容的參考型別或可為 Null 的型別之間執行特定轉換類型
在這裡,我們建立起六個物件,分別指定到型別為 object 的陣列中。
接著,我們使用 as 運算子,將執行時期的物件型別,強制轉型成為 string 這個型別,若當時物件的實際產生的型別不是 string,則,轉型出來的內容將會是空值 null。
object[] objArray = new object[6];
objArray[0] = new ClassA();
objArray[1] = new ClassB();
objArray[2] = "hello";
objArray[3] = 123;
objArray[4] = 123.4;
objArray[5] = null;

for (int i = 0; i < objArray.Length; ++i)
{
    // 在這裡進行型別轉換
    string s = objArray[i] as string;
    Console.Write("{0}:", i);
    if (s != null)
    {
        Console.WriteLine("'" + s + "'");
    }
    else
    {
        Console.WriteLine("not a string");
    }
}
底下是輸出結果
0:not a string
1:not a string
2:'hello'
3:not a string
4:not a string
5:not a string
Press any key for continuing...

is 的用法

從微軟官方的說明文件中,我們可以知道
is 運算子用於檢查物件是否與指定的類型相容
在這裡,我們分別建立 ClassA, ClassB, ClassC 這個三型別物件,在這三個物件,接著使用 is 運算子來檢驗是否與我們指定的型別運算之後,回得到 true 的回傳值。
在這裡,我們看到了,只要執行時期所產生的物件搭配 is 運算子運算之後,只要檢驗的型別,是當時物件的型別、繼承的型別、有繼承的介面,驗證結果皆會為真。
var cl1 = new ClassA();
Console.WriteLine($"cl1 is IFormatProvider : {cl1 is IFormatProvider}");
Console.WriteLine($"cl1 is ICloneable : {cl1 is ICloneable}");
Console.WriteLine($"cl1 is Object : {cl1 is Object}");
Console.WriteLine($"cl1 is ClassA : {cl1 is ClassA}");
Console.WriteLine($"cl1 is ClassB : {cl1 is ClassB}");
Console.WriteLine($"cl1 is ClassC : {cl1 is ClassC}");
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();

var cl2 = new ClassB();
Console.WriteLine($"cl2 is IFormatProvider : {cl2 is IFormatProvider}");
Console.WriteLine($"cl2 is ICloneable : {cl2 is ICloneable}");
Console.WriteLine($"cl2 is Object : {cl2 is Object}");
Console.WriteLine($"cl2 is ClassA : {cl2 is ClassA}");
Console.WriteLine($"cl2 is ClassB : {cl2 is ClassB}");
Console.WriteLine($"cl2 is ClassC : {cl2 is ClassC}");
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();

var cl3 = new ClassC();
Console.WriteLine($"cl3 is IFormatProvider : {cl3 is IFormatProvider}");
Console.WriteLine($"cl3 is ICloneable : {cl3 is ICloneable}");
Console.WriteLine($"cl3 is Object : {cl3 is Object}");
Console.WriteLine($"cl3 is ClassA : {cl3 is ClassA}");
Console.WriteLine($"cl3 is ClassB : {cl3 is ClassB}");
Console.WriteLine($"cl3 is ClassC : {cl3 is ClassC}");
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();
底下是輸出結果
cl1 is IFormatProvider : False
cl1 is ICloneable : True
cl1 is Object : True
cl1 is ClassA : True
cl1 is ClassB : False
cl1 is ClassC : False
Press any key for continuing...

cl2 is IFormatProvider : False
cl2 is ICloneable : True
cl2 is Object : True
cl2 is ClassA : True
cl2 is ClassB : True
cl2 is ClassC : False
Press any key for continuing...

cl3 is IFormatProvider : False
cl3 is ICloneable : True
cl3 is Object : True
cl3 is ClassA : True
cl3 is ClassB : True
cl3 is ClassC : True
Press any key for continuing...
我們在進行另外一個測試,那就是若產生的物件為一個衍生型別,而宣告的持有物件變數的型別為基底型別,這樣使用 is 運算子的結果會如何呢?
從底下的執行結果輸出內容可以清楚的觀察到, is 運算子會使用執行時期實際產生的物件型別,來進行型別檢驗。
Console.WriteLine($"ClassA clxl = cl2;");
ClassA clx1 = cl2;
Console.WriteLine($"ClassB clx2 = cl3;");
ClassB clx2 = cl3;
Console.WriteLine($"ClassA clx3 = cl3;");
ClassA clx3 = cl3;
Console.WriteLine($"{Environment.NewLine}");

Console.WriteLine($"clx1 is ClassA : {clx1 is ClassA}");
Console.WriteLine($"clx1 is ClassB : {clx1 is ClassB}");
Console.WriteLine($"clx1 is ClassC : {clx1 is ClassC}");
Console.WriteLine($"clx2 is ClassA : {clx2 is ClassA}");
Console.WriteLine($"clx2 is ClassB : {clx2 is ClassB}");
Console.WriteLine($"clx2 is ClassC : {clx2 is ClassC}");
Console.WriteLine($"clx3 is ClassA : {clx3 is ClassA}");
Console.WriteLine($"clx3 is ClassB : {clx3 is ClassB}");
Console.WriteLine($"clx3 is ClassC : {clx3 is ClassC}");
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();
底下是輸出結果
ClassA clxl = cl2;
ClassB clx2 = cl3;
ClassA clx3 = cl3;

clx1 is ClassA : True
clx1 is ClassB : True
clx1 is ClassC : False
clx2 is ClassA : True
clx2 is ClassB : True
clx2 is ClassC : True
clx3 is ClassA : True
clx3 is ClassB : True
clx3 is ClassC : True
Press any key for continuing...

類型模式執行模式比對

從微軟的官方文件中,可以得到
從 C# 7 開始,您可以搭配類型模式使用模式比對來撰寫更簡潔的程式碼,以使用 is 陳述式。
我們在這裡來使用看看,他的用法如下:
expr is type varname
例如,在底下的測試程式碼中,使用的 MyTest is ClassC e1 就是這樣的用法;若使用類型模式執行模式比對得到為否,e1 這個變數便會為空值 null。
var cl1 = new ClassA();
var cl2 = new ClassB();
var cl3 = new ClassC();

object MyTest;

// 用類型模式執行模式比對時,is 會測試運算式是否可轉換成指定的類型;如果可以的話,則會將它轉換成該類型的變數。 它是 is 陳述式的直接延伸,允許精簡類型的評估和轉換。
// https://docs.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/is#type
MyTest = cl2;
if (MyTest is ClassC e1)
{
    Console.WriteLine($"cl2 is ClassC : {e1.ToString()}");
}
if (MyTest is ClassA e2)
{
    Console.WriteLine($"cl2 is ClassA : {e2.ToString()}");
}
Console.WriteLine($"Press any key for continuing...{Environment.NewLine}");
Console.ReadKey();
底下是輸出結果
cl2 is ClassA : It is ClassB
Press any key for continuing...

Press any key for continuing...