.NET源码生成器之SyntaxTree踩坑

一、不可变性的坑

1. 节点不可变

  • record调用AddParameterListParameters后record并不会修改
  • 以下Case中record的代码依然是record Person;
csharp 复制代码
var record = SyntaxGenerator.RecordDeclaration("Person")
    .WithSemicolonToken();
record.AddParameterListParameters(SyntaxGenerator.StringType.Parameter("Name"));
// record Person;

2. 可以用变量接收更改后的结果

  • record和record2是不一样的
  • record代码为record Person;
  • record2代码为record Person(string Name);
csharp 复制代码
var record = SyntaxGenerator.RecordDeclaration("Person")
    .WithSemicolonToken();
var record2 = record.AddParameterListParameters(SyntaxGenerator.StringType.Parameter("Name"));

3. 不可变性的集合

  • SyntaxTree常用的SyntaxList和SeparatedSyntaxList都是不可变的
  • 以下集合初始化和Add都无效
  • 需要特别注意
  • 如果用变量赋值Add的返回值是可以的
csharp 复制代码
var list = new SeparatedSyntaxList<ParameterSyntax>
{
    SyntaxGenerator.IntType.Parameter("Id")
};
list.Add(SyntaxGenerator.StringType.Parameter("Name"));
Assert.Empty(list);

二、"类型"系统的坑

  • 这里说的"类型"是指类型名或者说类型名占位符
  • 用于表示变量、属性和方法返回值类型

1. 预定义类型

  • 使用SyntaxFactory.PredefinedType定义预定义类型需要传个SyntaxToken
  • 但是这个SyntaxToken不满足条件就会抛异常
  • 指定16种情况,类似枚举
  • 却没有一对应的类型、枚举或属性
csharp 复制代码
public static PredefinedTypeSyntax PredefinedType(SyntaxToken keyword)
{
    switch (keyword.Kind())
    {
        case SyntaxKind.BoolKeyword:
        case SyntaxKind.ByteKeyword:
        case SyntaxKind.SByteKeyword:
        case SyntaxKind.IntKeyword:
        case SyntaxKind.UIntKeyword:
        case SyntaxKind.ShortKeyword:
        case SyntaxKind.UShortKeyword:
        case SyntaxKind.LongKeyword:
        case SyntaxKind.ULongKeyword:
        case SyntaxKind.FloatKeyword:
        case SyntaxKind.DoubleKeyword:
        case SyntaxKind.DecimalKeyword:
        case SyntaxKind.StringKeyword:
        case SyntaxKind.CharKeyword:
        case SyntaxKind.ObjectKeyword:
        case SyntaxKind.VoidKeyword: break;
        default: throw new ArgumentException(nameof(keyword));
    }
    return (PredefinedTypeSyntax)Syntax.InternalSyntax.SyntaxFactory.PredefinedType((Syntax.InternalSyntax.SyntaxToken)keyword.Node!).CreateRed();
}

1.1 EasySyntax中填了这个坑

  • SyntaxGenerator的其中16个静态属性表示这16种预定义类型
csharp 复制代码
 /// <summary>
 /// bool
 /// </summary>
 public static PredefinedTypeSyntax BoolType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.BoolKeyword));
 /// <summary>
 /// byte
 /// </summary>
 public static PredefinedTypeSyntax ByteType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ByteKeyword));
 /// <summary>
 /// sbyte
 /// </summary>
 public static PredefinedTypeSyntax SByteType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.SByteKeyword));
 /// <summary>
 /// int
 /// </summary>
 public static PredefinedTypeSyntax IntType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword));
 /// <summary>
 /// uint
 /// </summary>
 public static PredefinedTypeSyntax UIntType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.UIntKeyword));
 /// <summary>
 /// short
 /// </summary>
 public static PredefinedTypeSyntax ShortType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ShortKeyword));
 /// <summary>
 /// ushort
 /// </summary>
 public static PredefinedTypeSyntax UShortType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.UShortKeyword));
 /// <summary>
 /// long
 /// </summary>
 public static PredefinedTypeSyntax LongType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.LongKeyword));
 /// <summary>
 /// ulong
 /// </summary>
 public static PredefinedTypeSyntax ULongType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ULongKeyword));
 /// <summary>
 /// float
 /// </summary>
 public static PredefinedTypeSyntax FloatType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.FloatKeyword));
 /// <summary>
 /// double
 /// </summary>
 public static PredefinedTypeSyntax DoubleType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.DoubleKeyword));
 /// <summary>
 /// decimal
 /// </summary>
 public static PredefinedTypeSyntax DecimalType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.DecimalKeyword));
 /// <summary>
 /// string
 /// </summary>
 public static PredefinedTypeSyntax StringType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword));
 /// <summary>
 /// char
 /// </summary>
 public static PredefinedTypeSyntax CharType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.CharKeyword));
 /// <summary>
 /// object
 /// </summary>
 public static PredefinedTypeSyntax ObjectType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword));
 /// <summary>
 /// void
 /// </summary>
 public static PredefinedTypeSyntax VoidType => SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword));

2. 普通类型

  • 一个简单的类型名,非预定义的16种情况
  • IdentifierNameSyntax是TypeSyntax子类,可以表示类型名
  • IdentifierNameSyntax是标识名,也可以标识变量名、字段属性名、方法名或命名空间
  • IdentifierNameSyntax是TypeSyntax子类这个事实笔者也是花了一些时间才消化的
  • 这是SyntaxTree的设计哲学,设计如此
  • 想通了也好理解,类名是个标识符、变量名和类型成员名何尝不是标识符,没必要区分开
  • SyntaxFactory.IdentifierName("User")可以表示User类
  • SyntaxFactory.IdentifierName("user")可以表示User类的实例
csharp 复制代码
var type = SyntaxFactory.IdentifierName("User");
var user = SyntaxFactory.IdentifierName("user");
var variable = type.Variable(user.Identifier);

3. 限定名类型

  • 就是含命名空间的类名
  • QualifiedNameSyntax也是TypeSyntax子类,可以表示限定名类型
  • 同样QualifiedNameSyntax也可以表示成员名
  • Models.User可以表示特定的类
  • user1.Name自然也可以表示特定的属性
csharp 复制代码
var type = SyntaxFactory.IdentifierName("User");
var user = SyntaxFactory.IdentifierName("user1");
var property = SyntaxFactory.IdentifierName("Name");
var @namespace = SyntaxFactory.IdentifierName("Models");
// Models.User
var qualifiedType = type.Qualified(@namespace);
// user.Name
var qualifiedProperty = property.Qualified(user1);

4. 泛型

  • 就是含类型参数的类名
  • GenericNameSyntax也是TypeSyntax子类,可以表示泛型类型
  • GenericNameSyntax也可以表示泛型方法
csharp 复制代码
// List<int>
var listType = SyntaxGenerator.Generic("List", SyntaxGenerator.IntType);
// GetFieldValue<int>
var method = SyntaxGenerator.Generic("GetFieldValue", SyntaxGenerator.IntType);

三、ParseTypeName的坑

1. 预定义类型

1.1 预定义类型应该使用SyntaxFactory.PredefinedType

  • ParseTypeName("int")的实际类型PredefinedTypeSyntax
  • 使用SyntaxGenerator.IntType也是不错的
csharp 复制代码
var type = SyntaxFactory.ParseTypeName("int");
if (type is PredefinedTypeSyntax predefinedType)
{
    var predefinedType0 = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword));
    Assert.Equal(predefinedType0.Keyword.Kind(), predefinedType.Keyword.Kind());
}

1.2 预定义类型调用ParseTypeName性能不好

  • ParseTypeName代码为SyntaxFactory.ParseTypeName("int")
  • PredefinedType代码为SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword))
  • ParseTypeName耗时为30倍,内存为12倍
  • 如果嫌SyntaxFactory.PredefinedType表达式长,可以试试SyntaxGenerator.IntType
Method Mean Error StdDev Median Ratio RatioSD Gen0 Allocated Alloc Ratio
ParseTypeName 217.174 ns 0.6011 ns 0.6681 ns 217.703 ns 30.80 0.72 0.0575 992 B 12.40
PredefinedType 7.056 ns 0.1512 ns 0.1681 ns 6.929 ns 1.00 0.03 0.0046 80 B 1.00

2. 普通类型

2.1 普通类型应该使用SyntaxFactory.IdentifierName

  • ParseTypeName("User")的实际类型IdentifierNameSyntax
csharp 复制代码
var type = SyntaxFactory.ParseTypeName("User");
if (type is IdentifierNameSyntax identifierName)
{
    var identifierName0 = SyntaxFactory.IdentifierName("User");
    Assert.Equal(identifierName0.ToFullString(), identifierName.ToFullString());
}

2.2 普通类型调用ParseTypeName性能也不好

  • IdentifierName代码为SyntaxFactory.IdentifierName("User")
  • ParseTypeName代码为SyntaxFactory.ParseTypeName("User")
  • ParseTypeName耗时为46倍,内存为7倍
Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
IdentifierName 10.84 ns 0.238 ns 0.275 ns 1.00 0.03 0.0083 - - 144 B 1.00
ParseTypeName 508.55 ns 0.100 ns 0.111 ns 46.95 1.15 0.0694 0.0163 0.0082 1056 B 7.33

3. 限定名类型

  • 就是含命名空间的类名

3.1 限定名类型应该使用SyntaxFactory.IdentifierName

  • ParseTypeName("Models.User")的实际类型QualifiedNameSyntax与SyntaxFactory.IdentifierName("User").Qualified("Models")一致
  • 其实用SyntaxFactory.IdentifierName("Models.User")也是可以的
csharp 复制代码
var type = SyntaxFactory.ParseTypeName("Models.User");
if (type is QualifiedNameSyntax qualifiedName)
{
    var qualifiedName0 = SyntaxFactory.IdentifierName("User").Qualified("Models");
    Assert.Equal(qualifiedName0.ToFullString(), qualifiedName.ToFullString());
    var identifierName = SyntaxFactory.IdentifierName("Models.User");
    Assert.Equal(qualifiedName0.ToFullString(), identifierName.ToFullString());
}

3.2 限定名类型调用ParseTypeName性能也不好

  • QualifiedName代码为SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("Models"), SyntaxFactory.IdentifierName("User"))
  • ParseTypeName代码为SyntaxFactory.ParseTypeName("Models.User")
  • IdentifierName代码为SyntaxFactory.IdentifierName("Models.User")
  • ParseTypeName耗时22倍,内存2倍多
  • IdentifierName性能最好
Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
QualifiedName 30.85 ns 0.101 ns 0.116 ns 1.00 0.01 0.0231 - - 400 B 1.00
ParseTypeName 707.37 ns 0.029 ns 0.033 ns 22.93 0.08 0.0713 0.0194 0.0064 1120 B 2.80
IdentifierName 10.69 ns 0.090 ns 0.104 ns 0.35 0.00 0.0083 - - 144 B 0.36

4. 泛型

  • 就是含类型参数的类名

4.1 泛型应该使用SyntaxFactory.GenericName

  • ParseTypeName("List ")的实际类型GenericNameSyntax与SyntaxGenerator.Generic("List", SyntaxGenerator.IntType)一致
  • SyntaxGenerator.Generic("List", SyntaxGenerator.IntType)是SyntaxFactory.GenericName("List").AddTypeArgumentListArguments(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword)))的简化
csharp 复制代码
var type = SyntaxFactory.ParseTypeName("List<int>");
if (type is not GenericNameSyntax genericName)
{
    var genericName0 = SyntaxGenerator.Generic("List", SyntaxGenerator.IntType);
    Assert.Equal(genericName0.ToFullString(), genericName.ToFullString());
}

4.2 泛型调用ParseTypeName性能也不好

  • GenericName代码为SyntaxFactory.GenericName("List").AddTypeArgumentListArguments(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword)))
  • ParseTypeName代码为SyntaxFactory.ParseTypeName("List ")
  • ParseTypeName耗时2倍
Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
GenericName 128.9 ns 0.92 ns 1.02 ns 1.00 0.01 0.0607 - 1.02 KB 1.00
ParseTypeName 284.3 ns 0.69 ns 0.74 ns 2.21 0.02 0.0658 0.0001 1.11 KB 1.08

5. 泛型限定名

  • 就是含命名空间的泛型

5.1 泛型限定名应该使用SyntaxFactory.IdentifierName

  • SyntaxFactory.ParseTypeName("System.Collections.Generic.List ")的实际类型QualifiedNameSyntax与SyntaxGenerator.Generic("List", SyntaxGenerator.IntType).Qualified("System.Collections.Generic")一致
  • 其实直接用GenericName也是可以的
csharp 复制代码
var type = SyntaxFactory.ParseTypeName("System.Collections.Generic.List<int>");
if (type is QualifiedNameSyntax qualifiedName)
{
    var qualifiedName0 = SyntaxGenerator.Generic("List", SyntaxGenerator.IntType).Qualified("System.Collections.Generic");
    Assert.Equal(qualifiedName0.ToFullString(), qualifiedName.ToFullString());
    var genericName = SyntaxGenerator.Generic("System.Collections.Generic.List", SyntaxGenerator.IntType);
    Assert.Equal(qualifiedName0.ToFullString(), genericName.ToFullString());
}

5.2 泛型限定名调用ParseTypeName性能也不好

  • QualifiedName代码为SyntaxGenerator.Generic("List", SyntaxGenerator.IntType).Qualified("System.Collections.Generic")
  • ParseTypeName代码为SyntaxFactory.ParseTypeName("System.Collections.Generic.List ")
  • GenericName代码为SyntaxGenerator.Generic("System.Collections.Generic.List", SyntaxGenerator.IntType)
  • ParseTypeName耗时5倍多
  • 直接SyntaxGenerator.Generic性能最好
Method Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
QualifiedName 72.81 ns 2.099 ns 2.333 ns 71.06 ns 1.00 0.04 0.0384 - 664 B 1.00
ParseTypeName 425.04 ns 1.605 ns 1.717 ns 426.37 ns 5.84 0.18 0.0663 0.0001 1144 B 1.72
GenericName 42.27 ns 0.637 ns 0.708 ns 42.81 ns 0.58 0.02 0.0236 - 408 B 0.61

四、空与null不同

1. 类型参数空与null不同

1.1 ParameterList()定义空参数

csharp 复制代码
var recordDeclaration = SyntaxGenerator.RecordDeclaration("Person")
    .WithParameterList(SyntaxFactory.ParameterList())
    .WithSemicolonToken();
// record Person();

1.2 null为无参数

  • 默认就是null
csharp 复制代码
var recordDeclaration = SyntaxGenerator.RecordDeclaration("Person")
    .WithParameterList(null)
    .WithSemicolonToken();
// record Person;

五、分号问题

  • 一般SyntaxTree会自动处理分号
  • 对于简化语法是需要手动加分号的
  • 使用WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))加分号
  • 可以简化为WithSemicolonToken()

1. 无成员的类型需要加分号

csharp 复制代码
var recordDeclaration = SyntaxGenerator.RecordStructDeclaration("Person")
    .Public()
    .AddParameterListParameters(
        SyntaxGenerator.StringType.Parameter("Name")
    )
    .WithSemicolonToken();
// public record struct Person(string Name);

2. 空方法体的方法

csharp 复制代码
var method = SyntaxGenerator.IntType.Method("CreateId")
    .Public()
    .Partial()
    .WithSemicolonToken();
// public partial int CreateId();

3. 空的属性操作器

csharp 复制代码
var property = SyntaxFactory.PropertyDeclaration(
    SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword)),
    SyntaxFactory.Identifier("Id"))
    .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
    .AddAccessorListAccessors(
        SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
            .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
        SyntaxFactory.AccessorDeclaration(SyntaxKind.InitAccessorDeclaration)
            .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
    );
// public int Id { get; set; }

3.1 使用EasySyntax定义空的属性操作器自动加分号

csharp 复制代码
var property = SyntaxGenerator.IntType.Property("Id",
    SyntaxKind.GetAccessorDeclaration,
    SyntaxKind.InitAccessorDeclaration)
    .Public();
// public int Id { get; set; }

六、SyntaxFactory和SyntaxKind的坑

  • SyntaxKind枚举有几百项
  • SyntaxFactory也有几百个静态方法,很多静态方法需要配合SyntaxKind使用
  • 有时从SyntaxFactory几百中选正确方法再从SyntaxKind几百中选正确的枚举项是个不小的挑战
  • EasySyntax解决了部分问题,而且还是常用的那些

七、总结

  • SyntaxTree有些坑还是需要注意的
  • EasySyntax已经填了一些坑,建议尝试一下
  • EasySyntax更多信息可以参考上一篇文章

源码托管地址: https://github.com/donetsoftwork/Hand.Generators ,欢迎大家直接查看源码。

gitee同步更新:https://gitee.com/donetsoftwork/hand.-generators

如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

相关推荐
xiangji1 天前
.net源码生成器使用SyntaxTree生成代码及简化语法
sourcegenerator·syntaxtree·easysyntax
mysolisoft5 个月前
Avalonia+ReactiveUI+Sourcegenerators实现异步命令
avalonia·reactiveui·sourcegenerator
mysolisoft5 个月前
Avalonia+ReactiveUI实现记录自动更新
c#·avalonia·reactiveui·sourcegenerator