Bagikan melalui


Tutorial: Menulis handler interpolasi string kustom

Dalam tutorial ini, Anda akan mempelajari cara:

  • Menerapkan pola handler interpolasi string
  • Berinteraksi dengan penerima dalam operasi interpolasi string.
  • Menambahkan argumen ke dalam handler interpolasi string
  • Memahami fitur pustaka baru untuk interpolasi string

Prasyarat

Anda harus menyiapkan komputer Anda untuk menjalankan .NET 6, termasuk pengompilasi C# 10. Pengompilasi C# 10 tersedia mulai dengan Visual Studio 2022 atau .NET 6 SDK.

Tutorial ini mengasumsikan Anda terbiasa dengan C# dan .NET, termasuk Visual Studio atau .NET CLI.

Kerangka baru

C# 10 menambahkan dukungan untuk handler string terinterpolasi kustom. Handler string terinterpolasi adalah jenis yang memproses ekspresi tempat penampung dalam string terinterpolasi. Tanpa handler kustom, tempat penampung diproses mirip dengan String.Format. Setiap tempat penampung diformat sebagai teks, lalu komponen digabungkan untuk membentuk string yang dihasilkan.

Anda dapat menulis handler untuk skenario apa pun di mana Anda menggunakan informasi tentang string yang dihasilkan. Apakah akan digunakan? Batasan apa yang ada dalam format? Beberapa contohnya termasuk:

  • Anda mungkin tidak memerlukan string yang dihasilkan lebih besar dari beberapa batas, seperti 80 karakter. Anda dapat memproses string terinterpolasi untuk mengisi buffer dengan panjang tetap, dan berhenti memproses setelah panjang buffer tercapai.
  • Anda mungkin memiliki format tabular, dan setiap tempat penampung harus memiliki panjang tetap. Handler kustom dapat memberlakukan itu alih-alih memaksa semua kode klien agar sesuai.

Dalam tutorial ini, Anda akan membuat handler interpolasi string untuk salah satu skenario performa inti: pustaka pengelogan. Bergantung pada tingkat log yang dikonfigurasi, pekerjaan pembuatan pesan log tidak diperlukan. Jika pengelogan sedang nonaktif, pekerjaan pembuatan string dari ekspresi string terinterpolasi tidak diperlukan. Pesan tidak pernah dicetak, sehingga perangkaian string dapat dilewati. Selain itu, ekspresi apa pun yang digunakan dalam tempat penampung, termasuk menghasilkan jejak tumpukan, tidak perlu dilakukan.

Handler string terinterpolasi dapat menentukan apakah string yang diformat akan digunakan, dan hanya melakukan pekerjaan yang penting jika diperlukan.

Implementasi awal

Mari kita mulai dari kelas dasar Logger yang mendukung tingkat yang berbeda:

public enum LogLevel
{
    Off,
    Critical,
    Error,
    Warning,
    Information,
    Trace
}

public class Logger
{
    public LogLevel EnabledLevel { get; init; } = LogLevel.Error;

    public void LogMessage(LogLevel level, string msg)
    {
        if (EnabledLevel < level) return;
        Console.WriteLine(msg);
    }
}

Logger ini mendukung enam tingkat yang berbeda. Saat pesan tidak akan melewati filter tingkat log, maka tidak akan ada output. API publik untuk pencatat menerima string (sepenuhnya diformat) sebagai pesan. Semua pekerjaan untuk membuat string telah dilakukan.

Menerapkan pola handler

Langkah ini untuk membuat handler string terinterpolasi yang membuat ulang perilaku saat ini. Handler string terinterpolasi adalah jenis yang harus memiliki karakteristik berikut:

  • System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute yang diterapkan ke jenis.
  • Konstruktor yang memiliki dua parameter int, literalLength dan formattedCount. (Parameter lainnya diizinkan).
  • Metode publik AppendLiteral dengan tanda tangan: public void AppendLiteral(string s).
  • Metode publik generik AppendFormatted dengan tanda tangan: public void AppendFormatted<T>(T t).

Secara internal, penyusun membuat string yang diformat dan menyediakan anggota bagi klien untuk mengambil string tersebut. Kode berikut menunjukkan jenis LogInterpolatedStringHandler yang memenuhi persyaratan berikut:

[InterpolatedStringHandler]
public ref struct LogInterpolatedStringHandler
{
    // Storage for the built-up string
    StringBuilder builder;

    public LogInterpolatedStringHandler(int literalLength, int formattedCount)
    {
        builder = new StringBuilder(literalLength);
        Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
    }

    public void AppendLiteral(string s)
    {
        Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
        
        builder.Append(s);
        Console.WriteLine($"\tAppended the literal string");
    }

    public void AppendFormatted<T>(T t)
    {
        Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");

        builder.Append(t?.ToString());
        Console.WriteLine($"\tAppended the formatted object");
    }

    internal string GetFormattedText() => builder.ToString();
}

Sekarang Anda dapat menambahkan kelebihan beban ke LogMessagedi kelas Logger untuk mencoba handler string terinterpolasi baru Anda:

public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

Anda tidak perlu menghapus metode asli LogMessage, pengompilasi akan lebih memilih metode dengan parameter handler terinterpolasi daripada metode dengan parameter string saat argumen berupa ekspresi string terinterpolasi.

Anda dapat memverifikasi bahwa handler baru dipanggil menggunakan kode berikut sebagai program utama:

var logger = new Logger() { EnabledLevel = LogLevel.Warning };
var time = DateTime.Now;

logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time}. This won't be printed.");
logger.LogMessage(LogLevel.Warning, "Warning Level. This warning is a string, not an interpolated string expression.");

Menjalankan aplikasi menghasilkan output yang mirip dengan teks berikut:

        literal length: 65, formattedCount: 1
        AppendLiteral called: {Error Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This is an error. It will be printed.}
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: {Trace Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This won't be printed.}
        Appended the literal string
Warning Level. This warning is a string, not an interpolated string expression.

Menelusuri output, Anda dapat melihat bagaimana pengompilasi menambahkan kode untuk memanggil handler dan membuat string:

  • Pengompilasi menambahkan panggilan untuk membangun handler, meneruskan panjang total teks harfiah dalam string format, dan jumlah tempat penampung.
  • Pengompilasi menambahkan panggilan ke AppendLiteral dan AppendFormatted untuk setiap bagian string harfiah dan untuk setiap tempat penampung.
  • Pengompilasi LogMessage memanggil metode menggunakan CoreInterpolatedStringHandler sebagai argumen.

Terakhir, perhatikan bahwa peringatan terakhir tidak memanggil handler string terinterpolasi. Argumen tersebut adalah string, sehingga panggilan meminta kelebihan beban lainnya dengan parameter string.

Menambahkan lebih banyak kemampuan ke handler

Versi sebelumnya dari handler string terinterpolasi mengimplementasikan pola. Untuk menghindari pemrosesan setiap ekspresi tempat penampung, Anda memerlukan informasi lebih lanjut pada handler. Di bagian ini, Anda akan meningkatkan handler sehingga tidak banyak pekerjaan saat string yang dibuat tidak akan ditulis ke log. Anda menggunakan System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute untuk menentukan pemetaan antara parameter ke API publik dan parameter ke konstruktor handler. Yang menyediakan handler dengan informasi yang diperlukan untuk menentukan apakah string terinterpolasi harus dievaluasi.

Mari kita mulai dengan perubahan pada Handler. Pertama, tambahkan bidang untuk melacak apakah handler diaktifkan. Tambahkan dua parameter ke konstruktor: satu untuk menentukan tingkat log untuk pesan ini, dan yang lainnya sebagai referensi ke objek log:

private readonly bool enabled;

public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel)
{
    enabled = logger.EnabledLevel >= logLevel;
    builder = new StringBuilder(literalLength);
    Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}

Selanjutnya, gunakan bidang sehingga handler Anda hanya menambahkan harfiah atau objek yang diformat saat string akhir akan digunakan:

public void AppendLiteral(string s)
{
    Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
    if (!enabled) return;

    builder.Append(s);
    Console.WriteLine($"\tAppended the literal string");
}

public void AppendFormatted<T>(T t)
{
    Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
    if (!enabled) return;

    builder.Append(t?.ToString());
    Console.WriteLine($"\tAppended the formatted object");
}

