Öğretici: Özel dize ilişkilendirme işleyicisi yazma

Bu öğreticide aşağıdakilerin nasıl yapılacağını öğreneceksiniz:

  • Dize ilişkilendirme işleyicisi desenini uygulama
  • Bir dize ilişkilendirme işleminde alıcıyla etkileşime geçin.
  • Dize ilişkilendirme işleyicisine bağımsız değişkenler ekleme
  • Dize ilişkilendirme için yeni kitaplık özelliklerini anlama

Önkoşullar

C# 10 derleyicisi de dahil olmak üzere makinenizi .NET 6'yı çalıştıracak şekilde ayarlamanız gerekir. C# 10 derleyicisi Visual Studio 2022 veya .NET 6 SDK'sı ile başlayarak kullanılabilir.

Bu öğreticide, Visual Studio veya .NET CLI dahil olmak üzere C# ve .NET hakkında bilgi sahibi olduğunuz varsayılır.

Yeni ana hat

C# 10, özel bir ilişkilendirilmiş dize işleyicisi için destek ekler. İlişkili dize işleyicisi, yer tutucu ifadesini ilişkilendirilmiş bir dizede işleyen bir türdür. Özel işleyici olmadan, yer tutucular benzer şekilde String.Formatişlenir. Her yer tutucu metin olarak biçimlendirilir ve ardından bileşenler sonuçta elde edilen dizeyi oluşturacak şekilde birleştirilir.

Sonuçta elde edilen dize hakkındaki bilgileri kullandığınız herhangi bir senaryo için bir işleyici yazabilirsiniz. Kullanılacak mı? Biçimde hangi kısıtlamalar var? Bazı Örnekler:

  • Sonuçta elde edilen dizelerin hiçbirinin 80 karakter gibi bir sınırdan büyük olmamasını zorunlu kılabilirsiniz. Sabit uzunlukta bir arabelleği doldurmak için ilişkilendirilmiş dizeleri işleyebilir ve arabellek uzunluğuna ulaşıldıktan sonra işlemeyi durdurabilirsiniz.
  • Tablosal bir biçiminiz olabilir ve her yer tutucu sabit uzunlukta olmalıdır. Özel işleyici, tüm istemci kodunu uyumlu olmaya zorlamak yerine bunu zorunlu kılabilir.

Bu öğreticide, temel performans senaryolarından biri için bir dize ilişkilendirme işleyicisi oluşturacaksınız: günlük kitaplıkları. Yapılandırılan günlük düzeyine bağlı olarak, günlük iletisi oluşturma işi gerekli değildir. Günlük kapalıysa, ilişkilendirilmiş dize ifadesinden bir dize oluşturma çalışması gerekli değildir. İleti hiçbir zaman yazdırılmaz, bu nedenle herhangi bir dize birleştirme atlanabilir. Ayrıca, yığın izlemeleri oluşturma da dahil olmak üzere yer tutucularda kullanılan ifadelerin yapılması gerekmez.

İlişkili dize işleyicisi, biçimlendirilmiş dizenin kullanılıp kullanılmayacağını belirleyebilir ve yalnızca gerekirse gerekli çalışmayı gerçekleştirebilir.

İlk uygulama

Farklı düzeyleri destekleyen temel Logger bir sınıftan başlayalım:

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

Bu Logger , altı farklı düzeyi destekler. İleti günlük düzeyi filtresini geçmezse çıkış olmaz. Günlükçü için genel API, ileti olarak bir (tam olarak biçimlendirilmiş) dize kabul eder. Dizeyi oluşturmak için tüm çalışmalar zaten yapılmıştır.

İşleyici desenini uygulama

Bu adım, geçerli davranışı yeniden oluşturan bir ilişkilendirilmiş dize işleyicisi oluşturmaktır. İlişkili dize işleyicisi, aşağıdaki özelliklere sahip olması gereken bir türdür:

  • System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute türüne uygulanır.
  • ve formattedCountolmak üzere iki int parametresi literalLength olan bir oluşturucu. (Daha fazla parametreye izin verilir).
  • İmzası olan genel AppendLiteral bir yöntem: public void AppendLiteral(string s).
  • İmzası olan genel bir genel AppendFormatted yöntem: public void AppendFormatted<T>(T t).

Oluşturucu, biçimlendirilmiş dizeyi dahili olarak oluşturur ve bir istemcinin bu dizeyi alması için bir üye sağlar. Aşağıdaki kod, bu gereksinimleri karşılayan bir LogInterpolatedStringHandler tür gösterir:

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

