Partage via


Primitifs : bibliothèque d’extensions pour .NET

Dans cet article, vous allez découvrir la bibliothèque Microsoft.Extensions.Primitives. Les primitifs dans cet article ne doivent pas être confondus avec les types primitifs .NET de la BCL ou ceux du langage C#. Au lieu de cela, les types dans la bibliothèque du primitif servent de blocs de construction pour certains des packages NuGet .NET périphériques, tels que :

Notifications de modifications

La propagation de notifications lorsqu’une modification se produit est un concept fondamental dans la programmation. L’état observé d’un objet peut changer le plus souvent. Lorsqu’une modification se produit, les implémentations de l’interface Microsoft.Extensions.Primitives.IChangeToken peuvent être utilisées pour informer les parties intéressées de ladite modification. Les implémentations suivantes sont disponibles :

En tant que développeur, vous êtes également libre d’implémenter votre propre type. L’interface IChangeToken définit quelques propriétés :

  • IChangeToken.HasChanged : obtient une valeur qui indique si une modification a eu lieu.
  • IChangeToken.ActiveChangeCallbacks : indique si le jeton déclenche des rappels de manière proactive. Si false, le consommateur du jeton doit interroger HasChanged pour détecter les modifications.

Fonctionnalités basées sur une instance

Considérez l’exemple d’utilisation suivant du CancellationChangeToken :

CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);

Console.WriteLine($"HasChanged: {cancellationChangeToken.HasChanged}");

static void callback(object? _) =>
    Console.WriteLine("The callback was invoked.");

using (IDisposable subscription =
    cancellationChangeToken.RegisterChangeCallback(callback, null))
{
    cancellationTokenSource.Cancel();
}

Console.WriteLine($"HasChanged: {cancellationChangeToken.HasChanged}\n");

// Outputs:
//     HasChanged: False
//     The callback was invoked.
//     HasChanged: True

Dans l’exemple précédent, une CancellationTokenSource est instanciée et son Token est passé au constructeur CancellationChangeToken. L’état initial de HasChanged est écrit dans la console. Un Action<object?> callback qui écrit lorsque le rappel est appelé dans la console est créé. La méthode RegisterChangeCallback(Action<Object>, Object) du jeton est appelée, en tenant compte du callback. Dans l’instruction using, la cancellationTokenSource est annulée. Cela déclenche le rappel, et l’état HasChanged est à nouveau écrit dans la console.

Lorsque vous devez prendre des mesures à partir de plusieurs sources de modifications, utilisez le CompositeChangeToken. Cette implémentation agrège un ou plusieurs jetons de modification et déclenche chaque rappel inscrit exactement une fois, quel que soit le nombre de fois où une modification est déclenchée. Prenons l’exemple suivant :

CancellationTokenSource firstCancellationTokenSource = new();
CancellationChangeToken firstCancellationChangeToken = new(firstCancellationTokenSource.Token);

CancellationTokenSource secondCancellationTokenSource = new();
CancellationChangeToken secondCancellationChangeToken = new(secondCancellationTokenSource.Token);

CancellationTokenSource thirdCancellationTokenSource = new();
CancellationChangeToken thirdCancellationChangeToken = new(thirdCancellationTokenSource.Token);

var compositeChangeToken =
    new CompositeChangeToken(
        new IChangeToken[]
        {
            firstCancellationChangeToken,
            secondCancellationChangeToken,
            thirdCancellationChangeToken
        });

static void callback(object? state) =>
    Console.WriteLine($"The {state} callback was invoked.");

// 1st, 2nd, 3rd, and 4th.
compositeChangeToken.RegisterChangeCallback(callback, "1st");
compositeChangeToken.RegisterChangeCallback(callback, "2nd");
compositeChangeToken.RegisterChangeCallback(callback, "3rd");
compositeChangeToken.RegisterChangeCallback(callback, "4th");

// It doesn't matter which cancellation source triggers the change.
// If more than one trigger the change, each callback is only fired once.
Random random = new();
int index = random.Next(3);
CancellationTokenSource[] sources = new[]
{
    firstCancellationTokenSource,
    secondCancellationTokenSource,
    thirdCancellationTokenSource
};
sources[index].Cancel();

