このチュートリアルでは、.NET と C# 言語のさまざまな機能について説明します。 学習内容:
- .NET CLI の基本
- C# コンソール アプリケーションの構造
- コンソール入出力
- .NET でのファイル I/O API の基本
- .NET でのタスク ベースの非同期プログラミングの基本
テキスト ファイルを読み取り、そのテキスト ファイルの内容をコンソールにエコーするアプリケーションを構築します。 コンソールへの出力は、読み上げに合わせてペースが調整されます。 '<' (より小さい) キーまたは '>' (より大きい) キーを押すと、速度を上げたり遅くしたりできます。 このアプリケーションは、Windows、Linux、macOS、または Docker コンテナーで実行できます。
このチュートリアルには多くの機能があります。 それらを 1 つずつビルドしてみましょう。
[前提条件]
- 最新の .NET SDK
- Visual Studio Code エディター
- C# DevKit
アプリを作成する
最初の手順では、新しいアプリケーションを作成します。 コマンド プロンプトを開き、アプリケーションの新しいディレクトリを作成します。 現在のディレクトリにします。 コマンド プロンプトでコマンド dotnet new console
を入力します。 例えば次が挙げられます。
E:\development\VSprojects>mkdir teleprompter
E:\development\VSprojects>cd teleprompter
E:\development\VSprojects\teleprompter>dotnet new console
The template "Console Application" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on E:\development\VSprojects\teleprompter\teleprompter.csproj...
Determining projects to restore...
Restored E:\development\VSprojects\teleprompter\teleprompter.csproj (in 78 ms).
Restore succeeded.
これにより、基本的な "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# メソッドです。 反復子メソッドは、遅延評価されるシーケンスを返します。 つまり、シーケンス内の各項目は、シーケンスを使用するコードによって要求されるときに生成されます。 反復子メソッドは、1 つ以上の yield return
ステートメントを含むメソッドです。 ReadFrom
メソッドによって返されるオブジェクトには、シーケンス内の各項目を生成するコードが含まれています。 この例では、ソース ファイルから次のテキスト行を読み取り、その文字列を返す必要があります。 呼び出し元のコードがシーケンスから次の項目を要求するたびに、コードはファイルから次のテキスト行を読み取って返します。 ファイルが完全に読み取られた場合、シーケンスは項目がなくなったことを示します。
新しい可能性がある 2 つの C# 構文要素があります。 このメソッドの using
ステートメントは、リソースのクリーンアップを管理します。 using
ステートメント (この例ではreader
) で初期化される変数は、IDisposable インターフェイスを実装する必要があります。 そのインターフェイスは、リソースを解放するときに呼び出す必要がある 1 つのメソッド ( 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
を使用して) プログラムを実行すると、コンソールに出力されたすべての行が表示されます。
遅延の追加および出力の書式設定
コードは今の状態だと、表示が早すぎて読み上げることができません。 次に、出力に遅延を追加する必要があります。 開始すると、非同期処理を可能にするコア コードの一部がビルドされます。 ただし、これらの最初の手順では、いくつかのアンチパターンに従います。 アンチパターンは、コードを追加するときにコメントで指摘され、コードは後の手順で更新されます。
このセクションには 2 つの手順があります。 まず、反復子メソッドを更新して、行全体ではなく単一の単語を返します。 これは、これらの変更によって行われます。 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;
}
サンプルを実行すると、あらかじめ設定されたペースで読み上げることができます。
非同期タスク
この最後の手順では、1 つのタスクで非同期的に出力を書き込むコードを追加します。また、テキスト表示を高速化または遅くしたり、テキスト表示を完全に停止したりする場合は、別のタスクを実行してユーザーからの入力を読み取ります。 これにはいくつかの手順があり、最終的には、必要なすべての更新プログラムが提供されます。 最初の手順では、ファイルを読み取って表示するためにこれまでに作成したコードを表す非同期 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);
}
}
}
2 つの変更が表示されます。 まず、メソッドの本体で、タスクが完了するまで同期的に待機するために Wait() を呼び出す代わりに、このバージョンでは await
キーワードを使用します。 そのためには、メソッドシグネチャに async
修飾子を追加する必要があります。 このメソッドは、 Task
を返します。 Task
オブジェクトを返す return ステートメントがないことに注意してください。 代わりに、その Task
オブジェクトは、 await
演算子を使用するときにコンパイラによって生成されるコードによって作成されます。 このメソッドは、 await
に達すると返されると想像できます。 返された Task
は、作業が完了しなかったことを示します。 待機中のタスクが完了すると、メソッドが再開されます。 完了まで実行されると、返された Task
は完了したことを示します。
呼び出し元のコードは、返された Task
を監視して、いつ完了したかを判断できます。
ShowTeleprompter
の呼び出しの前に、await
キーワードを追加します。
await ShowTeleprompter();
これには、 Main
メソッドシグネチャを次に変更する必要があります。
static async Task Main(string[] args)
async Main
メソッドの詳細については、「基礎」セクションを参照してください。
次に、コンソールから読み取る 2 番目の非同期メソッドを記述し、'<' (より小さい)、'>' (より大きい) キー、および '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);
}
これにより、コンソールからキーを読み取る Action デリゲートを表すラムダ式が作成され、ユーザーが '<' (より小さい) キーまたは '>' (より大きい) キーを押したときの遅延を表すローカル変数が変更されます。 デリゲート メソッドは、ユーザーが 'X' キーまたは 'x' キーを押すと終了します。これにより、ユーザーはいつでもテキストの表示を停止できます。 このメソッドでは、 ReadKey() を使用して、ユーザーがキーを押すのをブロックして待機します。
次に、これら 2 つのタスク間の共有データを処理できるクラスを作成します。 このクラスには、遅延と、ファイルが完全に読み取られたことを示すフラグ Done
の 2 つのパブリック プロパティが含まれています。
using static System.Math;
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;
}
}
新しいファイルを作成します。.csで終わる任意の名前を指定できます。 たとえば、TelePrompterConfig.cs。 TelePrompterConfig クラス コードを貼り付け、保存して閉じます。 次に示すように、そのクラスを TeleprompterConsole
名前空間に配置します。 using static
ステートメントを使用すると、外側のクラス名または名前空間名を指定せずに、Min
メソッドとMax
メソッドを参照できます。 using static
ステートメントは、1 つのクラスからメソッドをインポートします。 これは、名前空間からすべてのクラスをインポートするstatic
のないusing
ステートメントとは対照的です。
次に、新しいconfig
オブジェクトを使用するようにShowTeleprompter
メソッドとGetInput
メソッドを更新する必要があります。 この機能を完了するには、これらのタスク (GetInput
とShowTeleprompter
) の両方を開始し、これら 2 つのタスク間の共有データも管理する新しいasync Task
戻りメソッドを作成する必要があります。 RunTelePrompter タスクを作成して両方のタスクを開始し、最初のタスクの終了時に終了します。
private static async Task RunTeleprompter()
{
var config = new TelePrompterConfig();
var displayTask = ShowTeleprompter(config);
var speedTask = GetInput(config);
await Task.WhenAny(displayTask, speedTask);
}
ここでの 1 つの新しいメソッドは、 WhenAny(Task[]) 呼び出しです。 これで、引数リスト内のいずれかのタスクが完了するとすぐに完了する Task
が作成されます。
次に、遅延に config
オブジェクトを使用するには、ShowTeleprompter
メソッドと GetInput
メソッドの両方を更新する必要があります。 config オブジェクトは、これらのメソッドにパラメーターとして渡されています。 コピー/貼り付けを使用して、メソッドをここで新しいコードに完全に置き換えます。 コードが 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
ではなく、RunTeleprompter
を呼び出すようにMain
を更新する必要があります。
await RunTeleprompter();
結論
このチュートリアルでは、コンソール アプリケーションでの作業に関連する C# 言語と .NET Core ライブラリに関する多くの機能について説明しました。 この知識を基にして、言語とここで紹介するクラスの詳細を調べることができます。 ファイルとコンソールの I/O の基本、タスク ベースの非同期プログラミングのブロックと非ブロッキングの使用、C# 言語のツアー、および C# プログラムの編成方法、.NET CLI について説明しました。
ファイル I/O の詳細については、「 ファイルおよびストリーム I/O」を参照してください。 このチュートリアルで使用される非同期プログラミング モデルの詳細については、「 タスク ベースの非同期プログラミング と 非同期プログラミング」を参照してください。
.NET