Remplacement d'événements C # par des extensions réactives à l'aide de la génération de code

Bonjour, je m'appelle Ivan et je suis développeur.





NETConf 2020, programmé pour coïncider avec la sortie de .NET 5. L'un des orateurs a parlé des générateurs de sources C # . Après une recherche sur youtube, j'ai trouvé une autre bonne vidéo sur ce sujet . Je vous conseille de les regarder. Ils montrent comment, pendant que le développeur écrit le code, le code est généré et InteliSense récupère immédiatement le code généré, propose les méthodes et propriétés générées, et le compilateur ne jure pas de leur absence. À mon avis, c'est une bonne occasion d'élargir les capacités de la langue et je vais essayer de le démontrer.





Idée

Quelqu'un connaît-il LINQ ? Donc, pour les événements, il existe une bibliothèque similaire Reactive Extensions , qui vous permet de traiter les événements de la même manière que LINQ .





Le problème est que pour utiliser les extensions réactives, vous devez organiser les événements sous la forme d'extensions réactives, et comme tous les événements des bibliothèques standard sont écrits sous une forme standard, il n'est pas pratique d'utiliser les extensions réactives. Il existe une béquille qui convertit les événements C # standard en extensions réactives. Ça ressemble à ça. Disons qu'il y a une classe avec un événement:





public partial class Example
{
    public event Action<int, string, bool> ActionEvent;
}
      
      



Pour utiliser cet événement dans le style Reactive Extensions , vous devez écrire une méthode d'extension de vue:





public static IObservable<(int, string, bool)> RxActionEvent(this TestConsoleApp.Example obj)
{
    if (obj == null) throw new ArgumentNullException(nameof(obj));
    return Observable.FromEvent<System.Action<int, string, bool>, (int, string, bool)>(
    conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)),
    h => obj.ActionEvent += h,
    h => obj.ActionEvent -= h);
}
      
      



Et après cela, vous pouvez profiter de tous les avantages des extensions réactives , par exemple, comme ceci:





var example = new  Example();
example.RxActionEvent().Where(obj => obj.Item1 > 10).Take(1).Subscribe((obj)=> { /* some action  */});
      
      



Donc, l'idée est que cette béquille est générée par elle-même et que les méthodes peuvent être utilisées à partir d'InteliSense pendant le développement.





Une tâche

1)  «.» «Rx», , example.RxActionEvent()



, , , Action ActionEvent, .RxActionEvent()



, :





public static IObservable<(System.Int32 Item1Int32, System.String Item2String, System.Boolean Item3Boolean)> RxActionEvent(this TestConsoleApp.Example obj)
{
    if (obj == null) throw new ArgumentNullException(nameof(obj));
    return Observable.FromEvent<System.Action<System.Int32, System.String, System.Boolean>, (System.Int32 Item1Int32, System.String Item2String, System.Boolean 
Item3Boolean)>(
    conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)),
    h => obj.ActionEvent += h,
    h => obj.ActionEvent -= h);
}
      
      



2) InteliSense .





 

2 .





:





<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
  </ItemGroup>
</Project>
      
      



, netstandard2.0 2 Microsoft.CodeAnalysis.Analyzers Microsoft.CodeAnalysis.CSharp.Workspaces.





:





<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.Reactive" Version="5.0.0" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
  </ItemGroup>
</Project>
      
      



, :





<ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
      
      



 

[Generator]



ISourceGenerator:





[Generator]
public class RxGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)  {  }
    public void Execute(GeneratorExecutionContext context)  {  }
}
      
      



M Initialize , Execute .





Initialize ISyntaxReceiver.





, :





  • ->





  • ISyntaxReceiver->





  • ISyntaxReceiver , ->





  • Execute ISyntaxReceiver, .





, :





[Generator]
public class RxGenerator : ISourceGenerator
{
    private const string firstText = @"using System; using System.Reactive.Linq; namespace RxGenerator{}";
    public void Initialize(GeneratorInitializationContext context)
    {
        //  ISyntaxReceiver
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }
    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver) return;
        //      "RxGenerator.cs"  ,   firstText
        context.AddSource("RxGenerator.cs", firstText);
    }
    class SyntaxReceiver : ISyntaxReceiver
    {
        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
        //     ,     .
        }
    }
}
      
      