Console.WriteLine();

// Outputs:
//     The 4th callback was invoked.
//     The 3rd callback was invoked.
//     The 2nd callback was invoked.
//     The 1st callback was invoked.

Dans le code C# précédent, trois instances d’objet CancellationTokenSource sont créées et associées à des instances CancellationChangeToken correspondantes. Le jeton composite est instancié en passant un tableau des jetons au constructeur CompositeChangeToken. Le Action<object?> callback est créé, mais cette fois, l’objet state est utilisé et écrit dans la console en tant que message mis en forme. Le rappel est inscrit quatre fois, chacun avec un argument d’objet d’état légèrement différent. Le code utilise un générateur de nombres pseudo-aléatoires pour choisir l’une des sources de jeton de modification (quelle qu’elle soit) et appeler sa méthode Cancel(). Cela déclenche la modification, appelant chaque rappel inscrit exactement une fois.

Autre approche static

En guise d’alternative à l’appel de RegisterChangeCallback, vous pouvez utiliser la classe statique Microsoft.Extensions.Primitives.ChangeToken. Tenez compte du modèle de consommation suivant :

CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);

IChangeToken producer()
{
    // The producer factory should always return a new change token.
    // If the token's already fired, get a new token.
    if (cancellationTokenSource.IsCancellationRequested)
    {
        cancellationTokenSource = new();
        cancellationChangeToken = new(cancellationTokenSource.Token);
    }

    return cancellationChangeToken;
}

void consumer() => Console.WriteLine("The callback was invoked.");

using (ChangeToken.OnChange(producer, consumer))
{
    cancellationTokenSource.Cancel();
}

// Outputs:
//     The callback was invoked.

Tout comme les exemples précédents, vous aurez besoin d’une implémentation de IChangeToken produite par le changeTokenProducer. Le producteur est défini comme étant un Func<IChangeToken> et il est prévu qu’un nouveau jeton soit retourné à chaque appel. Le consumer est soit une Action lorsque state n’est pas utilisé ou une Action<TState> où le type générique TState circule via la notification des modifications.

Générateurs de jetons, segments et valeurs de chaîne

Il est courant d’interagir avec des chaînes dans le cadre du développement d’applications. Diverses représentations de chaînes sont analysées, fractionnées ou itérées. La bibliothèque de primitifs offre quelques types de choix qui permettent d’interagir avec des chaînes de façon plus optimisée et efficace. Examinons les types suivants :

  • StringSegment : représentation optimisée d’une sous-chaîne.
  • StringTokenizer : segmente une string en instances StringSegment.
  • StringValues : représente null, zéro, une ou plusieurs chaînes de manière efficace.

Le type StringSegment

Dans cette section, vous allez découvrir une représentation optimisée d’une sous-chaîne appelée type StringSegment struct . Prenons l’exemple de code C# suivant montrant certaines des propriétés StringSegment et la méthode AsSpan :

var segment =
    new StringSegment(
        "This a string, within a single segment representation.",
        14, 25);

Console.WriteLine($"Buffer: \"{segment.Buffer}\"");
Console.WriteLine($"Offset: {segment.Offset}");
Console.WriteLine($"Length: {segment.Length}");
Console.WriteLine($"Value: \"{segment.Value}\"");

Console.Write("Span: \"");
foreach (char @char in segment.AsSpan())
{
    Console.Write(@char);
}
Console.Write("\"\n");

// Outputs:
//     Buffer: "This a string, within a single segment representation."
//     Offset: 14
//     Length: 25
//     Value: " within a single segment "
//     " within a single segment "

Le code précédent instancie la propriété StringSegment selon une valeur string, un offset et une length. Le StringSegment.Buffer est l’argument de chaîne d’origine et la StringSegment.Value est la sous-chaîne basée sur les valeurs StringSegment.Length et StringSegment.Offset.

La structure StringSegment fournit plusieurs méthodes d’interaction avec le segment.

Le type StringTokenizer