Selanjutnya, Anda harus memperbarui deklarasi LogMessage sehingga pengompilasi meneruskan parameter tambahan ke konstruktor handler. Hal tersebut ditangani menggunakan System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute pada argumen handler:

public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

Atribut ini menentukan daftar argumen ke LogMessage yang memetakan ke parameter yang mengikuti parameter literalLength dan formattedCount yang diperlukan. String kosong (""), menentukan penerima. Pengompilasi menggantikan nilai objek Logger yang diwakili oleh this untuk argumen berikutnya ke konstruktor handler. Pengompilasi menggantikan nilai level untuk argumen berikut. Anda dapat memberikan sejumlah argumen untuk handler apa pun yang Anda tulis. Argumen yang Anda tambahkan adalah argumen string.

Anda dapat menjalankan versi ini menggunakan kode pengujian yang sama. Kali ini, Anda akan melihat hasil berikut:

        literal length: 65, formattedCount: 1
        AppendLiteral called: {Error Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This is an error. It will be printed.}
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: {Trace Level. CurrentTime: }
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        AppendLiteral called: {. This won't be printed.}
Warning Level. This warning is a string, not an interpolated string expression.

Anda dapat melihat bahwa metode AppendLiteral dan AppendFormat sedang dipanggil, tetapi tidak mengerjakan apa pun. Handler telah menentukan bahwa string akhir tidak akan diperlukan, sehingga handler tidak membuatnya. Masih ada beberapa perbaikan yang harus dilakukan.

Pertama, Anda dapat menambahkan kelebihan beban AppendFormatted yang membatasi argumen ke jenis yang mengimplementasikan System.IFormattable. Kelebihan beban ini memungkinkan penelepon untuk menambahkan string format di tempat penampung. Saat membuat perubahan ini, mari kita ubah juga jenis pengembalian metode AppendFormatted dan AppendLiteral yang lain, dari void ke bool (jika salah satu metode ini memiliki jenis pengembalian yang berbeda, maka Anda akan mendapatkan kesalahan kompilasi). Perubahan itu memungkinkan mengalami sirkuit pendek terhadap. Metode tersebut mengembalikan false untuk menunjukkan bahwa pemrosesan ekspresi string terinterpolasi harus dihentikan. Mengembalikan true menunjukkan bahwa itu harus berlanjut. Dalam contoh ini, Anda menggunakannya untuk berhenti memproses saat string yang dihasilkan tidak diperlukan. Sirkuit pendek mendukung tindakan yang lebih halus. Anda dapat berhenti memproses ekspresi setelah mencapai panjang tertentu untuk mendukung buffer dengan panjang tetap. Atau beberapa kondisi dapat menunjukkan elemen tersisa sudah tidak diperlukan.

public void AppendFormatted<T>(T t, string format) where T : IFormattable
{
    Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with format {{{format}}} is of type {typeof(T)},");

    builder.Append(t?.ToString(format, null));
    Console.WriteLine($"\tAppended the formatted object");
}

Dengan tambahan tersebut, Anda dapat menentukan string format dalam ekspresi string terinterpolasi Anda:

var time = DateTime.Now;

logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. The time doesn't use formatting.");
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time:t}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time:t}. This won't be printed.");

:t pada pesan pertama menentukan "format waktu singkat" untuk waktu saat ini. Contoh sebelumnya menunjukkan salah satu kelebihan beban ke metode AppendFormatted yang dapat Anda buat untuk handler Anda. Anda tidak perlu menentukan argumen generik untuk objek yang sedang diformat. Anda mungkin memiliki cara yang lebih efisien untuk mengonversi jenis yang Anda buat menjadi string. Anda dapat menulis kelebihan beban AppendFormatted yang mengambil jenis tersebut alih-alih argumen generik. Pengompilasi akan memilih kelebihan beban yang terbaik. Runtime bahasa umum menggunakan teknik ini untuk mengonversi System.Span<T> ke output string. Anda dapat menambahkan parameter bilangan bulat untuk menentukan perataan output, dengan atau tanpa IFormattable. System.Runtime.CompilerServices.DefaultInterpolatedStringHandler yang dikirim dengan .NET 6 berisi sembilan kelebihan beban AppendFormatted untuk penggunaan yang berbeda. Anda dapat menggunakannya sebagai referensi saat membuat handler untuk tujuan Anda.

Jalankan sampel sekarang, dan Anda akan melihat bahwa untuk pesan Trace, hanya AppendLiteral yang pertama yang akan dipanggil:

        literal length: 60, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted called: 10/20/2021 12:18:29 PM is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: . The time doesn't use formatting.
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:18:29 PM. The time doesn't use formatting.
        literal length: 65, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted (IFormattable version) called: 10/20/2021 12:18:29 PM with format {t} is of type System.DateTime,
        Appended the formatted object
        AppendLiteral called: . This is an error. It will be printed.
        Appended the literal string
Error Level. CurrentTime: 12:18 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: Trace Level. CurrentTime:
Warning Level. This warning is a string, not an interpolated string expression.

Anda dapat membuat satu pembaruan terakhir ke konstruktor handler yang meningkatkan efisiensi. Handler dapat menambahkan parameter akhir out bool. Mengatur parameter tersebut ke false menunjukkan bahwa handler tidak boleh dipanggil sama sekali untuk memproses ekspresi string terinterpolasi:

public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel level, out bool isEnabled)
{
    isEnabled = logger.EnabledLevel >= level;
    Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
    builder = isEnabled ? new StringBuilder(literalLength) : default!;
}

Perubahan tersebut membuat Anda dapat menghapus bidang enabled. Kemudian, Anda dapat mengubah jenis pengembalian AppendLiteral dan AppendFormatted menjadi void. Saat Anda menjalankan sampel, Anda akan melihat output berikut:

        literal length: 60, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted called: 10/20/2021 12:19:10 PM is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: . The time doesn't use formatting.
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. The time doesn't use formatting.
        literal length: 65, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted (IFormattable version) called: 10/20/2021 12:19:10 PM with format {t} is of type System.DateTime,
        Appended the formatted object
        AppendLiteral called: . This is an error. It will be printed.
        Appended the literal string
Error Level. CurrentTime: 12:19 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
Warning Level. This warning is a string, not an interpolated string expression.

Satu-satunya output ketika LogLevel.Trace ditentukan adalah output dari konstruktor. Handler menunjukkan bahwa itu tidak diaktifkan, sehingga tidak ada metode Append yang dipanggil.

Contoh ini mengilustrasikan poin penting untuk handler string terinterpolasi, terutama ketika pustaka pengelogan digunakan. Efek samping apa pun di tempat penampung mungkin tidak terjadi. Tambahkan kode berikut ke program utama Anda dan lihat perilaku ini menjalankan tugasnya:

int index = 0;
int numberOfIncrements = 0;
for (var level = LogLevel.Critical; level <= LogLevel.Trace; level++)
{
    Console.WriteLine(level);
    logger.LogMessage(level, $"{level}: Increment index a few times {index++}, {index++}, {index++}, {index++}, {index++}");
    numberOfIncrements += 5;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");

Anda dapat melihat variabel index meningkat lima kali dalam setiap iterasi perulangan. Karena tempat penampung dievaluasi hanya untuk tingkat Critical, Error, dan Warning, bukan untuk Information dan Trace, nilai index akhir tidak sesuai dengan harapan:

Critical
Critical: Increment index a few times 0, 1, 2, 3, 4
Error
Error: Increment index a few times 5, 6, 7, 8, 9
Warning
Warning: Increment index a few times 10, 11, 12, 13, 14
Information
Trace
Value of index 15, value of numberOfIncrements: 25

Handler string terinterpolasi memberikan kontrol yang lebih besar atas bagaimana ekspresi string terinterpolasi dikonversi menjadi string. Tim runtime bahasa umum .NET telah menggunakan fitur ini untuk meningkatkan performa di beberapa area. Anda dapat menggunakan kemampuan yang sama dalam pustaka Anda sendiri. Untuk menjelajahi lebih lanjut, lihat System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Ini menyediakan implementasi yang lebih lengkap daripada yang Anda buat di sini. Anda akan melihat lebih banyak kelebihan beban yang dimungkinkan untuk metode Append.