VS, using RxGenerator;



VS.





ISyntaxReceiver

OnVisitSyntaxNode MemberAccessExpressionSyntax.





private class SyntaxReceiver : ISyntaxReceiver
{
    public List<MemberAccessExpressionSyntax> GenerateCandidates { get; } =
        new List<MemberAccessExpressionSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (!(syntaxNode is MemberAccessExpressionSyntax syntax)) return;
        if (syntax.HasTrailingTrivia || syntax.Name.IsMissing) return;
        if (!syntax.Name.ToString().StartsWith("Rx")) return;
        GenerateCandidates.Add(syntax);

    }
}
      
      



:





  • syntax.Name.IsMissing







  • syntax.HasTrailingTrivia



    -





  • !syntax.Name.ToString().StartsWith("Rx")



    "Rx"





, .





     

:





  •  ,    





  •   . , 





    System.Action<System.Int32, System.String, System.Boolean,xSouceGeneratorXUnitTests.SomeEventArgs>







  •     





:





private static IEnumerable<(string ClassType, string EventName, string EventType, List<string> ArgumentTypes)>
    GetExtensionMethodInfo(GeneratorExecutionContext context, SyntaxReceiver receiver)
{
    HashSet<(string ClassType, string EventName)>
        hashSet = new HashSet<(string ClassType, string EventName)>();
    foreach (MemberAccessExpressionSyntax syntax in receiver.GenerateCandidates)
    {
        SemanticModel model = context.Compilation.GetSemanticModel(syntax.SyntaxTree);
        ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch
        {
            IMethodSymbol s => s.ReturnType,
            ILocalSymbol s => s.Type,
            IPropertySymbol s => s.Type,
            IFieldSymbol s => s.Type,
            IParameterSymbol s => s.Type,
            _ => null
        };
        if (typeSymbol == null) continue;

...
      
      



SemanticModel. . ITypeSymbol. ITypeSymbol .





...
        string eventName = syntax.Name.ToString().Substring(2);

        if (!(typeSymbol.GetMembersOfType<IEventSymbol>().FirstOrDefault(m => m.Name == eventName) is { } ev)
        ) continue;

        if (!(ev.Type is INamedTypeSymbol namedTypeSymbol)) continue;
        if (namedTypeSymbol.DelegateInvokeMethod == null) continue;
        if (!hashSet.Add((typeSymbol.ToString(), ev.Name))) continue;

        string fullType = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat);
        List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters
            .Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList();
        yield return (typeSymbol.ToString(), ev.Name, fullType, typeArguments);
    }
}
      
      



:





string fullType = namedTypeSymbol.ToDisplayString(symbolDisplayFormat);
      
      



SymbolDisplayFormat SymbolDisplayFormat ToDisplayString() . ToDisplayString() :





System.Action<System.Int32, System.String, System.Boolean, RxSouceGeneratorXUnitTests.SomeEventArgs>
      
      







Action<int, string, bool, SomeEventArgs>
      
      



.





:





List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters.Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList();
      
      



.





StringBuilder , , .





Execute:





Spoiler
public void Execute(GeneratorExecutionContext context)
{
    if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) return;

    if (!(receiver.GenerateCandidates.Any()))
    {
        context.AddSource("RxGenerator.cs", startText);
        return;
    }

    StringBuilder sb = new();
    sb.AppendLine("using System;");
    sb.AppendLine("using System.Reactive.Linq;");
    sb.AppendLine("namespace RxMethodGenerator{");
    sb.AppendLine("    public static class RxGeneratedMethods{");

    foreach ((string classType, string eventName, string eventType, List<string> argumentTypes) in
        GetExtensionMethodInfo(context,
            receiver))
    {
        string tupleTypeStr;
        string conversionStr;

        switch (argumentTypes.Count)
        {
            case 0:
                tupleTypeStr = classType;
                conversionStr = "conversion => () => conversion(obj),";
                break;
            case 1:
                tupleTypeStr = argumentTypes.First();
                conversionStr = "conversion => obj1 => conversion(obj1),";
                break;
            default:
                tupleTypeStr =
                    $"({string.Join(", ", argumentTypes.Select((x, i) => $"{x} Item{i + 1}{x.Split('.').Last()}"))})";
                string objStr = string.Join(", ", argumentTypes.Select((x, i) => $"obj{i}"));
                conversionStr = $"conversion => ({objStr}) => conversion(({objStr})),";
                break;
        }

        sb.AppendLine(@$"        public static IObservable<{tupleTypeStr}> Rx{eventName}(this {classType} obj)");
        sb.AppendLine( @"        {");

        sb.AppendLine(  "            if (obj == null) throw new ArgumentNullException(nameof(obj));");
        sb.AppendLine(@$"            return Observable.FromEvent<{eventType}, {tupleTypeStr}>(");
        sb.AppendLine(@$"            {conversionStr}");
        sb.AppendLine(@$"            h => obj.{eventName} += h,");
        sb.AppendLine(@$"            h => obj.{eventName} -= h);");

        sb.AppendLine(  "        }");
    }
    sb.AppendLine(      "    }");
    sb.AppendLine(      "}");

    context.AddSource("RxGenerator.cs", sb.ToString());
}
      
      







  InteliSense     