Artık yeni ilişkilendirilmiş dize işleyicinizi denemek için sınıfına Logger bir aşırı yükleme LogMessage ekleyebilirsiniz:

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

Özgün LogMessage yöntemi kaldırmanız gerekmez; derleyici, bağımsız değişken ilişkilendirilmiş bir dize ifadesi olduğunda parametreli bir string yöntem yerine ilişkilendirilmiş işleyici parametresine sahip bir yöntemi tercih eder.

Yeni işleyicinin ana program olarak aşağıdaki kodu kullanarak çağrıldığı doğrulayabilirsiniz:

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.");

Uygulamanın çalıştırılması aşağıdaki metne benzer bir çıkış oluşturur:

        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.

Çıktıyı izlediğinizde, derleyicinin işleyiciyi çağırmak ve dizeyi derlemek için nasıl kod eklediğini görebilirsiniz:

  • Derleyici, biçim dizesindeki değişmez metnin toplam uzunluğunu ve yer tutucu sayısını geçirerek işleyiciyi oluşturmak için bir çağrı ekler.
  • Derleyici, değişmez değer dizesinin her bölümü ve her yer tutucu için ve çağrıları AppendLiteralAppendFormatted ekler.
  • Derleyici, yöntemini bağımsız değişken olarak kullanarak CoreInterpolatedStringHandler çağırırLogMessage.

Son olarak, son uyarının ilişkilendirilmiş dize işleyicisini çağırmadığını fark edin. bağımsız değişkeni bir string'dir, bu nedenle çağrı bir dize parametresiyle diğer aşırı yüklemeyi çağırır.

İşleyiciye daha fazla özellik ekleme

İlişkili dize işleyicisinin önceki sürümü deseni uygular. Her yer tutucu ifadenin işlenmesini önlemek için işleyicide daha fazla bilgiye ihtiyacınız olacaktır. Bu bölümde, oluşturduğunuz dize günlüğe yazılmayacağı zaman daha az çalışmaması için işleyicinizi geliştireceksiniz. Bir ortak API'ye parametreler arasında eşleme ve işleyicinin oluşturucusna parametreler arasında eşleme belirtmek için kullanırsınız System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute . Bu, işleyiciye, ilişkilendirilmiş dizenin değerlendirilmesinin gerekip gerekmediğini belirlemek için gereken bilgileri sağlar.

İşleyici'de yapılan değişikliklerle başlayalım. İlk olarak, işleyicinin etkin olup olmadığını izlemek için bir alan ekleyin. Oluşturucuya iki parametre ekleyin: biri bu iletinin günlük düzeyini belirtmek için, diğeri de günlük nesnesine bir başvuru:

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

Ardından, işleyicinizin yalnızca son dize kullanıldığında değişmez değerleri veya biçimlendirilmiş nesneleri eklemesi için alanını kullanın:

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

Ardından, derleyicinin ek parametreleri işleyicinin LogMessage oluşturucusna geçirmesi için bildirimini güncelleştirmeniz gerekir. İşleyici bağımsız değişkeni kullanılarak System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute işlenir:

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

Bu öznitelik, gerekli literalLength ve formattedCount parametreleri izleyen parametrelerle eşleyen bağımsız değişkenlerin LogMessage listesini belirtir. Boş dize (""), alıcıyı belirtir. Derleyici, işleyicinin oluşturucusunun Logger sonraki bağımsız değişkeni için tarafından this temsil edilen nesnesinin değerinin yerini alır. Derleyici, aşağıdaki bağımsız değişken için değerinin level yerini alır. Yazdığınız herhangi bir işleyici için istediğiniz sayıda bağımsız değişken sağlayabilirsiniz. Eklediğiniz bağımsız değişkenler dize bağımsız değişkenleridir.

Bu sürümü aynı test kodunu kullanarak çalıştırabilirsiniz. Bu kez aşağıdaki sonuçları görürsünüz:

        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.

ve AppendFormat yöntemlerinin AppendLiteral çağrıldığını görebilirsiniz, ancak herhangi bir iş yapmıyorlar. İşleyici, son dizenin gerekli olmadığını belirledi, bu nedenle işleyici bunu derlemez. Yine de yapılması gereken birkaç geliştirme vardır.

