本教學課程會教導您 .NET 和 C# 語言中的許多功能。 您將了解:
- .NET CLI 的基本概念
- C# 主控台應用程式的結構
- 控制台 I/O
- .NET 中檔案 I/O API 的基本概念
- .NET 中以工作為基礎的異步程序設計的基本概念
您將建置可讀取文字檔的應用程式,並將該文本檔的內容回應至主控台。 控制台的輸出會調整速度,以便適合大聲朗讀。 您可以按 '<' (小於) 或 '>' (大於) 鍵來加快或放慢速度。 您可以在 Windows、Linux、macOS 或 Docker 容器中執行此應用程式。
本教學課程有許多功能。 讓我們逐一建置它們。
先決條件
- 最新 .NET SDK
- Visual Studio Code 編輯器
- C# 開發套件
建立應用程式
第一個步驟是建立新的應用程式。 開啟命令提示字元,併為您的應用程式建立新的目錄。 讓該目錄成為目前的目錄。 在命令提示字元中輸入命令 dotnet new console
。 這會建立基本 「Hello World」 應用程式的入門檔案。
開始進行修改之前,讓我們執行簡單的 Hello World 應用程式。 建立應用程式之後,請在命令提示字元中輸入 dotnet run
。 此命令會執行 NuGet 套件還原程式、建立應用程式可執行檔,以及執行可執行檔。
簡單的 Hello World 應用程式程式代碼全都在 Program.cs中。 使用您慣用的文字編輯器開啟該檔案。 將 Program.cs 中的程式碼取代為下列程式碼:
namespace TeleprompterConsole;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
在檔案頂端,請參閱 namespace
語句。 和您可能使用過的其他面向物件語言一樣,C# 會使用命名空間來組織類型。 這個 Hello World 程式並無不同。 您可以看到程式位於命名空間中,名稱為 TeleprompterConsole
。
讀取和回應檔案
要新增的第一項功能是能夠讀取文本檔,並將所有文字顯示到主控台。 首先,讓我們新增文本檔。 將此 範例 的 GitHub 存放庫 sampleQuotes.txt 檔案複製到您的項目目錄中。 這會做為應用程式的腳本。 如需如何下載本教學課程範例應用程式的資訊,請參閱 範例和教學課程中的指示,。
接下來,在 Program
類別中新增下列方法(位於 Main
方法的正下方):
static IEnumerable<string> ReadFrom(string file)
{
string? line;
using (var reader = File.OpenText(file))
{
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
此方法是稱為 反覆運算器方法的特殊 C# 方法類型。 反覆運算器方法會傳回延遲評估的序列。 這表示序列中的每個項目都會在取用序列的程式代碼要求時產生。 迭代器方法是包含一或多個 yield return
語句的方法。
ReadFrom
方法傳回的物件包含程序代碼,以產生序列中的每個專案。 在此範例中,涉及從來源檔案讀取下一行文字,並傳回該字串。 每次呼叫程式代碼要求序列中的下一個專案時,程式代碼都會從檔案讀取下一行文字,並傳回它。 當檔案完全讀取時,這表示沒有其他項目。
您可能不熟悉兩個 C# 的語法元素。 此方法中的 using
語句會管理資源清除。 在 using
語句中初始化的變數(在此範例中為 ,reader
),必須實作 IDisposable 介面。 該介面會定義單一方法,Dispose
,該方法應在資源釋放時呼叫。 編譯程式會在執行到達 using
語句的右大括號時產生呼叫。 由編譯器生成的程式碼可確保即使程式碼在 using 語句所定義的區塊中引發例外狀況,資源仍會被釋放。
reader
變數是使用 var
關鍵詞來定義。
var
定義 隱含型別局部變數。 這表示變數的類型是由指派給變數之對象的編譯時間類型所決定。 在這裡,這是來自 OpenText(String) 方法的傳回值,這是 StreamReader 物件。
現在,讓我們填入程式代碼,以在 Main
方法中讀取檔案:
var lines = ReadFrom("sampleQuotes.txt");
foreach (var line in lines)
{
Console.WriteLine(line);
}
執行程式(使用dotnet run
),您可以看到每一行都列印到主控台。
新增延遲和格式化輸出
你擁有的東西顯示得太快,無法大聲朗讀。 現在您需要在輸出中新增延遲。 一開始,您將建置一些可啟用異步處理的核心程序代碼。 不過,這些第一個步驟將會遵循一些反模式。 當您新增程序代碼時,批註中會指出反模式,稍後步驟會更新程序代碼。
本節有兩個步驟。 首先,您將更新反覆運算器方法,以傳回單一單字而非整行。 這是透過這些修改完成的。 以下列程式代碼取代 yield return line;
語句:
var words = line.Split(' ');
foreach (var word in words)
{
yield return word + " ";
}
yield return Environment.NewLine;
接下來,您需要修改讀取檔案行的方式,並在寫入每個單字之後新增延遲。 以下列區塊取代 Main
方法中的 Console.WriteLine(line)
語句:
Console.Write(line);
if (!string.IsNullOrWhiteSpace(line))
{
var pause = Task.Delay(200);
// Synchronously waiting on a task is an
// anti-pattern. This will get fixed in later
// steps.
pause.Wait();
}
執行範例,並檢查輸出。 現在,每一個單字都會列印,後面接著 200 毫秒的延遲。 不過,輸出顯示了一些問題,因為來源文本檔中的多行字串超過80個字符且沒有換行符。 當內容滾動時,可能會很難閱讀。 這很容易修正。 您只會追蹤每一行的長度,並在每一行長度達到特定臨界值時產生新行。 在 ReadFrom
方法中,在宣告 words
之後,宣告一個儲存行長度的局部變數。
var lineLength = 0;
然後,在 yield return word + " ";
語句後面新增下列程序代碼(右大括弧之前):
lineLength += word.Length + 1;
if (lineLength > 70)
{
yield return Environment.NewLine;
lineLength = 0;
}
執行範例,您將能夠以預先設定的步調大聲朗讀。
異步任務
在最後一個步驟中,您會新增程式代碼以異步方式在一個工作中撰寫輸出,同時執行另一個工作來讀取使用者輸入,如果他們想要加速或減緩文字顯示速度,或完全停止文字顯示。 這有一些步驟,最後,您將擁有您需要的所有更新。 第一個步驟是建立異步 Task 傳回方法,代表您到目前為止建立的程序代碼,以讀取和顯示檔案。
將此方法新增至您的 Program
類別(取自您 Main
方法的主體):
private static async Task ShowTeleprompter()
{
var words = ReadFrom("sampleQuotes.txt");
foreach (var word in words)
{
Console.Write(word);
if (!string.IsNullOrWhiteSpace(word))
{
await Task.Delay(200);
}
}
}
您會注意到兩個變更。 首先,在方法的主體中,此版本會使用 await
關鍵詞,而不是呼叫 Wait() 同步等候工作完成。 若要這樣做,您必須將 async
修飾詞新增至方法簽章。 這個方法會傳回 Task
。 請注意,沒有傳回 Task
物件的 return 語句。 相反,Task
物件是由編譯器在使用 await
運算符時生成的代碼所建立的。 您可以想像這個方法在到達 await
時會返回。 傳回的 Task
表示工作尚未完成。 方法會在等候的工作完成時繼續執行。 當它執行到完成時,傳回的 Task
會指出它已完成。
呼叫碼可以監視傳回的 Task
,以判斷何時完成。
在呼叫 ShowTeleprompter
之前新增 await
關鍵詞:
await ShowTeleprompter();
這需要您將 Main
方法簽章變更為:
static async Task Main(string[] args)
在基本概念一節中深入瞭解 async Main
方法。
接下來,您需要撰寫第二個非同步方法,從 Console 讀取並監看<(小於)、>(大於)和『X』或『x』鍵。 以下是您為該工作新增的方法:
private static async Task GetInput()
{
var delay = 200;
Action work = () =>
{
do {
var key = Console.ReadKey(true);
if (key.KeyChar == '>')
{
delay -= 10;
}
else if (key.KeyChar == '<')
{
delay += 10;
}
else if (key.KeyChar == 'X' || key.KeyChar == 'x')
{
break;
}
} while (true);
};
await Task.Run(work);
}
這將建立一個 Lambda 表達式,用於表示一個 Action 委派,該委派從主控台讀取鍵盤輸入,並在使用者按下 '<'(小於)或 '>'(大於)鍵時,修改代表延遲的局部變數。 當使用者按下 『X' 或 'x' 鍵時,委派方法就會完成,讓用戶隨時停止顯示文字。 此方法會使用 ReadKey() 來封鎖並等候使用者按下按鍵。
若要完成這項功能,您必須建立新的 async Task
傳回方法,以啟動這兩項工作(GetInput
和 ShowTeleprompter
),同時管理這兩項工作之間的共享數據。
是時候建立類別,以處理這兩項工作之間的共享數據。 這個類別包含兩個公用屬性:延遲和旗標 Done
,表示檔案已完全讀取:
namespace TeleprompterConsole;
internal class TelePrompterConfig
{
public int DelayInMilliseconds { get; private set; } = 200;
public void UpdateDelay(int increment) // negative to speed up
{
var newDelay = Min(DelayInMilliseconds + increment, 1000);
newDelay = Max(newDelay, 20);
DelayInMilliseconds = newDelay;
}
public bool Done { get; private set; }
public void SetDone()
{
Done = true;
}
}
將該類別放在新的檔案中,並在 TeleprompterConsole
命名空間中包含該類別,如下所示。 您也必須在檔案頂端新增 using static
語句,以便參考 Min
和 Max
方法,而不需要封入類別或命名空間名稱。
using static
語句會從一個類別匯入方法。 這與不含 static
的 using
語句相反,它會從命名空間匯入所有類別。
using static System.Math;
接下來,您必須更新 ShowTeleprompter
和 GetInput
方法來使用新的 config
物件。 撰寫最後一個 Task
傳回 async
方法來啟動工作,並在第一個工作完成時結束:
private static async Task RunTeleprompter()
{
var config = new TelePrompterConfig();
var displayTask = ShowTeleprompter(config);
var speedTask = GetInput(config);
await Task.WhenAny(displayTask, speedTask);
}
這裡的其中一個新方法是 WhenAny(Task[]) 呼叫。 這會建立一個 Task
,只要引數列表中的任務中有任何一個完成,它就會完成。
接下來,您必須更新 ShowTeleprompter
和 GetInput
方法,以便使用 config
物件來處理延遲:
private static async Task ShowTeleprompter(TelePrompterConfig config)
{
var words = ReadFrom("sampleQuotes.txt");
foreach (var word in words)
{
Console.Write(word);
if (!string.IsNullOrWhiteSpace(word))
{
await Task.Delay(config.DelayInMilliseconds);
}
}
config.SetDone();
}
private static async Task GetInput(TelePrompterConfig config)
{
Action work = () =>
{
do {
var key = Console.ReadKey(true);
if (key.KeyChar == '>')
config.UpdateDelay(-10);
else if (key.KeyChar == '<')
config.UpdateDelay(10);
else if (key.KeyChar == 'X' || key.KeyChar == 'x')
config.SetDone();
} while (!config.Done);
};
await Task.Run(work);
}
這個新版本的 ShowTeleprompter
會呼叫 TeleprompterConfig
類別中的新方法。 現在,您必須更新 Main
來呼叫 RunTeleprompter
,而不是 ShowTeleprompter
:
await RunTeleprompter();
結論
本教學課程說明 C# 語言和與在控制台應用程式中運作相關的 .NET Core 連結庫的一些功能。 您可以根據這項知識來探索語言的詳細資訊,以及這裡介紹的類別。 您已了解檔案和控制台 I/O 的基本概念、以工作為基礎的異步程序設計中的封鎖和非封鎖使用、C# 語言的導覽、C# 程式的組織方式,以及 .NET CLI。
如需檔案 I/O 的詳細資訊,請參閱 檔案和資料流 I/O。 如需本教學課程中使用的異步程式設計模型詳細資訊,請參閱 以工作為基礎的異步程式設計 和 異步程式設計。