«.» InteliSense . . «.» . , MS . .





CompletionProvider InteliSense «.». NuGet, .





.





CompletionProvider , , CompletionProvider:





public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{
    switch (trigger.Kind)
    {
        case CompletionTriggerKind.Insertion:
            int insertedCharacterPosition = caretPosition - 1;
            if (insertedCharacterPosition <= 0) return false;
            char ch = text[insertedCharacterPosition];
            char previousCh = text[insertedCharacterPosition - 1];
            return ch == '.' && !char.IsWhiteSpace(previousCh) && previousCh != '\t' && previousCh != '\r' && previousCh != '\n';
        default:
            return false;
    }
}
      
      



«.» - .





True , InteliSense:





public override async Task ProvideCompletionsAsync(CompletionContext context)
{
    SyntaxNode? syntaxNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
    if (!(syntaxNode?.FindNode(context.CompletionListSpan) is ExpressionStatementSyntax
        expressionStatementSyntax)) return;
    if (!(expressionStatementSyntax.Expression is MemberAccessExpressionSyntax syntax)) return;
    if (!(await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is { }
        model)) return;

    ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch
    {
        IMethodSymbol s => s.ReturnType,
        ILocalSymbol s => s.Type,
        IPropertySymbol s => s.Type,
        IFieldSymbol s => s.Type,
        IParameterSymbol s => s.Type,
        _ => null
    };
    if (typeSymbol == null) return;

    foreach (IEventSymbol ev in typeSymbol.GetMembersOfType<IEventSymbol>())
    {
        ...        
        //     InteliSense
        CompletionItem item = CompletionItem.Create($"Rx{ev.Name}");
        context.AddItem(item);
    }
}
      
      



, , .





, InteliSense:





public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
    return Task.FromResult(CompletionDescription.FromText(" "));
}
      
      



InteliSense , , «.» :





public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item,
    char? commitKey, CancellationToken cancellationToken)
{
    string newText = $".{item.DisplayText}()";
    TextSpan newSpan = new TextSpan(item.Span.Start - 1, 1);

    TextChange textChange = new TextChange(newSpan, newText);
    return await Task.FromResult(CompletionChange.Create(textChange));
}
      
      



!





    

Visual Studio №16.8.3. GitHub Visual Studio. Rider ReSharper 2020.3. ReSharper , 2020.3.





, . WPF , GitHub Roslyn.





CompletionProvider Vsix . NuGet . . using , NuGet.





   

Initialize Debugger.Launch();



VS





public void Initialize(GeneratorInitializationContext context)
{
    #if (DEBUG)
    Debugger.Launch();
    #endif
    context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
      
      



. - VS, .





CompletionProvider VS «Analyzer with code Fix». , Vsix. CompletionProvider , .





 

Le code du générateur tient en 140 lignes. Dans ces 140 lignes, nous avons réussi à changer la syntaxe du langage, à nous débarrasser des événements en les remplaçant par des extensions réactives avec une approche plus pratique, à mon avis. Je pense que la technologie des générateurs de code source changera considérablement l'approche du développement de bibliothèques et d'extensions.





Liens

NuGet





Github








All Articles