共用方式為


主控台應用程式

本教學課程會教導您 .NET 和 C# 語言中的許多功能。 您將了解:

  • .NET CLI 的基本概念
  • C# 主控台應用程式的結構
  • 控制台 I/O
  • .NET 中檔案 I/O API 的基本概念
  • .NET 中以工作為基礎的異步程序設計的基本概念

您將建置可讀取文字檔的應用程式,並將該文本檔的內容回應至主控台。 控制台的輸出會調整速度,以便適合大聲朗讀。 您可以按 '<' (小於) 或 '>' (大於) 鍵來加快或放慢速度。 您可以在 Windows、Linux、macOS 或 Docker 容器中執行此應用程式。

本教學課程有許多功能。 讓我們逐一建置它們。

先決條件

建立應用程式

第一個步驟是建立新的應用程式。 開啟命令提示字元,併為您的應用程式建立新的目錄。 讓該目錄成為目前的目錄。 在命令提示字元中輸入命令 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 傳回方法,以啟動這兩項工作(GetInputShowTeleprompter),同時管理這兩項工作之間的共享數據。

是時候建立類別,以處理這兩項工作之間的共享數據。 這個類別包含兩個公用屬性:延遲和旗標 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 語句,以便參考 MinMax 方法,而不需要封入類別或命名空間名稱。 using static 語句會從一個類別匯入方法。 這與不含 staticusing 語句相反,它會從命名空間匯入所有類別。

using static System.Math;

接下來,您必須更新 ShowTeleprompterGetInput 方法來使用新的 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,只要引數列表中的任務中有任何一個完成,它就會完成。

接下來,您必須更新 ShowTeleprompterGetInput 方法,以便使用 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。 如需本教學課程中使用的異步程式設計模型詳細資訊,請參閱 以工作為基礎的異步程式設計異步程式設計