L’objet StringTokenizer est un type de structure qui segmente une string en instances StringSegment. La segmentation de chaînes volumineuses implique généralement de fractionner la chaîne et d’effectuer une itération sur celle-ci. Cela dit, String.Split est probablement la méthode qui vient à l’esprit. Ces API sont similaires, mais en général, StringTokenizer offre de meilleures performances. Commençons par examiner l’exemple suivant :

var tokenizer =
    new StringTokenizer(
        s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
        new[] { ' ' });

foreach (StringSegment segment in tokenizer)
{
    // Interact with segment
}

Dans le code précédent, une instance du type StringTokenizer est créée avec 900 paragraphes générés automatiquement de texte Lorem Ipsum et un tableau avec une valeur unique d’un caractère d’espace blanc ' '. Chaque valeur dans le générateur de jetons est représentée sous la forme d’un StringSegment. Le code itère les segments, ce qui permet au consommateur d’interagir avec chaque segment.

Évaluation de comparaison de StringTokenizer et string.Split

Avec les différentes méthodes de segmentation de chaînes, il convient de comparer deux méthodes à un point de référence. À l’aide du package NuGet BenchmarkDotNet, tenez compte des deux méthodes d’évaluation suivantes :

  1. En utilisant StringTokenizer :

    StringBuilder buffer = new();
    
    var tokenizer =
        new StringTokenizer(
            s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
            new[] { ' ', '.' });
    
    foreach (StringSegment segment in tokenizer)
    {
        buffer.Append(segment.Value);
    }
    
  2. En utilisant String.Split :

    StringBuilder buffer = new();
    
    string[] tokenizer =
        s_nineHundredAutoGeneratedParagraphsOfLoremIpsum.Split(
            new[] { ' ', '.' });
    
    foreach (string segment in tokenizer)
    {
        buffer.Append(segment);
    }
    

Les deux méthodes se ressemblent au niveau de la surface de l’API, et elles sont toutes les deux capables de fractionner une chaîne volumineuse en segments. Les résultats de l’évaluation ci-dessous montrent que l’approche StringTokenizer est presque trois fois plus rapide, mais les résultats peuvent varier. Comme pour toutes les considérations relatives aux performances, vous devez évaluer votre cas d’usage spécifique.

Méthode Moyenne Erreur StdDev Ratio
Générateur de jetons 3,315 ms 0,0659 ms 0,0705 ms 0.32
Split 10,257 ms 0,2018 ms 0,2552 ms 1.00

Légende

  • Moyenne : moyenne arithmétique de toutes les mesures
  • Erreur : moitié de l’intervalle de confiance de 99,9 %
  • Écart type : écart type de toutes les mesures
  • Médiane : valeur séparant la moitié supérieure de toutes les mesures (50e centile)
  • Ratio : moyenne de la distribution de ratios (actuelle/base)
  • Écart type du ratio : écart type de la distribution du ratio (actuel/base)
  • 1 ms : 1 milliseconde (0,001 s)

Pour plus d’informations sur l’évaluation avec .NET, consultez BenchmarkDotNet.

Le type StringValues

L’objet StringValues est un type struct qui représente null, zéro, une ou plusieurs chaînes de manière efficace. Le type StringValues peut être construit avec l’une des syntaxes suivantes : string? ou string?[]?. À l’aide du texte de l’exemple précédent, examinez le code C# suivant :

StringValues values =
    new(s_nineHundredAutoGeneratedParagraphsOfLoremIpsum.Split(
        new[] { '\n' }));

Console.WriteLine($"Count = {values.Count:#,#}");

foreach (string? value in values)
{
    // Interact with the value
}
// Outputs:
//     Count = 1,799

Le code précédent instancie un objet StringValues en fonction d’un tableau de valeurs de chaîne. Le StringValues.Count est écrit dans la console.

Le type StringValues est une implémentation des types de collection suivants :

  • IList<string>
  • ICollection<string>
  • IEnumerable<string>
  • IEnumerable
  • IReadOnlyList<string>
  • IReadOnlyCollection<string>

Par conséquent, il peut être itéré et il est possible d’interagir avec chaque value si nécessaire.

Voir aussi