習慣在 Windows + Visual Studio 進行開發工作的 .NET programmer 在一開始轉入 .NET Core 時最大的不解應該是,何謂 Command-Line Interface (CLI) 了? Photo by  hannah joshua  o...

習慣在 Windows + Visual Studio 進行開發工作的 .NET programmer 在一開始轉入 .NET Core 時最大的不解應該是,何謂 Command-Line Interface (CLI) 了?


Photo by hannah joshua on Unsplash


.NET Command-Line Interface (CLI)

.NET Command-Line interface (CLI) 基本上就是一個下指令的介面,這些指令可以執行 .NET Core 上的相關操作,例如 dotnet new mvc 可以建立一個 .NET Core MVC 的專案;另外 dotnet --info 則是列出目前環境的資訊,包含安裝的 .NET Core Runtime 列表等。


為什麼還要有 CLI 呢?基於 .NET Core 是一個跨平台的框架,所以 .NET CLI 也是跨平台的,表示同一個指令在不同平台上的結果是一樣的。.NET CLI 包含在 .NET Core SDK 裡面中,只要安裝好 .NET Core SDK 就可以使用 CLI 中的指令了。以下是 CLI 的一些相關資訊:


沒錯,.NET Core SDK 也是開源的。基本上,dotnet 這個指令就是一個 console 專案開發的。

Microsoft Docs 網站上針對 CLI 指令的詳細說明。


.NET Tools 

.NET Core 是允許我們開發自已的 Command-Line 指令,而這些在 Command-Line Interface 上使用的指令 (或工具),一般稱為 .NET Tools。 其實 .NET Tool 是一特殊的 NuGet package 裡面包含 Console Application,安裝後可透過 CLI 執行。


如果有興趣知道 .NET Tool 是如何開發的,建議可以在 Google 搜尋 custom dotnet tool 就會不少的參考資料。Microsoft Docs 網站上也有一篇 Tutorial: Create a .NET tool using the .NET CLI 範例說明如何建立一個 .NET tool。


.NET Tools 的安裝有兩種,一是安裝成 Global Tool;另一是 Local Tool。在說明安裝前先介紹一個 Microsoft 提供的 .NET Tool,叫 dotnetsay 。這個 .NET Tool 純粹是示範用途而己,不會執行什麼操作,就是在 Command 視窗上顯示一個圖。你可以在 nuget 網站上輸入 dotnetsay 就可以找到該 .NET Tool,也可以去 GitHub 上看看他的原始碼。


As a local tool

Local tool 是將指令安裝專案下的路徑,因此該指令只有在專案下是有效的。安裝指令如下:

dotnet tool install dotnetsay


As a global tool

Global tool 是將指令安裝系統指定的路徑,因該路徑已在環境中設好 PATH,因此安裝好的 global tool 就是一個全域工具。

dotnet tool install -g dotnetsay

全域的安裝就是加上 -g 這個參數即可,如果是在 Windows 的環境,預設的安裝路徑是 %USERPROFILE%\.dotnet\tools;如果是 Linux/MacOS 安裝路徑則是 $HOME/.dotnet/tools 。


~Keeping Coding; Keep Writing



Photo by Annie Spratt on Unsplash C# 開發人員應該都非常熟悉 System.String 這個型別,String 應該是在 .NET 裡很重要且經常被使用的型別了。依據 MSDN 上的解釋,String 是表示一組依順序存放的字元集合 ( a...


C# 開發人員應該都非常熟悉 System.String 這個型別,String 應該是在 .NET 裡很重要且經常被使用的型別了。依據 MSDN 上的解釋,String 是表示一組依順序存放的字元集合 (a sequential collection of characters),並且每個字元都是 UTF-16 的 Code Unit。


在 .NET 裡 String 提供了許多函式,如 Remove 或 Replace 等可以快速改變字串的值。另外 ToCharArray 函式則會回傳一個字元 Char 陣列,所以 String 字串在內部是以 Char 陣列的方式存放。雖然我們很常使用 System.String 型別,但其實 String 是一個很特殊的類別。


String Interning