İlk olarak, bağımsız değişkeni uygulayan System.IFormattablebir türe kısıtlayan bir aşırı yükleme AppendFormatted ekleyebilirsiniz. Bu aşırı yükleme, çağıranların yer tutuculara biçim dizeleri eklemesini sağlar. Bu değişikliği yaparken diğer AppendFormatted ve AppendLiteral yöntemlerin dönüş türünü de olarak değiştirelim (bu yöntemlerden voidbool herhangi biri farklı dönüş türlerine sahipse, derleme hatası alırsınız). Bu değişiklik kısa devreyi etkinleştirir. yöntemleri, ilişkilendirilmiş dize ifadesinin işlenmesinin durdurulması gerektiğini belirtmek için döndürür false . döndürülmesi true , devam etmesi gerektiğini gösterir. Bu örnekte, sonuçta elde edilen dize gerekli olmadığında işlemeyi durdurmak için bunu kullanıyorsunuz. Kısa devre daha ayrıntılı eylemleri destekler. Sabit uzunlukta arabellekleri desteklemek için belirli bir uzunluğa ulaştığında ifadenin işlenmesini durdurabilirsiniz. Veya bazı koşullar kalan öğelerin gerekli olmadığını gösterebilir.

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

Bu eklemeyle, ilişkilendirilmiş dize ifadenizde biçim dizeleri belirtebilirsiniz:

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 İlk iletideki, geçerli saat için "kısa saat biçimini" belirtir. Önceki örnekte, işleyiciniz için oluşturabileceğiniz yönteme AppendFormatted yönelik aşırı yüklemelerden biri gösterildi. Biçimlendirilen nesne için genel bir bağımsız değişken belirtmeniz gerekmez. Oluşturduğunuz türleri dizeye dönüştürmek için daha verimli yollarınız olabilir. Genel bağımsız değişken yerine bu türleri alan aşırı yüklemeler AppendFormatted yazabilirsiniz. Derleyici en iyi aşırı yüklemeyi seçer. Çalışma zamanı, dize çıkışına dönüştürmek System.Span<T> için bu tekniği kullanır. Çıkışın hizalamasını belirtmek için, ile veya olmadan bir IFormattabletamsayı parametresi ekleyebilirsiniz. System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.NET 6 ile birlikte gelen, farklı kullanımlar AppendFormatted için dokuz aşırı yükleme içerir. Bunu, amaçlarınıza uygun bir işleyici oluştururken başvuru olarak kullanabilirsiniz.

Örneği şimdi çalıştırdığınızda ileti için Trace yalnızca ilkinin AppendLiteral çağrıldığını görürsünüz:

        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.

İşleyicinin oluşturucusunun verimliliğini artıran son bir güncelleştirme yapabilirsiniz. İşleyici son out bool parametreyi ekleyebilir. Bu parametreyi olarak ayarlamak false , ilişkilendirilmiş dize ifadesini işlemek için işleyicinin hiç çağrılmaması gerektiğini gösterir:

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

Bu değişiklik, alanı kaldırabileceğiniz enabled anlamına gelir. Ardından ve AppendFormattedvoiddönüş türünü AppendLiteral olarak değiştirebilirsiniz. Şimdi örneği çalıştırdığınızda aşağıdaki çıkışı görürsünüz:

        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.

Belirtildiğinde LogLevel.Trace tek çıkış oluşturucudan gelen çıkıştır. İşleyici etkinleştirilmediğini belirttiğinden yöntemlerden Append hiçbiri çağrılmamıştı.

Bu örnekte, özellikle günlük kitaplıkları kullanılırken, ilişkilendirilmiş dize işleyicileri için önemli bir nokta gösterilmektedir. Yer tutuculardaki yan etkiler oluşmayabilir. Ana programınıza aşağıdaki kodu ekleyin ve bu davranışı çalışırken görün:

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

Değişkenin döngünün index her yinelemesinde beş kez artırıldığından haberdar olabilirsiniz. Yer tutucular ve için değil yalnızca , Error ve TraceWarning düzeyleri için CriticalInformation değerlendirildiğindenindex, son değeri beklentiyle eşleşmiyor:

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

İlişkili dize işleyicileri, ilişkilendirilmiş dize ifadesinin bir dizeye nasıl dönüştürüldüğü üzerinde daha fazla denetim sağlar. .NET çalışma zamanı ekibi, çeşitli alanlarda performansı artırmak için bu özelliği zaten kullanmıştır. Kendi kitaplıklarınızda aynı özelliği kullanabilirsiniz. Daha fazla araştırmak için bölümüne System.Runtime.CompilerServices.DefaultInterpolatedStringHandlerbakın. Burada oluşturduğunuzdan daha eksiksiz bir uygulama sağlar. Yöntemler için Append mümkün olan çok daha fazla aşırı yükleme göreceksiniz.