Mengganti Peristiwa C # dengan Ekstensi Reaktif Menggunakan Pembuatan Kode

Halo, nama saya Ivan dan saya adalah seorang pengembang.





Konferensi .NETConf 2020 diadakan baru-baru ini, bertepatan dengan rilis .NET 5, di mana salah satu pembicara berbicara tentang Generator Sumber C # . Mencari di youtube saya menemukan video bagus lainnya tentang topik ini . Saya menyarankan Anda untuk menontonnya. Mereka menunjukkan bagaimana, saat pengembang menulis kode, kode dibuat, dan InteliSense segera mengambil kode yang dihasilkan, menawarkan metode dan properti yang dihasilkan, dan kompilator tidak bersumpah atas ketidakhadirannya. Menurut saya, ini adalah kesempatan yang baik untuk mengembangkan kemampuan bahasanya dan saya akan mencoba mendemonstrasikannya.





Ide

Apakah ada yang tahu LINQ ? Jadi untuk acara ada pustaka serupa Reactive Extensions , yang memungkinkan Anda memproses acara dengan cara yang sama seperti LINQ .





Masalahnya adalah untuk menggunakan Ekstensi Reaktif, Anda perlu mengatur acara dalam bentuk Ekstensi Reaktif, dan karena semua peristiwa di pustaka standar ditulis dalam bentuk standar, tidak nyaman menggunakan Ekstensi Reaktif. Ada kruk yang mengubah kejadian C # standar menjadi Ekstensi Reaktif. Ini terlihat seperti ini. Katakanlah ada kelas dengan beberapa acara:





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



Untuk menggunakan acara ini dalam gaya Ekstensi Reaktif , Anda perlu menulis metode ekstensi tampilan:





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



Dan setelah itu, Anda bisa memanfaatkan semua keunggulan Reactive Extensions , misalnya seperti ini:





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



Jadi, idenya adalah kruk ini dihasilkan dengan sendirinya, dan metode dapat digunakan dari InteliSense selama pengembangan.





Sebuah tugas

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 , .





 

Kode generator cocok dengan 140 baris. Dalam 140 baris ini, ternyata untuk mengubah sintaks bahasa, menyingkirkan peristiwa dengan menggantinya dengan Ekstensi Reaktif dengan pendekatan yang lebih nyaman, menurut pendapat saya. Saya pikir teknologi generator kode sumber akan sangat mengubah pendekatan untuk mengembangkan perpustakaan dan ekstensi.





Tautan

NuGet





Github








All Articles