String interning 幾乎是現代程式語言都會有的一種機制,是指同一個字串在記憶體中只會存放一份。同樣的在 .NET Common Language Runtime 裡,System.String 也是有 intern pool 的機制,讓同一個字串 (literal string) 只會保存在同一個記憶體區塊中,以提昇記憶體的使用效率。

string name1 = "Peter"; 
string name2 = "Peter"; 

Console.WriteLine("Reference of name1 & name2 are same: {0}", 
    Object.ReferenceEquals(name1, name2).ToString());
// Output: Reference of name1 & name2 are same: True

string name3 = "P";
name3 += "eter";

Console.WriteLine("Reference of name1 & name3 are same: {0}", 
    Object.ReferenceEquals(name1, name3).ToString());
// Output: Reference of name1 & name3 are same: False

首先宣告兩個字串變數並賦於相同的值,比較兩個變數的 ReferenceEquals 得到的結果是 True,表示兩個變數都指向同一個記憶體位置。


接下來宣告字串變數 name3 時我們用了字串相加,雖然其值都是一樣的,但比較 ReferenceEquals 的結果是 False,表示其記憶體位置不同。

Char[] chars = { 'P', 'e', 't', 'e', 'r' };
string name4 = new string(chars);

Console.WriteLine("Reference of name1 & name4 are same: {0}",
                Object.ReferenceEquals(name1, name4).ToString());
// Output: Reference of name1 & name4 are same: False

其實在 CLR 裡,只有 compile-time 時是 literal string 的字串才會在 string pool 上建立。以上面的範例,在建立字串變數 name4 時是傳入一個 char 陣列,雖然其值也是 Peter,但比較 ReferenceEquals 的結果也是 False,所以不是每一個 "Peter" 都會存放同一個記憶體位置。


那如果字串是透過外部輸入,例如在 Console 是透過 ReadLine 或透過 TextBox 取值呢?

Console.Write(@"Please enter ""Peter"": ");
string name5 = Console.ReadLine();

Console.WriteLine("Reference of name1 & name5 are same: {0}",
    Object.ReferenceEquals(name1, name5).ToString());
// Output: Reference of name1 & name5 are same: False

以上範例中字串變數 name5 的值是透過 Console.ReadLine 輸入,就算你輸入一模一樣的值,其 ReferenceEquals 的結果一樣是 False,因為在 compile-time 時並不是 literal string。


但如果我們希望就算是非 literal string 也可在 intern pool 裡指向同一個記憶體位置呢?在 .NET 裡 String 提供了一個 Intern 的靜態函式,會檢查 intern pool 裡是否己有該字串存在,如有則回傳同一個字串,其記憶體位置也會是一致的。

Console.Write(@"Please enter ""Peter"": ");
string name6 = String.Intern(Console.ReadLine());

Console.WriteLine("Reference of name1 & name6 are same: {0}", 
    Object.ReferenceEquals(name1, name6).ToString());
// Output: Reference of name1 & name7 are same: True

就以上範例,字串變數 name6 透過 Console.ReadLine 取後再呼叫 String.Intern 這個函式取得 intern pool 上的同一個字串,這時比較兩個變數的 ReferenceEquals 就會是 True 了,表示是同一個記憶體位置。


在 MSDN 上針對 string 的 intern pool 也有解釋,可以參考連結

The common language runtime conserves string storage by maintaining a table, called the intern pool, that contains a single reference to each unique literal string declared or created programmatically in your program. Consequently, an instance of a literal string with a particular value only exists once in the system.


另外,在微軟公開 .NET Framework 的 source code 裡,我們看到 String.Intern 這個靜態函式的原始程式碼如下:

public static String Intern(String str) {
    if (str==null) {
            throw new ArgumentNullException("str");
    }
    Contract.Ensures(Contract.Result<string>().Length == str.Length);
    Contract.Ensures(str.Equals(Contract.Result<string>()));
    Contract.EndContractBlock();
 
    return Thread.GetDomain().GetOrInternString(str);
}

我們看到,最後是透過 Thread.GetDomain 這個函式取回該執行緒的 AppDomain 物件,再呼叫 GetOrInternString 函式來取回 intern pool 裡的字串。


