Olá, meu nome é Ivan e sou desenvolvedor.
Recentemente foi realizada a NETConf 2020, programada para coincidir com o lançamento do .NET 5. Um dos palestrantes falou sobre Geradores de Fontes C # . Depois de pesquisar no youtube, encontrei outro bom vídeo sobre esse assunto . Aconselho você a observá-los. Eles mostram como, enquanto o desenvolvedor está escrevendo o código, o código é gerado e o InteliSense imediatamente pega o código gerado, oferece os métodos e propriedades gerados e o compilador não jura por sua ausência. Na minha opinião, esta é uma boa oportunidade para expandir as capacidades do idioma e tentarei demonstrar isso.
Idéia
Alguém conhece o LINQ ? Portanto, para eventos, há uma biblioteca similar Extensões reativas , que permite processar eventos da mesma forma que LINQ .
O problema é que, para usar extensões reativas, você precisa organizar eventos na forma de extensões reativas e, como todos os eventos nas bibliotecas padrão são escritos em uma forma padrão, não é conveniente usar extensões reativas. Existe uma muleta que converte eventos C # padrão em extensões reativas. Se parece com isso. Digamos que haja uma aula com algum evento:
public partial class Example
{
public event Action<int, string, bool> ActionEvent;
}
Para usar este evento no estilo de extensões reativas , você precisa escrever um método de extensão de visualização:
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);
}
E depois disso você pode aproveitar todas as vantagens das Extensões Reativas , por exemplo:
var example = new Example();
example.RxActionEvent().Where(obj => obj.Item1 > 10).Take(1).Subscribe((obj)=> { /* some action */});
Então, a ideia é que essa muleta seja gerada por si mesma, e os métodos do InteliSense possam ser usados durante o desenvolvimento.
Uma tarefa
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 .
, :
->
-
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 , , .
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 , .
O código do gerador cabe em 140 linhas. Nessas 140 linhas, acabou mudando a sintaxe da linguagem, livrando-se de eventos substituindo-os por Extensões Reativas com uma abordagem mais conveniente, na minha opinião. Eu acho que a tecnologia de geradores de código-fonte mudará muito a abordagem para o desenvolvimento de bibliotecas e extensões.