例によるDOTNETコード生成概要
102091 ワード
導入
コード生成は非常に興味深いトピックです.コードを書く代わりに、コードを書くコードを書くことができます.コンパイル時にコード生成を行うことができます(新しいファンシーソースジェネレータ)と実行時(式では、ilを出力).とにかく、ランタイムでメソッドとクラスを作成するアイデアは私に魔法のように聞こえる.私は十分な理解を持っている今、私は私はコード生成を使用して、より効率的でエレガントな方法で解決することがいくつかのタスクを持っていたことに気づいた.残念ながら、私はそれについて何も知りませんでした.インターネットを検索すると、かなり高いエントリの閾値で結果を与え、彼らは機能の全体の理解を与えなかった.記事の例のほとんどは全く些細なので、まだそれを実際に適用する方法は不明です.ここでは、最初のステップとして解決することができる特定の問題を記述したいmetaprogramming そして、異なるコード生成アプローチの概観を与える.たくさんのコードがあります.
タスク説明
我々のアプリケーションがいくつかの源からストリングの配列としてデータを受け取ると想像しましょう(単純なために、ストリング、整数とdatetime値は入力配列で予想されます).
["John McClane", "1994-11-05T13:15:30", "4455"]
この入力を特定のクラスのインスタンスに解析する一般的な方法が必要です.これはパーサデリゲートを作成するインターフェイスですT
出力)public interface IParserFactory
{
Func<string[], T> GetParser<T>() where T : new();
}
私の使用ParserOutputAttribute
パーサーの出力として使用するクラスを識別します.と私はArrayIndexAttribute
配列の要素のそれぞれに対応するプロパティを理解するには、次の手順に従います.[ParserOutput]
public class Data
{
[ArrayIndex(0)] public string Name { get; set; } // will be "John McClane"
[ArrayIndex(2)] public int Number { get; set; } // will be 4455
[ArrayIndex(1)] public DateTime Birthday { get; set; } // will be 1994-11-05T13:15:30
}
配列要素が対象型に解析できない場合は無視されます.一般的な考えとして、実装を制限したくない
Data
クラスのみ.適切な属性を持つ任意の型のパーサーデリゲートを作成します.平時C
まず最初に、既知の型に対して、コード生成やリフレクションなしで単純なCチェックコードを記述します.
var data = new Data();
if (0 < inputArray.Length)
{
data.Name = inputArray[0];
}
if (1 < inputArray.Length && DateTime.TryParse(inputArray[1], out var bd))
{
data.Birthday = bd;
}
if (2 < inputArray.Length && int.TryParse(inputArray[2], out var n))
{
data.Number = n;
}
return data;
全く簡単、右?しかし、実行時やコンパイル時に任意の型に対して同じコードを生成したいと思います.レッツゴー!反射
との最初のアプローチでreflection パーサデリゲートを生成するつもりはありません.代わりに、ターゲット型のインスタンスを作成し、リフレクションAPIを使用してプロパティを設定します.
public class ReflectionParserFactory : IParserFactory
{
public Func<string[], T> GetParser<T>() where T : new()
{
return ArrayIndexParse<T>;
}
private static T ArrayIndexParse<T>(string[] data) where T : new()
{
// create a new instance of target type
var instance = new T();
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
//go through all public and non-static properties
//read and parse corresponding element in array and if success - set property value
for (int i = 0; i < props.Length; i++)
{
var attrs = props[i].GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray();
if (attrs.Length == 0) continue;
int order = ((ArrayIndexAttribute)attrs[0]).Order;
if (order < 0 || order >= data.Length) continue;
if (props[i].PropertyType == typeof(string))
{
props[i].SetValue(instance, data[order]);
continue;
}
if (props[i].PropertyType == typeof(int))
{
if (int.TryParse(data[order], out var intResult))
{
props[i].SetValue(instance, intResult);
}
continue;
}
if (props[i].PropertyType == typeof(DateTime))
{
if (DateTime.TryParse(data[order], out var dtResult))
{
props[i].SetValue(instance, dtResult);
}
}
}
return instance;
}
}
それは動作し、それはかなり読みやすいです.でもそれはslow チェックbenchmarks セクションも参照).このコードを非常に頻繁に呼び出したいならば、それは問題でありえました.私は実際のコード生成を使用してより洗練された何かを実装したい.コード生成
エクスプレッションツリー
からofficial documentation :
Expression trees represent code in a tree-like data structure, where each node is an expression, for example, a method call or a binary operation such as x < y. You can compile and run code represented by expression trees.
Expression.Call
メソッドを呼び出すにはExpression.Loop
繰り返しの論理などを追加するには、これらのブロックを使用してtree
命令の最後に、実行時にデリゲートにコンパイルします.public class ExpressionTreeParserFactory : IParserFactory
{
public Func<string[], T> GetParser<T>() where T : new()
{
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
//declare an input parameter of the delegate
ParameterExpression inputArray = Expression.Parameter(typeof(string[]), "inputArray");
//declare an output parameter of the delegate
ParameterExpression instance = Expression.Variable(typeof(T), "instance");
//create a new instance of target type
var block = new List<Expression>
{
Expression.Assign(instance, Expression.New(typeof(T).GetConstructors()[0]))
};
var variables = new List<ParameterExpression> {instance};
//go through all public and non-static properties
foreach (var prop in props)
{
var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray();
if (attrs.Length == 0) continue;
int order = ((ArrayIndexAttribute)attrs[0]).Order;
if (order < 0) continue;
//validate an index from ArrayIndexAttribute
var orderConst = Expression.Constant(order);
var orderCheck = Expression.LessThan(orderConst, Expression.ArrayLength(inputArray));
if (prop.PropertyType == typeof(string))
{
//set string property
var stringPropertySet = Expression.Assign(
Expression.Property(instance, prop),
Expression.ArrayIndex(inputArray, orderConst));
block.Add(Expression.IfThen(orderCheck, stringPropertySet));
continue;
}
//get parser method from the list of available parsers (currently we parse only Int and DateTime)
if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser))
{
continue;
}
var parseResult = Expression.Variable(prop.PropertyType, "parseResult");
var parserCall = Expression.Call(parser, Expression.ArrayIndex(inputArray, orderConst), parseResult);
var propertySet = Expression.Assign(
Expression.Property(instance, prop),
parseResult);
//set property if an element of array is successfully parsed
var ifSet = Expression.IfThen(parserCall, propertySet);
block.Add(Expression.IfThen(orderCheck, ifSet));
variables.Add(parseResult);
}
block.Add(instance);
//compile lambda expression into delegate
return Expression.Lambda<Func<string[], T>>(
Expression.Block(variables.ToArray(), Expression.Block(block)),
inputArray).Compile();
}
}
放出する
DotNetコンパイラはC言語のコードを中間言語に変換しますCIL or just IL ) それから、dotnetランタイムはILを機械命令に翻訳します.例えば、sharplab.io 簡単にILがどのように見えるか確認できます.
public class EmitIlParserFactory : IParserFactory
{
public Func<string[], T> GetParser<T>() where T : new()
{
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
var dm = new DynamicMethod($"from_{typeof(string[]).FullName}_to_{typeof(T).FullName}",
typeof(T), new [] { typeof(string[]) }, typeof(EmitIlParserFactory).Module);
var il = dm.GetILGenerator();
//create a new instance of target type
var instance = il.DeclareLocal(typeof(T));
il.Emit(OpCodes.Newobj, typeof(T).GetConstructors()[0]);
il.Emit(OpCodes.Stloc, instance);
//go through all public and non-static properties
foreach (var prop in props)
{
var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray();
if (attrs.Length == 0) continue;
int order = ((ArrayIndexAttribute)attrs[0]).Order;
if (order < 0) continue;
var label = il.DefineLabel();
if (prop.PropertyType == typeof(string))
{
//check whether order from ArrayIndexAttribute is a valid index of the input array
il.Emit(OpCodes.Ldc_I4, order);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldlen);
il.Emit(OpCodes.Bge_S, label);
//set string property
il.Emit(OpCodes.Ldloc, instance);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, order);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Callvirt, prop.GetSetMethod());
il.MarkLabel(label);
continue;
}
//get parser method from the list of available parsers (currently we parse only Int and DateTime)
if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser))
{
continue;
}
//check whether order from ArrayIndexAttribute is a valid index of the input array
il.Emit(OpCodes.Ldc_I4, order);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldlen);
il.Emit(OpCodes.Bge_S, label);
var parseResult = il.DeclareLocal(prop.PropertyType);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, order);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Ldloca, parseResult);
il.EmitCall(OpCodes.Call, parser, null);
il.Emit(OpCodes.Brfalse_S, label);
//set property if an element of array is successfully parsed
il.Emit(OpCodes.Ldloc, instance);
il.Emit(OpCodes.Ldloc, parseResult);
il.Emit(OpCodes.Callvirt, prop.GetSetMethod());
il.MarkLabel(label);
}
il.Emit(OpCodes.Ldloc, instance);
il.Emit(OpCodes.Ret);
//create delegate from il instructions
return (Func<string[], T>)dm.CreateDelegate(typeof(Func<string[], T>));
}
}
シグイル
public class SigilParserFactory : IParserFactory
{
public Func<string[], T> GetParser<T>() where T : new()
{
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
var il = Emit<Func<string[], T>>.NewDynamicMethod($"from_{typeof(string[]).FullName}_to_{typeof(T).FullName}");
var instance = il.DeclareLocal<T>();
il.NewObject<T>();
il.StoreLocal(instance);
foreach (var prop in props)
{
var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray();
if (attrs.Length == 0) continue;
int order = ((ArrayIndexAttribute)attrs[0]).Order;
if (order < 0) continue;
var label = il.DefineLabel();
if (prop.PropertyType == typeof(string))
{
il.LoadConstant(order);
il.LoadArgument(0);
il.LoadLength<string>();
il.BranchIfGreaterOrEqual(label);
il.LoadLocal(instance);
il.LoadArgument(0);
il.LoadConstant(order);
il.LoadElement<string>();
il.CallVirtual(prop.GetSetMethod());
il.MarkLabel(label);
continue;
}
if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser))
{
continue;
}
il.LoadConstant(order);
il.LoadArgument(0);
il.LoadLength<string>();
il.BranchIfGreaterOrEqual(label);
var parseResult = il.DeclareLocal(prop.PropertyType);
il.LoadArgument(0);
il.LoadConstant(order);
il.LoadElement<string>();
il.LoadLocalAddress(parseResult);
il.Call(parser);
il.BranchIfFalse(label);
il.LoadLocal(instance);
il.LoadLocal(parseResult);
il.CallVirtual(prop.GetSetMethod());
il.MarkLabel(label);
}
il.LoadLocal(instance);
il.Return();
return il.CreateDelegate();
}
}
キャッシュコンパイルパーサー
我々は、パーサーデリゲートを作成するために3つのアプローチを実装しました:式木、ilとsigilを放出します.すべてのケースでは、同じ問題があります.
IParserFactory.GetParser
あなたがそれを呼ぶたびに、ハード仕事(表現木を構築するか、ILを放出して、それから委任を作成して)をします.解決策はとても簡単です-ただキャッシュしてください.public class CachedParserFactory : IParserFactory
{
private readonly IParserFactory _realParserFactory;
private readonly ConcurrentDictionary<string, Lazy<object>> _cache;
public CachedParserFactory(IParserFactory realParserFactory)
{
_realParserFactory = realParserFactory;
_cache = new ConcurrentDictionary<string, Lazy<object>>();
}
public Func<string[], T> GetParser<T>() where T : new()
{
return (Func<string[], T>)(_cache.GetOrAdd($"aip_{_realParserFactory.GetType().FullName}_{typeof(T).FullName}",
new Lazy<object>(() => _realParserFactory.GetParser<T>(), LazyThreadSafetyMode.ExecutionAndPublication)).Value);
}
}
今より効率的なデリゲートのコンパイルされたバージョンを再利用します.ロスリンベースアプローチ
Roslynはコードをコンパイルするだけでなく、構文解析をしてコードを生成する能力を与えるdotnetコンパイラプラットフォームです.
Roslynランタイムコード生成
public static class RoslynParserInitializer
{
public static IParserFactory CreateFactory()
{
//get all types marked with ParserOutputAttribute
var targetTypes =
(from a in AppDomain.CurrentDomain.GetAssemblies()
from t in a.GetTypes()
let attributes = t.GetCustomAttributes(typeof(ParserOutputAttribute), true)
where attributes != null && attributes.Length > 0
select t).ToArray();
var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>();
var builder = new StringBuilder();
builder.AppendLine(@"
using System;
using Parsers.Common;
public class RoslynGeneratedParserFactory : IParserFactory
{");
//go through all types
foreach (var targetType in targetTypes)
{
var targetTypeName = targetType.Name;
var targetTypeFullName = targetType.FullName;
var targetTypeParserName = targetTypeName + "Parser";
typeNames.Add((targetTypeName, targetTypeFullName, targetTypeParserName));
//generate private parser method for each target type
builder.AppendLine($"private static T {targetTypeParserName}<T>(string[] input)");
builder.Append($@"
{{
var {targetTypeName}Instance = new {targetTypeFullName}();");
var props = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
//go through all properties of the target type
foreach (var prop in props)
{
var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray();
if (attrs.Length == 0) continue;
int order = ((ArrayIndexAttribute)attrs[0]).Order;
if (order < 0) continue;
if (prop.PropertyType == typeof(string))
{
builder.Append($@"
if({order} < input.Length)
{{
{targetTypeName}Instance.{prop.Name} = input[{order}];
}}
");
}
if (prop.PropertyType == typeof(int))
{
builder.Append($@"
if({order} < input.Length && int.TryParse(input[{order}], out var parsed{prop.Name}))
{{
{targetTypeName}Instance.{prop.Name} = parsed{prop.Name};
}}
");
}
if (prop.PropertyType == typeof(DateTime))
{
builder.Append($@"
if({order} < input.Length && DateTime.TryParse(input[{order}], out var parsed{prop.Name}))
{{
{targetTypeName}Instance.{prop.Name} = parsed{prop.Name};
}}
");
}
}
builder.Append($@"
object obj = {targetTypeName}Instance;
return (T)obj;
}}");
}
builder.AppendLine("public Func<string[], T> GetParser<T>() where T : new() {");
foreach (var typeName in typeNames)
{
builder.Append($@"
if (typeof(T) == typeof({typeName.TargetTypeFullName}))
{{
return {typeName.TargetTypeParserName}<T>;
}}
");
}
builder.AppendLine("throw new NotSupportedException();}");
builder.AppendLine("}");
var syntaxTree = CSharpSyntaxTree.ParseText(builder.ToString());
//reference assemblies
string assemblyName = Path.GetRandomFileName();
var refPaths = new List<string> {
typeof(Object).GetTypeInfo().Assembly.Location,
typeof(Enumerable).GetTypeInfo().Assembly.Location,
Path.Combine(Path.GetDirectoryName(typeof(GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll"),
typeof(RoslynParserInitializer).GetTypeInfo().Assembly.Location,
typeof(IParserFactory).GetTypeInfo().Assembly.Location,
Path.Combine(Path.GetDirectoryName(typeof(GCSettings).GetTypeInfo().Assembly.Location), "netstandard.dll"),
};
refPaths.AddRange(targetTypes.Select(x => x.Assembly.Location));
var references = refPaths.Select(r => MetadataReference.CreateFromFile(r)).ToArray();
// compile dynamic code
var compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
//compile assembly
using (var ms = new MemoryStream())
{
var result = compilation.Emit(ms);
//to get a proper errors
if (!result.Success)
{
throw new Exception(string.Join(",", result.Diagnostics.Where(diagnostic =>
diagnostic.IsWarningAsError ||
diagnostic.Severity == DiagnosticSeverity.Error).Select(x => x.GetMessage())));
}
ms.Seek(0, SeekOrigin.Begin);
// load assembly from memory
var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
var factoryType = assembly.GetType("RoslynGeneratedParserFactory");
if (factoryType == null) throw new NullReferenceException("Roslyn generated parser type not found");
//create an instance of freshly generated parser factory
return (IParserFactory)Activator.CreateInstance(factoryType);
}
}
}
発生源
[Generator]
public class ParserSourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
//uncomment to debug
//System.Diagnostics.Debugger.Launch();
}
public void Execute(GeneratorExecutionContext context)
{
var compilation = context.Compilation;
var parserOutputTypeSymbol = compilation.GetTypeByMetadataName("Parsers.Common.ParserOutputAttribute");
var attributeIndexTypeSymbol = compilation.GetTypeByMetadataName("Parsers.Common.ArrayIndexAttribute");
var typesToParse = new List<ITypeSymbol>();
foreach (var syntaxTree in compilation.SyntaxTrees)
{
var semanticModel = compilation.GetSemanticModel(syntaxTree);
//get all types marked with ParserOutputAttribute
typesToParse.AddRange(syntaxTree.GetRoot()
.DescendantNodesAndSelf()
.OfType<ClassDeclarationSyntax>()
.Select(x => semanticModel.GetDeclaredSymbol(x))
.OfType<ITypeSymbol>()
.Where(x => x.GetAttributes().Select(a => a.AttributeClass)
.Any(b => b == parserOutputTypeSymbol)));
}
var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>();
var builder = new StringBuilder();
builder.AppendLine(@"
using System;
using Parsers.Common;
namespace BySourceGenerator
{
public class Parser : IParserFactory
{");
//go through all types
foreach (var typeSymbol in typesToParse)
{
var targetTypeName = typeSymbol.Name;
var targetTypeFullName = GetFullName(typeSymbol);
var targetTypeParserName = targetTypeName + "Parser";
typeNames.Add((targetTypeName, targetTypeFullName, targetTypeParserName));
builder.AppendLine($"private static T {targetTypeParserName}<T>(string[] input)");
builder.Append($@"
{{
var {targetTypeName}Instance = new {targetTypeFullName}();");
var props = typeSymbol.GetMembers().OfType<IPropertySymbol>();
//go through all properties of the target type
foreach (var prop in props)
{
var attr = prop.GetAttributes().FirstOrDefault(x => x.AttributeClass == attributeIndexTypeSymbol);
if (attr == null || !(attr.ConstructorArguments[0].Value is int)) continue;
int order = (int) attr.ConstructorArguments[0].Value;
if (order < 0) continue;
if (GetFullName(prop.Type) == "System.String")
{
builder.Append($@"
if({order} < input.Length)
{{
{targetTypeName}Instance.{prop.Name} = input[{order}];
}}
");
}
if (GetFullName(prop.Type) == "System.Int32")
{
builder.Append($@"
if({order} < input.Length && int.TryParse(input[{order}], out var parsed{prop.Name}))
{{
{targetTypeName}Instance.{prop.Name} = parsed{prop.Name};
}}
");
}
if (GetFullName(prop.Type) == "System.DateTime")
{
builder.Append($@"
if({order} < input.Length && DateTime.TryParse(input[{order}], out var parsed{prop.Name}))
{{
{targetTypeName}Instance.{prop.Name} = parsed{prop.Name};
}}
");
}
}
builder.Append($@"
object obj = {targetTypeName}Instance;
return (T)obj;
}}");
}
builder.AppendLine("public Func<string[], T> GetParser<T>() where T : new() {");
foreach (var typeName in typeNames)
{
builder.Append($@"
if (typeof(T) == typeof({typeName.TargetTypeFullName}))
{{
return {typeName.TargetTypeParserName}<T>;
}}
");
}
builder.AppendLine("throw new NotSupportedException();}");
builder.AppendLine("}}");
var src = builder.ToString();
context.AddSource(
"ParserGeneratedBySourceGenerator.cs",
SourceText.From(src, Encoding.UTF8)
);
}
private static string GetFullName(ITypeSymbol typeSymbol) =>
$"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}";
}
ベンチマーク
ポストはベンチマークなしで包括的でありません.二つのことを比較したいです.
μs
- マイクロセカンドns
- ナノ秒,1μs=1000 ns
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1237 (21H1/May2021Update)
Intel Core i7-8550U CPU 1.80GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET SDK=5.0.401
[Host] : .NET 5.0.10 (5.0.1021.41214), X64 RyuJIT
DefaultJob : .NET 5.0.10 (5.0.1021.41214), X64 RyuJIT
パーサの生成
方法
平均
エラー
stddev
ジェン0
ジェン1
ゲン2
割り当てた
エミテン
22.02μs
0.495μs
1.429μs
1.2817
0.6409
0.0305
5 KB
エクスプレッションツリー
683.68μs
13.609μs
31.268μs
2.9297
0.9766
-
14 KB
シグイル
642.63μs
12.305μs
29.243μs
112.3047
-
-
460 KB
ロスリン
71605.64μs
2533.732μs
7350.817μs
1000.0000
-
-
5826 KB
パーサーの呼び出し
方法
平均
エラー
stddev
比率
聯合
ジェン0
割り当てた
エミテン
374.7 ns
7.75 ns
22.36 ns
1.02
0.08
0.0095
40 b
エクスプレッションツリー
378.1 ns
7.56 ns
20.57 ns
1.03
0.08
0.0095
40 b
反射
13625.0 ns
272.60 ns
750.81 ns
37.29
2.29
0.7782
3256 B
シグイル
378.9 ns
7.69 ns
21.06 ns
1.03
0.07
0.0095
40 b
ロスリン
404.2 NS
7.55 ns
17.80 ns
1.10
0.07
0.0095
40 b
発電機
384.4 ns
7.79 ns
21.46 ns
1.05
0.08
0.0095
40 b
マニュアル
367.8 ns
7.36 ns
15.68 ns
1.00
0.00
0.0095
40 b
反射の直接の使用の他のすべてのアプローチは、結果をほとんど同じにしますmanually written C# parser .
ソースコード
こちらgithub repository パーサーファクトリ、ユニットテスト、ベンチマーク.
Reference
この問題について(例によるDOTNETコード生成概要), 我々は、より多くの情報をここで見つけました https://dev.to/maximtkachenko/dotnet-code-generation-overview-by-example-1m16テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol