Aplikasi konsol

Tutorial ini mengajarkan Anda beberapa fitur dalam .NET dan bahasa C#. Anda akan mempelajari:

  • Dasar-dasar .NET CLI
  • Struktur Aplikasi Konsol C#
  • Konsol I/O
  • Dasar-dasar File API I/O di .NET
  • Dasar-dasar Pemrograman Asinkron Berbasis Tugas di .NET

Anda akan membuat aplikasi yang membaca file teks, dan menggemakan konten file teks tersebut ke konsol. Output ke konsol diatur agar sesuai dengan membacanya dengan keras. Anda dapat mempercepat atau memperlambat kecepatan dengan menekan tombol '<' (kurang dari) atau '>' (lebih besar dari). Anda dapat menjalankan aplikasi ini di Windows, Linux, macOS, atau dalam kontainer Docker.

Ada banyak fitur dalam tutorial ini. Mari kita membangunnya satu per satu.

Prasyarat

Membuat aplikasi

Langkah pertama adalah membuat aplikasi baru. Buka perintah dan buat direktori baru untuk aplikasi Anda. Jadikan direktori baru tersebut direktori saat ini. Ketik perintah dotnet new console pada perintah. Perintah ini membuat file starter untuk aplikasi dasar "Halo Dunia".

Sebelum mulai melakukan modifikasi, mari kita jalankan aplikasi Halo Dunia yang sederhana. Setelah membuat aplikasi, ketik dotnet run pada perintah. Perintah ini menjalankan proses pemulihan paket NuGet, membuat aplikasi yang dapat dieksekusi, dan menjalankan yang dapat dieksekusi.

Kode aplikasi Halo Dunia sederhana semuanya ada di Program.cs. Buka file tersebut dengan editor teks favorit Anda. Ganti kode di Program.cs dengan kode berikut:

namespace TeleprompterConsole;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Di bagian atas file, lihat pernyataan namespace. Seperti bahasa Berorientasi Objek lainnya yang mungkin pernah Anda gunakan, C# menggunakan namespace untuk mengatur jenis. Program Halo Dunia ini tidak berbeda. Anda dapat melihat bahwa program tersebut berada di namespace dengan nama TeleprompterConsole.

Membaca dan Menggemakan File

Fitur pertama yang ditambahkan adalah kemampuan untuk membaca file teks dan menampilkan semua teks tersebut ke konsol. Pertama, mari tambahkan file teks. Salin file sampleQuotes.txt dari repositori GitHub untuk sampel ini ke direktori proyek Anda. Ini akan berfungsi sebagai skrip untuk aplikasi Anda. Untuk informasi tentang cara mengunduh aplikasi sampel untuk tutorial ini, lihat petunjuk di Sampel dan Tutorial.

Selanjutnya, tambahkan metode berikut di kelas Program Anda (tepat di bawah metode Main):