String 物件是不能改變的 (Immutable)

我們知道在 CLR 裡有 intern pool 的機制,讓同一字串只會保存一份。這種機制帶來了 System.String 的第二個特性,就是 String 物件是 immutable 的。在這裡 immutable 不可改變表示在 String 物件一經建立後,該記憶體位置的值則不能被改變,如果改變了該 string 物件,CLR 則會配置另一塊記憶體位置存放,原記憶體位置則交由 Garbage Collector 負責回收。

string name1 = "Peter";
string name2 = "Peter";

Console.WriteLine("name1 = {0}", name1);
Console.WriteLine("name2 = {0}", name2);
Console.WriteLine("name1 & name2 are same reference: {0}",
    Object.ReferenceEquals(name1, name2).ToString());
// Output: name1 & name2 are same reference: True

看看以上範例,字串變數 name1 及 name2 都宣告並賦於同一個字串值,一經建立後 CLR 就依據 intern pool 的機制存放在同一個記憶體位置,比較兩個字串變數的 ReferenceEquals,其結果也是 True。

name2 = name2.Remove(name2.Length - 1, 1);
name2 += "r";

Console.WriteLine("name1 = {0}", name1);
Console.WriteLine("name2 = {0}", name2);
Console.WriteLine("name1 & name2 are same reference: {0}",
    Object.ReferenceEquals(name1, name2).ToString());
// Output: name1 & name2 are same reference: False

接下來改變字串變數 name2 的值,先透過 Remove 函式去掉最後一個字元然後再加回去,雖然最終的值都一樣還是 "Peter",但其記憶體位置已經與字串變數 name1 不一樣了。比較兩個字串變數的 ReferenceEquals,其結果是 False 了。


其實依 intern pool 的機制來看,一開始字串變數 name1 及 name2 都指向同一個記憶體位置,如果變數 name2 要改變其值,CLR 必須要配置另外一塊記憶體區塊,要不然變數 name1 也會受到影響。這也解釋了為何許多書藉或文章都建議,如果針對 String 進行大量的串接 (concatenate) 時請使用 StringBuilder。


另外在 CLR 裡,String 是一組連續存放在 Char 陣列的文字,而 Char 陣列在 CLR 裡是唯讀的,陣列的長度在建立時就是固定了,當變更 Char 陣列時,CLR 是另配置一個記憶體區塊,這也是另一個原因為什麼 String 是 immutable。

~Keeping Coding; Keep Writing

Windows Service 是在 Windows OS 下執行的一種常駐程式,網站上也有不少教學說明如何 step-by-step 的開發。如果有興趣也可以參考   How to: Create Windows Services 但是 Windows Service 無法在直...


Windows Service 是在 Windows OS 下執行的一種常駐程式,網站上也有不少教學說明如何 step-by-step 的開發。如果有興趣也可以參考

  How to: Create Windows Services

但是 Windows Service 無法在直接執行,必須要透過 install 才可以執行並看到執行結果,寫起來十分不方便。Google 了一下,發現可以將 Windows Service 改成一個 Console 的程式,在 Debug 時可以在 Console 下執行並將訊息輸出至畫面,待開發完成再包成安裝即可,十分方便。

步驟記錄如下:

1. 建立 Windows Service 專案

2. 開啟專案的 properties 畫面, 將 output type 改成 Console Application

3. 開啟 program.cs, 將 Main 函式改成如下:

static void Main(string[] args)
{
    ServiceBase[] ServicesToRun;
    ServicesToRun = new ServiceBase[] { new MyConsoleService() };

    if (Environment.UserInteractive)
    {
        Type type = typeof(ServiceBase);
        BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
        MethodInfo method = type.GetMethod("OnStart", flags);

        foreach (ServiceBase service in ServicesToRun)
        {
            method.Invoke(service, new object[] { args });
        }

        Console.WriteLine("== 結束請按任意鍵 =="); Console.Read();

        foreach (ServiceBase service in ServicesToRun)
        {
            service.Stop();
        }
    }
    else
    {
        ServiceBase.Run(ServicesToRun);
    }
}


~ Keep Coding; Keep Writing