做网站一定要用ps吗,做网站的公司天津,长沙网站seo多少钱,彩虹云主机示例代码#xff1a;https://download.csdn.net/download/hefeng_aspnet/90959927
找到将 CSV 数据映射到类属性的快速方法
处理 CSV 文件时#xff0c;一个常见的任务是读取文件并使用其内容填充类的属性。但如果可以自动化这个过程会怎样呢#xff1f;在本文中#xf…
示例代码https://download.csdn.net/download/hefeng_aspnet/90959927
找到将 CSV 数据映射到类属性的快速方法
处理 CSV 文件时一个常见的任务是读取文件并使用其内容填充类的属性。但如果可以自动化这个过程会怎样呢在本文中我们将了解如何使用 C# 反射自动将 CSV 文件中的列连接到类的属性。
这个想法很简单CSV 文件的标题应该与类中属性的名称匹配。我的代码将读取该文件并根据这些数据创建类的实例。这样即使 CSV 文件的结构发生变化或者你向类中添加了新的属性你的代码仍然可以正常工作。让我们看看反射如何简化这个过程
但这还不是全部。我们还将探讨另一种使用 C# 源代码生成器的解决方案。源代码生成器允许我们在程序构建过程中创建代码。这可以提高性能并减少运行时使用反射带来的一些问题。在探索这两种方法之后我们将比较它们的性能并分析每种解决方案的优缺点。
测试之间共享的代码
我创建了将用于测试的类和方法。模型类
public class RandomPropertiesClass { public string Name { get; set; } public int Age { get; set; } public double Height { get; set; } public string Surname { get; set; } public int HouseNumber { get; set; } public double Weight { get; set; } public string Address { get; set; } public int TaxCode { get; set; } public double Salary { get; set; } public string City { get; set; } public int ZipCode { get; set; } public double DiscountPercentage { get; set; } public string Country { get; set; } public int PhoneNumber { get; set; } public double Latitude { get; set; } public string Email { get; set; } public int YearsOfExperience { get; set; } public double Longitude { get; set; } public string Profession { get; set; } public int NumberOfChildren { get; set; } public double Temperature { get; set; } public string FavoriteColor { get; set; } public int ShoeSize { get; set; } public double PurchasePrice { get; set; } public string Hobby { get; set; } public int LicenseNumber { get; set; } public double AverageRating { get; set; } public string SpokenLanguage { get; set; } public int RoomNumber { get; set; } public double Balance { get; set; } public string SubscriptionType { get; set; } public int LoyaltyPoints { get; set; } public double DistanceTraveled { get; set; } public string Nationality { get; set; } public int WarrantyYears { get; set; } public double EnergyConsumption { get; set; } public string MaritalStatus { get; set; } public int OrderNumber { get; set; } }
我填充了这个类并用随机数据填充了集合
var faker new FakerRandomPropertiesClass() .RuleFor(r r.Name, f f.Name.FirstName()) .RuleFor(r r.Age, f f.Random.Int(18, 80)) ... .RuleFor(r r.MaritalStatus, f f.PickRandom(new[] { Single, Married, Divorced, Widowed })) .RuleFor(r r.OrderNumber, f f.Random.Int(1000, 9999));
ListRandomPropertiesClass randomData faker.Generate(10_000);
我将此文件保存为 csv 格式。
现在是时候读取此文件并反序列化为 C# 集合对象了。
反射Reflection
让我们开始反射吧。这是我创建的类
public static ListRandomPropertiesClass Import(string[] allRowsFile) { ListRandomPropertiesClass loadedData []; try { // read header string[] headers allRowsFile[0].Split(,); ListPropertyInfo propertiesInfo []; for (var counter 0; counter headers.Length; counter) { propertiesInfo.Add(typeof(RandomPropertiesClass).GetProperty(headers[counter])!); } for (var counter 1; counter allRowsFile.Length; counter) { // read the data var reader allRowsFile[counter]; RandomPropertiesClass item new(); var cols reader.Split(,); for (var i 0; i headers.Length; i) { PropertyInfo prop propertiesInfo[i]; object value Convert.ChangeType(cols[i], prop.PropertyType); prop.SetValue(item, value); } loadedData.Add(item); } return loadedData; } catch (Exception ex) { Console.WriteLine($Generic error: {ex.Message}); return []; } }
此类接受一个字符串数组该数组包含从 csv 文件读取的所有行。第一步是读取包含标题的第一行。我将其内容拆分为单个标题该标题将用于将值映射到属性类中这得益于反射
PropertyInfo prop propertiesInfo[i]; object value Convert.ChangeType(cols[i], prop.PropertyType); prop.SetValue(item, value);
现在该测试一下了
string[] allRowsFile File.ReadAllLines(Configuration.GetCsvFileNameWithPath); Stopwatch sw0 Stopwatch.StartNew(); var result0 MapWithEasyReflection.Import(allRowsFile); sw0.Stop(); ShowData(result0); Console.WriteLine($Reflection easy import {sw0.ElapsedMilliseconds}ms);
并且它运行良好 源生成器(Source generators)
C# 中的源生成器是 2020 年 .NET 5 引入的一项相对较新的功能尽管在编译时生成代码的概念在其他编程环境中已经存在了相当长一段时间。源生成器背后的想法是使开发人员能够在编译过程中动态创建代码通过减少样板代码和增强性能来提高生产力。
最初开发人员通常依赖 T4文本模板转换工具包之类的工具来生成代码。然而T4 模板是作为预构建步骤执行的这意味着生成的代码并未与 C# 编译器紧密集成。这种集成度的缺失可能会导致一些问题因为对生成代码的更改可能不会立即反映在 IDE 中需要手动构建来同步所有内容。
然而源生成器直接嵌入在 Roslyn 编译器C# 编译器平台中并作为构建管道的一部分工作。这种集成意味着源生成器生成的代码在创建后即可供 IDE 使用并实时提供 IntelliSense 支持、错误检测和重构选项等功能。
源代码生成器的主要目的是自动化重复的代码模式提升特定代码结构的性能并帮助开发人员专注于编写核心逻辑而不是冗余或样板代码。这可以加快开发速度减少人为错误并简化维护。
基本示例
using Microsoft.CodeAnalysis;
[Generator] public class CustomSourceGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) {} public void Execute(GeneratorExecutionContext context) { // Analyze compilation and create new source files string generatedCode namespace GeneratedNamespace { public class GeneratedClass { public void GeneratedMethod() { // Generated implementation } } }; context.AddSource(GeneratedFile.cs, generatedCode); } }
这段简单的代码将添加GeneratedClass到我们的源代码中然后可以像任何 C# 对象一样使用。然而源生成器的强大功能之一是能够读取我们的文件并分析代码内容从而动态创建新代码。
从 .NET Core 7 开始您可以使用源生成器版本的正则表达式。语法如下
public partial class Utilities { [GeneratedRegex(^id[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$, RegexOptions.Compiled)] public static partial Regex RegexIdHostName(); }
正则表达式源生成器使用该GeneratedRegex属性来为属性参数中定义的正则表达式模式创建源代码。要添加此新代码该属性必须位于partial类中并且没有主体的方法也必须是partial。这样就可以添加具有相同签名且包含实际函数主体的方法然后可以使用该方法
bool value RegexIdHostName.IsMatch(text_string);
同样在类级别微软工程师也在中使用了源生成器System.Text.Json从而实现了比 Newtonsoft 流行的 Json.NET 类更高的性能。根据文档
如何在 System.Text.Json - .NET 中使用源生成
鉴于以下类
public class WeatherForecast { public DateTime Date { get; set; } public int TemperatureCelsius { get; set; } public string? Summary { get; set; } }
可以使用以下代码创建序列化和反序列化类
[JsonSerializable(typeof(WeatherForecast))] internal partial class SourceGenerationContext : JsonSerializerContext { }
因为它是一个partial类所以将生成可在代码中使用的方法
jsonString JsonSerializer.Serialize( weatherForecast!, SourceGenerationContext.Default.WeatherForecast);
按照这个语法我将创建类似的代码。在最终的代码中我想添加一个方法将字符串映射到我的类如下所示
[GenerateSetPropertyRandomPropertiesClass()] internal static partial class ClassHelper { }
GenerateSetProperty将是我的属性我将在源生成器中使用它来搜索源代码查找partial要添加映射方法的类。泛型参数RandomPropertiesClass将是我要映射属性的类。在我的示例中生成的方法将是
internal static void SetPropertiesRandomPropertiesClass(RandomPropertiesClass obj, ReadOnlySpanchar row)
要创建源生成器我将首先创建一个针对的类库项目netstandard2.0。文件如下.csproj
Project SdkMicrosoft.NET.Sdk PropertyGroup TargetFrameworknetstandard2.0/TargetFramework LangVersion12/LangVersion EnforceExtendedAnalyzerRulestrue/EnforceExtendedAnalyzerRules IsRoslynComponenttrue/IsRoslynComponent Nullableenable/Nullable /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.CodeAnalysis.Analyzers Version3.11.0 PrivateAssetsall/PrivateAssets IncludeAssetsruntime; build; native; contentfiles; analyzers; buildtransitive/IncludeAssets /PackageReference PackageReference IncludeMicrosoft.CodeAnalysis.CSharp Version4.11.0 / PackageReference IncludeIsExternalInit Version1.0.3 PrivateAssetsall / PackageReference IncludeNullable Version1.3.1 PrivateAssetsall / /ItemGroup
/Project
我指定了所需的 C# 版本然后添加了对必要包的引用
Microsoft.CodeAnalysis.分析器 微软代码分析.CSharp 我还添加了IsExternalInit并Nullable解决了访问器和可空类型注释netstandard2.0的限制init。
要引用此项目您需要向ProjectReference添加一些属性
ItemGroup ProjectReference Include..\SourceGeneratorSetProperty\SourceGeneratorSetProperty.csproj ReferenceOutputAssemblyfalse OutputItemTypeAnalyzer / PackageReference IncludeMicrosoft.Net.Compilers.Toolset Version4.11.0 PrivateAssetsall/PrivateAssets IncludeAssetsruntime; build; native; contentfiles; analyzers; buildtransitive/IncludeAssets /PackageReference /ItemGroup
必需的它只是第一个ProjectReference当您从终端使用 dotnet build 命令进行编译时 Microsoft.Net.Compilers.Toolset很有用。
此外如果您使用 VSCode 编辑项目则可以将.csproj中的此代码添加到源生成器创建文件的指定目录中
PropertyGroup Condition$(Configuration) Debug EmitCompilerGeneratedFilestrue/EmitCompilerGeneratedFiles CompilerGeneratedFilesOutputPath$(BaseIntermediateOutputPath)Generated/CompilerGeneratedFilesOutputPath /PropertyGroup
好的现在是时候开始编写源生成器代码了。从 .NET Core 8 开始可以使用IIncrementalGenerator接口代替ISourceGenerator
[Generator(LanguageNames.CSharp)] public sealed class GeneratorFromAttributeClass : IIncrementalGenerator
该接口仅公开一种方法我在我的代码中使用了该方法
public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(GenerateFixedClasses); ... }
RegisterPostInitializationOutput允许我们插入静态代码。我用它来添加GenerateSetPropertyAttribute属性
private static void GenerateFixedClasses(IncrementalGeneratorPostInitializationContext context) { //languagec# var source $$ // auto-generated/ #nullable enable using System; namespace GeneratorFromAttributeExample; [AttributeUsage(AttributeTargets.Class, AllowMultiple true, Inherited false)] internal sealed class GenerateSetPropertyAttributeT() : Attribute where T : class {} ; var fileName GeneratorFromAttributeExample.GenerateSetPropertyAttribute.g.cs; context.AddSource(fileName, source); }
请记住此类项目不导入任何 DLL因此我们无法在代码中创建对此项目中类的引用。任何静态类或必需类都必须直接以代码形式添加。
现在到了更有趣的部分。添加此属性后我们将在源代码中使用它我们需要在代码中进行搜索
public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(GenerateFixedClasses); IncrementalValuesProvider(string TypeName, Accessibility ClassAccessibility, string? Namespaces, MasterType masterType) provider context.SyntaxProvider.ForAttributeWithMetadataName( GeneratorFromAttributeExample.GenerateSetPropertyAttribute1, predicate: FilterClass, transform: CreateObjCollection); var collectedClasses provider.Collect(); context.RegisterSourceOutput(collectedClasses, CreateSourceCode!); }
context.SyntaxProvider.ForAttributeWithMetadataName按属性名称搜索并且必须包含完整的命名空间字符串。由于该属性具有泛型类型因此必须使用“1”。
谓词定义了找到的对象的过滤器
private static bool FilterClass(SyntaxNode node, CancellationToken cancellationToken) node is ClassDeclarationSyntax;
由于该属性附加到一个类因此我将寻找ClassDeclarationSyntax- 如果它附加到一个方法我将使用MethodDeclarationSyntax。
经过第一个过滤后该Transform方法被调用
private static (string TypeName, Accessibility ClassAccessibility, string? Namespaces, MasterType masterType) CreateObjCollection(GeneratorAttributeSyntaxContext context, CancellationToken _) { var symbol (INamedTypeSymbol)context.TargetSymbol; var className symbol.Name; var classDeclarationSyntax (ClassDeclarationSyntax)context.TargetNode; var isPartial classDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword); var isStatic classDeclarationSyntax.Modifiers.Any(SyntaxKind.StaticKeyword); var namespacesClass string.IsNullOrEmpty(symbol.ContainingNamespace.Name) ? null : symbol.ContainingNamespace.ToDisplayString(); MasterType masterType new(className, isPartial, isStatic); Liststring classNames []; foreach (var attributeData in context.Attributes) { var attributeType attributeData.AttributeClass!; var typeArguments attributeType.TypeArguments; var typeSymbol typeArguments[0]; if (classNames.Contains(typeSymbol.Name)) continue; classNames.Add(typeSymbol.Name); var ns string.IsNullOrEmpty(typeSymbol.ContainingNamespace.Name) ? null : typeSymbol.ContainingNamespace.ToDisplayString(); var properties typeSymbol.GetMembers() .OfTypeIPropertySymbol() .Select(t new SubProperty(t.Name, t.Type)).ToList(); masterType.SubTypes.Add(new SubTypeClass( typeSymbol.Name, ns, properties) ); } return (className, symbol.DeclaredAccessibility, namespacesClass, masterType); }
context在类型的对象中GeneratorAttributeSyntaxContext我将从我添加的代码中获得所有必要的信息
[GenerateSetPropertyRandomPropertiesClass()] internal static partial class ClassHelper { }
首先我验证该属性所引用的类。我检查它的类型staticpartial并将此信息与名称和命名空间一起存储因为这些信息将用于创建新的代码。
循环context.Attributes是必要的因为类可以具有多个属性这允许我们为多个类创建映射方法。在这个循环中我检索了泛型属性中定义的类名并将其与属性集合一起保存。
所有这些信息都从函数返回并在最后一步用于创建代码
var collectedClasses provider.Collect(); context.RegisterSourceOutput(collectedClasses, CreateSourceCode!);
最后两行代码创建了所有收集到的信息的集合并将其传递给CreateSourceCode生成最终代码的方法
private static void CreateSourceCode(SourceProductionContext ctx, ImmutableArray(string TypeName, Accessibility ClassAccessibility, string Namespaces, MasterType MasterType) collectedClasses) { foreach (var info in collectedClasses) { if (!info.MasterType.IsPartial || !info.MasterType.IsStatic) { Helper.ReportClassNotSupportedDiagnostic(ctx, info.MasterType.ClassName); continue; } using StringWriter writer new(CultureInfo.InvariantCulture); using IndentedTextWriter tx new(writer); tx.WriteLine(// auto-generated/); tx.WriteLine(#nullable enable); tx.WriteLine(); tx.WriteLine(using GeneratorFromAttributeExample;); if (!string.IsNullOrEmpty(info.Namespaces)) { tx.WriteLine($namespace {info.Namespaces}); tx.WriteLine({); tx.Indent; } tx.WriteLine($[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\{typeof(GeneratorFromAttributeClass).Assembly.GetName().Name}\, \{typeof(GeneratorFromAttributeClass).Assembly.GetName().Version}\)]); tx.WriteLine(${SyntaxFacts.GetText(info.ClassAccessibility)} static partial class {info.TypeName}); tx.WriteLine({); tx.Indent; foreach (var subType in info.MasterType.SubTypes) { var ns string.IsNullOrEmpty(subType.Namespace) ? string.Empty : subType.Namespace! .; tx.WriteLine(${SyntaxFacts.GetText(info.ClassAccessibility)} static void SetProperties{subType.Classname}({ns}{subType.Classname} obj, ReadOnlySpanchar row)); tx.WriteLine({); tx.Indent; InsertPropertiesInSwitch(ctx, tx, subType); tx.Indent--; tx.WriteLine(}); tx.WriteLine(); } tx.Indent--; tx.WriteLine(}); if (!string.IsNullOrEmpty(info.Namespaces)) { tx.Indent--; tx.WriteLine(}); } Debug.Assert(tx.Indent 0); ctx.AddSource($GeneratorFromAttributeExample.{info.TypeName}.g.cs, writer.ToString()); } }
private static void InsertPropertiesInSwitch(SourceProductionContext context, IndentedTextWriter tx, SubTypeClass subType) { tx.WriteLine(var index row.IndexOf(,);); for (var counter 0; counter subType.Properties.Count; counter) { var property subType.Properties[counter]; var members property.Type.GetMembers(); var hasParseMethod members.Any(m m.Name Parse m is IMethodSymbol method method.IsStatic method.Parameters.Length 1 method.Parameters[0].Type.SpecialType SpecialType.System_String); var isLastProperty counter subType.Properties.Count - 1; var sliceString isLastProperty ? row : row.Slice(0, index); tx.WriteLine(hasParseMethod ? $obj.{property.Name} {property.Type.ToDisplayString()}.Parse({sliceString}); : $obj.{property.Name} {sliceString}.ToString(); ); if (!isLastProperty) { tx.WriteLine(row row.Slice(index 1);); tx.WriteLine(index row.IndexOf(,);); } } }
这些方法将最终的源代码创建为字符串。对于类型解析Roslyn 和语法树会检查当前类型是否具有以下Parse方法
var hasParseMethod members.Any(m m.Name Parse m is IMethodSymbol method method.IsStatic method.Parameters.Length 1 method.Parameters[0].Type.SpecialType SpecialType.System_String);
以下是生成的代码的示例
using GeneratorFromAttributeExample; namespace SourceGeneratorVsReflection.SourceGeneratorTest { [global::System.CodeDom.Compiler.GeneratedCodeAttribute(SourceGeneratorSetProperty, 1.0.0.0)] internal static partial class ClassHelper { internal static void SetPropertiesRandomPropertiesClass(SourceGeneratorVsReflection.Models.RandomPropertiesClass obj, ReadOnlySpanchar row) { var index row.IndexOf(,); obj.Name row.Slice(0, index).ToString(); row row.Slice(index 1); index row.IndexOf(,); obj.Age int.Parse(row.Slice(0, index)); row row.Slice(index 1); ... index row.IndexOf(,); obj.OrderNumber int.Parse(row); } } }
相比基于反射的代码ReadOnlySpan这里我也使用这个参数来优化性能因为数值类型的解析方法也支持这个参数。
在 Rider 中代码出现在使用项目的项目结构中 在 Visual Studio 2022 中 在 VSCode 中 现在该测试一切是否正常了 接下来我将使用 Benchmark 类来详细测量性能差异 源生成器版本在不到一半的时间内完成并使用三分之一的内存。
有人可能会认为这是因为使用了 Span所以我创建了一个基于反射的版本也使用了 Span。结果如下 在反射版本中使用 Span 在执行时间和内存使用方面略有优势但仍然远远落后于源生成器版本。
完整的源代码可以在这里找到https://download.csdn.net/download/hefeng_aspnet/90959927
包含一个控制台应用程序在“调试”模式下它会运行一个简单的测试来显示三种方法的结果。在“发布”模式下它会运行上面显示的基准测试。
然而一切真的都是阳光和彩虹吗 看到这些结果你可能会想为什么不把所有用反射编写的代码都移到更现代的源生成器中呢很多类确实可以从中受益。
然而源生成器的主要缺点是开发时间。对于这里展示的类我花费的时间比使用反射多五倍——而且我已经很熟悉源生成器了。此外目前编辑器在流畅处理源生成器方面存在问题。在 Visual Studio 中在初始方法定义和早期测试阶段您经常需要重新启动编辑器因为方法签名不会更新——编辑器一直显示旧版本的方法而且 Visual Studio 通常不会按照我上面展示的结构显示生成的文件等等……
如果您喜欢此文章请收藏、点赞、评论谢谢祝您快乐每一天。