static IEnumerable<string> ReadFrom(string file)
{
    string? line;
    using (var reader = File.OpenText(file))
    {
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

Metode ini adalah jenis khusus metode C# yang disebut metode iterator. Metode iterator mengembalikan urutan yang dievaluasi dengan malas. Itu berarti setiap item dalam urutan dihasilkan seperti yang diminta oleh kode yang menggunakan urutan tersebut. Metode iterator adalah metode yang berisi satu atau beberapa yield return pernyataan. Objek yang dikembalikan oleh metode ReadFrom berisi kode untuk menghasilkan setiap item dalam urutan. Dalam contoh ini, yang melibatkan membaca baris teks berikutnya dari file sumber, dan mengembalikan string tersebut. Setiap kali kode panggilan meminta item berikutnya dari urutan, kode membaca baris teks berikutnya dari file dan mengembalikannya. Ketika file sudah selesai dibaca, urutannya menunjukkan bahwa tidak ada item lagi.

Ada dua elemen sintaks C# yang mungkin baru bagi Anda. Pernyataan using dalam metode ini mengelola pembersihan sumber daya. Variabel yang diinisialisasi dalam pernyataan using (reader, dalam contoh ini) harus mengimplementasikan antarmuka IDisposable. Antarmuka tersebut mendefinisikan satu metode, Dispose, yang harus dipanggil saat sumber daya harus dirilis. Pengompilasi menghasilkan panggilan tersebut ketika eksekusi mencapai kurung kurawal penutup dari pernyataan using. Kode yang dihasilkan oleh pengompilasi memastikan bahwa sumber daya dirilis bahkan jika pengecualian ditampilkan dari kode di blok yang ditentukan oleh pernyataan penggunaan.

Variabel reader ditentukan menggunakan kata kunci var. var mendefinisikan variabel lokal yang diketik secara implisit. Itu berarti jenis variabel ditentukan oleh jenis waktu kompilasi dari objek yang ditetapkan ke variabel. Di sini, itu adalah nilai kembalian dari metode OpenText(String), yang merupakan objek StreamReader.

Sekarang, mari kita isi kode untuk membaca file dengan metode Main:

var lines = ReadFrom("sampleQuotes.txt");
foreach (var line in lines)
{
    Console.WriteLine(line);
}

Jalankan program (menggunakan dotnet run) dan Anda dapat melihat setiap baris dicetak ke konsol.

Menambahkan Penundaan dan Memformat output

Apa yang Anda miliki ditampilkan terlalu cepat untuk dibaca dengan keras. Sekarang Anda perlu menambahkan penundaan dalam output. Saat memulai, Anda akan membangun beberapa kode inti yang memungkinkan pemrosesan asinkron. Namun, langkah-langkah pertama ini akan mengikuti beberapa anti-pola. Anti-pola ditunjukkan dalam komentar saat Anda menambahkan kode, dan kode akan diperbarui pada langkah selanjutnya.

Ada dua langkah untuk bagian ini. Pertama, Anda akan memperbarui metode iterator untuk menampilkan satu kata, bukan seluruh baris. Hal tersebut dilakukan dengan modifikasi ini. Ganti pernyataan yield return line; dengan kode berikut:

var words = line.Split(' ');
foreach (var word in words)
{
    yield return word + " ";
}
yield return Environment.NewLine;

Selanjutnya, Anda perlu mengubah cara menggunakan baris file, dan menambahkan penundaan setelah menulis setiap kata. Ganti pernyataan Console.WriteLine(line) dalam metode Main dengan blok berikut:

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();
}

Jalankan sampel, dan periksa outputnya. Sekarang, setiap kata dicetak, diikuti dengan penundaan 200 md. Namun, output yang ditampilkan menunjukkan beberapa masalah karena file teks sumber memiliki beberapa baris yang memiliki lebih dari 80 karakter tanpa jeda baris. Tulisan tersebut akan sulit dibaca saat sedang discroll. Masalah tersebut mudah diperbaiki. Anda hanya akan melacak panjang setiap baris, dan membuat baris baru setiap kali panjang garis mencapai ambang tertentu. Deklarasikan variabel lokal setelah deklarasi words dalam metode ReadFrom yang menampung panjang baris:

var lineLength = 0;

Kemudian, tambahkan kode berikut setelah pernyataan yield return word + " "; (sebelum kurung kurawal):

lineLength += word.Length + 1;
if (lineLength > 70)
{
    yield return Environment.NewLine;
    lineLength = 0;
}

Jalankan sampel, dan Anda akan dapat membaca dengan lantang dengan kecepatan yang telah dikonfigurasi sebelumnya.

Tugas Asinkron

Pada langkah terakhir ini, Anda akan menambahkan kode untuk menulis output secara asinkron dalam satu tugas, sambil menjalankan tugas lain untuk membaca input dari pengguna jika mereka ingin mempercepat atau memperlambat tampilan teks, atau menghentikan tampilan teks sepenuhnya. Langkah ini memiliki beberapa langkah di dalamnya dan pada akhirnya, Anda akan memiliki semua pembaruan yang Anda butuhkan. Langkah pertama adalah membuat metode pengembalian Task asinkron yang mewakili kode yang telah Anda buat sejauh ini untuk membaca dan menampilkan file.

Tambahkan metode ini ke kelas Program Anda (diambil dari isi metode Main Anda):

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);
        }
    }
}

Anda akan melihat dua perubahan. Pertama, dalam isi metode, sebagai ganti memanggil Wait() untuk menunggu tugas selesai secara serempak, versi ini menggunakan kata kunci await. Untuk melakukannya, Anda perlu menambahkan pengubah async ke tanda tangan metode. Metode ini mengembalikan Task. Perhatikan bahwa tidak ada pernyataan pengembalian yang mengembalikan objek Task. Sebagai gantinya, objek Task tersebut dibuat oleh kode yang dihasilkan oleh pengompilasi saat Anda menggunakan operator await. Anda dapat membayangkan bahwa metode ini kembali ketika mencapai await. Task yang dikembalikan menunjukkan bahwa pekerjaan belum selesai. Metode dilanjutkan ketika tugas yang ditunggu selesai. Ketika telah dieksekusi sampai selesai, Task yang dikembalikan menunjukkan bahwa pekerjaan telah selesai. Kode panggilan dapat memantau Task yang dikembalikan untuk menentukan kapan pekerjaan akan selesai.

await Tambahkan kata kunci sebelum panggilan ke ShowTeleprompter:

await ShowTeleprompter();

Ini mengharuskan Anda untuk mengubah Main tanda tangan metode menjadi:

static async Task Main(string[] args)

Pelajari selengkapnya tentang async Main metode di bagian dasar-dasar kami.

Selanjutnya, Anda perlu menulis metode asinkron kedua untuk membaca dari Konsol dan memperhatikan tombol '<' (kurang dari), '>' (lebih besar dari) dan 'X' atau 'x'. Berikut metode yang Anda tambahkan untuk tugas tersebut:

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);
}

Metode ini membuat ekspresi lambda untuk mewakili delegasi Action yang membaca kunci dari Konsol dan memodifikasi variabel lokal yang mewakili penundaan saat pengguna menekan tombol '<' (kurang dari) atau '>' (lebih besar dari). Metode delegasi selesai ketika pengguna menekan tombol 'X' atau 'x', yang memungkinkan pengguna menghentikan tampilan teks kapan saja. Metode ini menggunakan ReadKey() untuk memblokir dan menunggu pengguna menekan tombol.

Untuk menyelesaikan fitur ini, Anda perlu membuat metode pengembalian async Task baru yang memulai kedua tugas ini (GetInput dan ShowTeleprompter), dan juga mengelola data bersama di antara kedua tugas ini.

Saatnya membuat kelas yang dapat menangani data bersama antara dua tugas ini. Kelas ini berisi dua properti publik: penundaan, dan tanda Done untuk menunjukkan bahwa file telah dibaca sepenuhnya:

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;
    }
}

Letakkan kelas tersebut di file baru, dan sertakan kelas tersebut di namespace TeleprompterConsole seperti yang ditunjukkan. Anda juga perlu menambahkan pernyataan using static di bagian atas file sehingga Anda dapat mereferensikan metode Min dan Max tanpa menyertakan nama kelas atau namespace. Pernyataan using static mengimpor metode dari satu kelas. Ini berbeda dengan pernyataan using tanpa static, yang mengimpor semua kelas dari namespace.

using static System.Math;

Selanjutnya, Anda perlu memperbarui metode ShowTeleprompter dan GetInput untuk menggunakan objek config baru. Tulis satu metode pengembalian Task terakhir async untuk memulai kedua tugas dan keluar saat tugas pertama selesai:

private static async Task RunTeleprompter()
{
    var config = new TelePrompterConfig();
    var displayTask = ShowTeleprompter(config);

    var speedTask = GetInput(config);
    await Task.WhenAny(displayTask, speedTask);
}

Satu-satunya metode baru di sini adalah panggilan WhenAny(Task[]). Metode inin membuat Task yang selesai segera setelah salah satu tugas dalam daftar argumennya selesai.

Selanjutnya, Anda perlu memperbarui metode ShowTeleprompter dan GetInput guna menggunakan objek config untuk penundaan:

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);
}

Versi baru ShowTeleprompter ini memanggil metode baru di kelas TeleprompterConfig. Sekarang, Anda perlu memperbarui Main untuk memanggil RunTeleprompter, bukan ShowTeleprompter:

await RunTeleprompter();

Kesimpulan

Tutorial ini menunjukkan kepada Anda beberapa fitur seputar bahasa C# dan pustaka .NET Core yang terkait dengan bekerja di aplikasi Konsol. Anda dapat membangun pengetahuan ini untuk mengeksplorasi lebih banyak tentang bahasa, dan kelas yang diperkenalkan di sini. Anda telah melihat dasar-dasar File dan Konsol I/O, penggunaan pemblokiran dan non-pemblokiran dari pemrograman asinkron berbasis Tugas, tur bahasa C# dan cara program C# diatur, dan .NET CLI.

Untuk informasi selengkapnya tentang File I/O, lihat File dan Aliran I/O. Untuk informasi selengkapnya tentang model pemrograman asinkron yang digunakan dalam tutorial ini, lihat Pemrograman Asinkron Berbasis Tugas dan Pemrograman Asinkron.