Skip to content

符号系统最佳实践

📚 文档导航

本文档介绍符号系统的最佳实践、性能优化技巧和实战场景。

📖 文档系列

文档内容难度
符号系统基础基础概念、符号层次、快速入门🟢 入门
符号类型详解INamedTypeSymbol、IMethodSymbol、IPropertySymbol 等🟡 中级
符号操作获取、遍历、查询、比较符号🟡 中级
高级主题继承、接口、特性、显示格式、文档注释🔴 高级
最佳实践性能优化、实战场景、设计模式🟡 中级

性能优化

符号缓存策略

csharp
using System.Collections.Concurrent;
using Microsoft.CodeAnalysis;

public class SymbolCache
{
    private readonly ConcurrentDictionary<string, INamedTypeSymbol> _typeCache = new();
    private readonly Compilation _compilation;
    
    public SymbolCache(Compilation compilation)
    {
        _compilation = compilation;
    }
    
    public INamedTypeSymbol GetOrAddType(string metadataName)
    {
        return _typeCache.GetOrAdd(metadataName, name =>
        {
            var type = _compilation.GetTypeByMetadataName(name);
            if (type == null)
            {
                throw new InvalidOperationException($"找不到类型: {name}");
            }
            return type;
        });
    }
    
    // 预加载常用类型
    public void PreloadCommonTypes()
    {
        var commonTypes = new[]
        {
            "System.String",
            "System.Int32",
            "System.Boolean",
            "System.Object",
            "System.Collections.Generic.List`1",
            "System.Collections.Generic.Dictionary`2",
            "System.Linq.Enumerable",
            "System.Threading.Tasks.Task",
            "System.Threading.Tasks.Task`1"
        };
        
        foreach (var typeName in commonTypes)
        {
            GetOrAddType(typeName);
        }
    }
}

批量符号操作

csharp
public class BatchSymbolOperations
{
    public Dictionary<INamedTypeSymbol, List<IMethodSymbol>> GetPublicMethodsByType(
        IEnumerable<INamedTypeSymbol> types)
    {
        var result = new Dictionary<INamedTypeSymbol, List<IMethodSymbol>>(
            SymbolEqualityComparer.Default);
        
        // 批量处理,避免重复遍历
        foreach (var type in types)
        {
            var publicMethods = type.GetMembers()
                .OfType<IMethodSymbol>()
                .Where(m => m.DeclaredAccessibility == Accessibility.Public &&
                           m.MethodKind == MethodKind.Ordinary)
                .ToList();
            
            result[type] = publicMethods;
        }
        
        return result;
    }
    
    public HashSet<INamedTypeSymbol> GetAllReferencedTypes(INamedTypeSymbol type)
    {
        var referencedTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
        var visited = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
        
        CollectReferencedTypes(type, referencedTypes, visited);
        
        return referencedTypes;
    }
    
    private void CollectReferencedTypes(
        INamedTypeSymbol type,
        HashSet<INamedTypeSymbol> referencedTypes,
        HashSet<INamedTypeSymbol> visited)
    {
        if (!visited.Add(type))
            return;
        
        // 基类
        if (type.BaseType != null)
        {
            referencedTypes.Add(type.BaseType);
            CollectReferencedTypes(type.BaseType, referencedTypes, visited);
        }
        
        // 接口
        foreach (var iface in type.Interfaces)
        {
            referencedTypes.Add(iface);
            CollectReferencedTypes(iface, referencedTypes, visited);
        }
        
        // 成员类型
        foreach (var member in type.GetMembers())
        {
            ITypeSymbol memberType = null;
            
            if (member is IMethodSymbol method)
            {
                memberType = method.ReturnType as INamedTypeSymbol;
                foreach (var param in method.Parameters)
                {
                    if (param.Type is INamedTypeSymbol paramType)
                    {
                        referencedTypes.Add(paramType);
                    }
                }
            }
            else if (member is IPropertySymbol property)
            {
                memberType = property.Type as INamedTypeSymbol;
            }
            else if (member is IFieldSymbol field)
            {
                memberType = field.Type as INamedTypeSymbol;
            }
            
            if (memberType is INamedTypeSymbol namedType)
            {
                referencedTypes.Add(namedType);
            }
        }
    }
}

性能优化流程图


最佳实践 vs 反模式

场景✅ 最佳实践❌ 反模式
符号比较使用 SymbolEqualityComparer.Default使用 ==Equals()
类型查找缓存 GetTypeByMetadataName 结果每次都重新查找
成员遍历使用 GetMembers() 一次性获取多次调用不同的 Get 方法
特性检查使用 GetAttributes()解析语法节点
集合存储使用 SymbolEqualityComparer使用默认比较器
符号查询批量处理后过滤逐个查询

真实应用场景

场景 1: 生成 Builder 模式代码

csharp
public class BuilderGenerator
{
    public string GenerateBuilder(INamedTypeSymbol classSymbol)
    {
        var className = classSymbol.Name;
        var builderName = $"{className}Builder";
        
        var sb = new StringBuilder();
        sb.AppendLine($"public class {builderName}");
        sb.AppendLine("{");
        
        // 为每个公共属性生成字段和方法
        var properties = classSymbol.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => p.DeclaredAccessibility == Accessibility.Public &&
                       p.SetMethod != null);
        
        foreach (var prop in properties)
        {
            var propType = prop.Type.ToDisplayString();
            var propName = prop.Name;
            var fieldName = $"_{char.ToLower(propName[0])}{propName.Substring(1)}";
            
            sb.AppendLine($"    private {propType} {fieldName};");
        }
        
        sb.AppendLine();
        
        foreach (var prop in properties)
        {
            var propType = prop.Type.ToDisplayString();
            var propName = prop.Name;
            var fieldName = $"_{char.ToLower(propName[0])}{propName.Substring(1)}";
            
            sb.AppendLine($"    public {builderName} With{propName}({propType} value)");
            sb.AppendLine($"    {{");
            sb.AppendLine($"        {fieldName} = value;");
            sb.AppendLine($"        return this;");
            sb.AppendLine($"    }}");
            sb.AppendLine();
        }
        
        sb.AppendLine($"    public {className} Build()");
        sb.AppendLine($"    {{");
        sb.AppendLine($"        return new {className}");
        sb.AppendLine($"        {{");
        
        foreach (var prop in properties)
        {
            var propName = prop.Name;
            var fieldName = $"_{char.ToLower(propName[0])}{propName.Substring(1)}";
            sb.AppendLine($"            {propName} = {fieldName},");
        }
        
        sb.AppendLine($"        }};");
        sb.AppendLine($"    }}");
        sb.AppendLine("}");
        
        return sb.ToString();
    }
}

场景 2: 验证 API 兼容性

csharp
public class APICompatibilityChecker
{
    public List<string> CheckCompatibility(
        INamedTypeSymbol oldVersion,
        INamedTypeSymbol newVersion)
    {
        var issues = new List<string>();
        
        // 检查公共成员是否被移除
        var oldPublicMembers = oldVersion.GetMembers()
            .Where(m => m.DeclaredAccessibility == Accessibility.Public)
            .ToList();
        
        foreach (var oldMember in oldPublicMembers)
        {
            var newMember = newVersion.GetMembers(oldMember.Name)
                .FirstOrDefault(m => SymbolEqualityComparer.Default.Equals(m, oldMember));
            
            if (newMember == null)
            {
                issues.Add($"成员 {oldMember.Name} 已被移除");
            }
            else if (oldMember is IMethodSymbol oldMethod && newMember is IMethodSymbol newMethod)
            {
                // 检查方法签名是否改变
                if (!SymbolEqualityComparer.Default.Equals(oldMethod.ReturnType, newMethod.ReturnType))
                {
                    issues.Add($"方法 {oldMethod.Name} 的返回类型已改变");
                }
                
                if (oldMethod.Parameters.Length != newMethod.Parameters.Length)
                {
                    issues.Add($"方法 {oldMethod.Name} 的参数数量已改变");
                }
            }
        }
        
        return issues;
    }
}

场景 3: 代码度量分析

csharp
public class CodeMetrics
{
    public class TypeMetrics
    {
        public string TypeName { get; set; }
        public int PublicMethods { get; set; }
        public int PrivateMethods { get; set; }
        public int Properties { get; set; }
        public int Fields { get; set; }
        public int LinesOfCode { get; set; }
        public int CyclomaticComplexity { get; set; }
    }
    
    public TypeMetrics AnalyzeType(INamedTypeSymbol type)
    {
        var metrics = new TypeMetrics
        {
            TypeName = type.ToDisplayString()
        };
        
        foreach (var member in type.GetMembers())
        {
            switch (member)
            {
                case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary:
                    if (method.DeclaredAccessibility == Accessibility.Public)
                        metrics.PublicMethods++;
                    else
                        metrics.PrivateMethods++;
                    break;
                    
                case IPropertySymbol:
                    metrics.Properties++;
                    break;
                    
                case IFieldSymbol:
                    metrics.Fields++;
                    break;
            }
        }
        
        return metrics;
    }
}

场景 4: 依赖注入容器生成

csharp
public class DIContainerGenerator
{
    public string GenerateRegistrations(IEnumerable<INamedTypeSymbol> types)
    {
        var sb = new StringBuilder();
        sb.AppendLine("public static class ServiceRegistrations");
        sb.AppendLine("{");
        sb.AppendLine("    public static IServiceCollection AddGeneratedServices(");
        sb.AppendLine("        this IServiceCollection services)");
        sb.AppendLine("    {");
        
        foreach (var type in types)
        {
            // 查找构造函数
            var constructor = type.Constructors
                .Where(c => c.DeclaredAccessibility == Accessibility.Public)
                .OrderByDescending(c => c.Parameters.Length)
                .FirstOrDefault();
            
            if (constructor == null) continue;
            
            // 查找实现的接口
            var serviceInterface = type.Interfaces.FirstOrDefault();
            
            if (serviceInterface != null)
            {
                sb.AppendLine($"        services.AddScoped<{serviceInterface.ToDisplayString()}, {type.ToDisplayString()}>();");
            }
            else
            {
                sb.AppendLine($"        services.AddScoped<{type.ToDisplayString()}>();");
            }
        }
        
        sb.AppendLine("        return services;");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        
        return sb.ToString();
    }
}

场景 5: API 文档生成

csharp
public class APIDocumentationGenerator
{
    public string GenerateDocumentation(INamedTypeSymbol type)
    {
        var sb = new StringBuilder();
        
        // 类型标题
        sb.AppendLine($"# {type.Name}");
        sb.AppendLine();
        
        // 命名空间
        sb.AppendLine($"**命名空间**: `{type.ContainingNamespace.ToDisplayString()}`");
        sb.AppendLine();
        
        // 程序集
        sb.AppendLine($"**程序集**: {type.ContainingAssembly.Name}");
        sb.AppendLine();
        
        // 类型说明
        var xmlDoc = type.GetDocumentationCommentXml();
        if (!string.IsNullOrEmpty(xmlDoc))
        {
            var summary = ExtractSummary(xmlDoc);
            if (!string.IsNullOrEmpty(summary))
            {
                sb.AppendLine("## 说明");
                sb.AppendLine(summary);
                sb.AppendLine();
            }
        }
        
        // 继承层次
        if (type.BaseType != null && type.BaseType.SpecialType != SpecialType.System_Object)
        {
            sb.AppendLine("## 继承");
            sb.AppendLine($"```");
            PrintInheritanceChain(type, sb);
            sb.AppendLine($"```");
            sb.AppendLine();
        }
        
        // 实现的接口
        if (type.Interfaces.Length > 0)
        {
            sb.AppendLine("## 实现的接口");
            foreach (var iface in type.Interfaces)
            {
                sb.AppendLine($"- `{iface.ToDisplayString()}`");
            }
            sb.AppendLine();
        }
        
        // 公共方法
        var publicMethods = type.GetMembers()
            .OfType<IMethodSymbol>()
            .Where(m => m.DeclaredAccessibility == Accessibility.Public &&
                       m.MethodKind == MethodKind.Ordinary);
        
        if (publicMethods.Any())
        {
            sb.AppendLine("## 公共方法");
            foreach (var method in publicMethods)
            {
                sb.AppendLine($"### {method.Name}");
                sb.AppendLine($"```csharp");
                sb.AppendLine(method.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
                sb.AppendLine($"```");
                
                var methodDoc = method.GetDocumentationCommentXml();
                if (!string.IsNullOrEmpty(methodDoc))
                {
                    var methodSummary = ExtractSummary(methodDoc);
                    if (!string.IsNullOrEmpty(methodSummary))
                    {
                        sb.AppendLine(methodSummary);
                    }
                }
                sb.AppendLine();
            }
        }
        
        return sb.ToString();
    }
    
    private string ExtractSummary(string xml)
    {
        if (string.IsNullOrEmpty(xml)) return string.Empty;
        try
        {
            var doc = XDocument.Parse(xml);
            return doc.Descendants("summary").FirstOrDefault()?.Value.Trim() ?? string.Empty;
        }
        catch
        {
            return string.Empty;
        }
    }
    
    private void PrintInheritanceChain(INamedTypeSymbol type, StringBuilder sb, int depth = 0)
    {
        var indent = new string(' ', depth * 2);
        sb.AppendLine($"{indent}{type.ToDisplayString()}");
        if (type.BaseType != null && type.BaseType.SpecialType != SpecialType.System_Object)
        {
            PrintInheritanceChain(type.BaseType, sb, depth + 1);
        }
    }
}

场景 6: 代码质量分析

csharp
public class CodeQualityAnalyzer
{
    public class QualityReport
    {
        public int TotalTypes { get; set; }
        public int PublicTypes { get; set; }
        public int AbstractTypes { get; set; }
        public int SealedTypes { get; set; }
        public int InterfaceTypes { get; set; }
        public double AverageMethodsPerType { get; set; }
        public double AveragePropertiesPerType { get; set; }
        public List<string> LargeClasses { get; set; } = new();
        public List<string> GodClasses { get; set; } = new();
    }
    
    public QualityReport AnalyzeAssembly(IAssemblySymbol assembly)
    {
        var report = new QualityReport();
        var allTypes = GetAllTypes(assembly.GlobalNamespace).ToList();
        
        report.TotalTypes = allTypes.Count;
        report.PublicTypes = allTypes.Count(t => t.DeclaredAccessibility == Accessibility.Public);
        report.AbstractTypes = allTypes.Count(t => t.IsAbstract);
        report.SealedTypes = allTypes.Count(t => t.IsSealed);
        report.InterfaceTypes = allTypes.Count(t => t.TypeKind == TypeKind.Interface);
        
        var methodCounts = allTypes.Select(t => t.GetMembers().OfType<IMethodSymbol>().Count()).ToList();
        var propertyCounts = allTypes.Select(t => t.GetMembers().OfType<IPropertySymbol>().Count()).ToList();
        
        report.AverageMethodsPerType = methodCounts.Any() ? methodCounts.Average() : 0;
        report.AveragePropertiesPerType = propertyCounts.Any() ? propertyCounts.Average() : 0;
        
        // 查找大类(方法数 > 20)
        report.LargeClasses = allTypes
            .Where(t => t.GetMembers().OfType<IMethodSymbol>().Count() > 20)
            .Select(t => t.ToDisplayString())
            .ToList();
        
        // 查找上帝类(方法数 > 50 或属性数 > 30)
        report.GodClasses = allTypes
            .Where(t => t.GetMembers().OfType<IMethodSymbol>().Count() > 50 ||
                       t.GetMembers().OfType<IPropertySymbol>().Count() > 30)
            .Select(t => t.ToDisplayString())
            .ToList();
        
        return report;
    }
    
    private IEnumerable<INamedTypeSymbol> GetAllTypes(INamespaceSymbol ns)
    {
        foreach (var type in ns.GetTypeMembers())
        {
            yield return type;
            
            // 递归获取嵌套类型
            foreach (var nestedType in GetNestedTypes(type))
            {
                yield return nestedType;
            }
        }
        
        foreach (var childNs in ns.GetNamespaceMembers())
        {
            foreach (var type in GetAllTypes(childNs))
            {
                yield return type;
            }
        }
    }
    
    private IEnumerable<INamedTypeSymbol> GetNestedTypes(INamedTypeSymbol type)
    {
        foreach (var member in type.GetTypeMembers())
        {
            yield return member;
            foreach (var nested in GetNestedTypes(member))
            {
                yield return nested;
            }
        }
    }
}

符号系统的设计模式

访问者模式

使用访问者模式遍历符号树:

csharp
public abstract class SymbolVisitor
{
    public virtual void Visit(ISymbol symbol)
    {
        switch (symbol)
        {
            case INamespaceSymbol ns:
                VisitNamespace(ns);
                break;
            case INamedTypeSymbol type:
                VisitNamedType(type);
                break;
            case IMethodSymbol method:
                VisitMethod(method);
                break;
            case IPropertySymbol property:
                VisitProperty(property);
                break;
            case IFieldSymbol field:
                VisitField(field);
                break;
        }
    }
    
    protected virtual void VisitNamespace(INamespaceSymbol symbol)
    {
        foreach (var member in symbol.GetMembers())
        {
            Visit(member);
        }
    }
    
    protected virtual void VisitNamedType(INamedTypeSymbol symbol)
    {
        foreach (var member in symbol.GetMembers())
        {
            Visit(member);
        }
    }
    
    protected virtual void VisitMethod(IMethodSymbol symbol) { }
    protected virtual void VisitProperty(IPropertySymbol symbol) { }
    protected virtual void VisitField(IFieldSymbol symbol) { }
}

// 使用示例
public class PublicAPICollector : SymbolVisitor
{
    public List<ISymbol> PublicAPIs { get; } = new();
    
    protected override void VisitNamedType(INamedTypeSymbol symbol)
    {
        if (symbol.DeclaredAccessibility == Accessibility.Public)
        {
            PublicAPIs.Add(symbol);
        }
        base.VisitNamedType(symbol);
    }
    
    protected override void VisitMethod(IMethodSymbol symbol)
    {
        if (symbol.DeclaredAccessibility == Accessibility.Public)
        {
            PublicAPIs.Add(symbol);
        }
    }
}

符号的调试技巧

符号信息输出

csharp
public class SymbolDebugger
{
    public void DumpSymbolInfo(ISymbol symbol, int indentLevel = 0)
    {
        var indent = new string(' ', indentLevel * 2);
        
        Console.WriteLine($"{indent}符号类型: {symbol.Kind}");
        Console.WriteLine($"{indent}名称: {symbol.Name}");
        Console.WriteLine($"{indent}完整名称: {symbol.ToDisplayString()}");
        Console.WriteLine($"{indent}元数据名称: {symbol.MetadataName}");
        Console.WriteLine($"{indent}访问修饰符: {symbol.DeclaredAccessibility}");
        Console.WriteLine($"{indent}是静态: {symbol.IsStatic}");
        Console.WriteLine($"{indent}是虚拟: {symbol.IsVirtual}");
        Console.WriteLine($"{indent}是抽象: {symbol.IsAbstract}");
        Console.WriteLine($"{indent}是密封: {symbol.IsSealed}");
        Console.WriteLine($"{indent}是外部: {symbol.IsExtern}");
        
        // 包含的符号
        if (symbol.ContainingSymbol != null)
        {
            Console.WriteLine($"{indent}包含在: {symbol.ContainingSymbol.ToDisplayString()}");
        }
        
        // 特性
        var attributes = symbol.GetAttributes();
        if (attributes.Length > 0)
        {
            Console.WriteLine($"{indent}特性:");
            foreach (var attr in attributes)
            {
                Console.WriteLine($"{indent}  - {attr.AttributeClass?.ToDisplayString()}");
            }
        }
        
        // 类型特定信息
        if (symbol is INamedTypeSymbol namedType)
        {
            DumpNamedTypeInfo(namedType, indentLevel + 1);
        }
        else if (symbol is IMethodSymbol method)
        {
            DumpMethodInfo(method, indentLevel + 1);
        }
    }
    
    private void DumpNamedTypeInfo(INamedTypeSymbol type, int indentLevel)
    {
        var indent = new string(' ', indentLevel * 2);
        
        Console.WriteLine($"{indent}类型种类: {type.TypeKind}");
        Console.WriteLine($"{indent}是泛型: {type.IsGenericType}");
        Console.WriteLine($"{indent}成员数量: {type.GetMembers().Length}");
        
        if (type.BaseType != null)
        {
            Console.WriteLine($"{indent}基类: {type.BaseType.ToDisplayString()}");
        }
        
        if (type.Interfaces.Length > 0)
        {
            Console.WriteLine($"{indent}接口:");
            foreach (var iface in type.Interfaces)
            {
                Console.WriteLine($"{indent}  - {iface.ToDisplayString()}");
            }
        }
    }
    
    private void DumpMethodInfo(IMethodSymbol method, int indentLevel)
    {
        var indent = new string(' ', indentLevel * 2);
        
        Console.WriteLine($"{indent}方法种类: {method.MethodKind}");
        Console.WriteLine($"{indent}返回类型: {method.ReturnType.ToDisplayString()}");
        Console.WriteLine($"{indent}是异步: {method.IsAsync}");
        Console.WriteLine($"{indent}是扩展方法: {method.IsExtensionMethod}");
        Console.WriteLine($"{indent}参数数量: {method.Parameters.Length}");
        
        if (method.Parameters.Length > 0)
        {
            Console.WriteLine($"{indent}参数:");
            foreach (var param in method.Parameters)
            {
                Console.WriteLine($"{indent}  - {param.Type.ToDisplayString()} {param.Name}");
            }
        }
    }
}

常见问题解答

Q1: 如何比较两个符号是否相同?

A: 使用 SymbolEqualityComparer.Default.Equals(symbol1, symbol2)。永远不要使用 ==Equals() 方法,因为它们比较的是引用而不是语义相等性。

Q2: 如何获取类的所有成员(包括继承的)?

A: 使用 GetMembers() 获取所有成员,包括继承的成员。如果只想获取当前类型声明的成员,可以检查 member.ContainingType 是否等于当前类型。

Q3: 如何检查类型是否实现了特定接口?

A: 使用 typeSymbol.AllInterfaces.Any(i => i.Name == "IMyInterface")。注意使用 AllInterfaces 而不是 Interfaces,因为前者包含所有继承的接口。

Q4: 如何获取泛型类型的类型参数?

A: 对于泛型类型定义,使用 typeSymbol.TypeParameters 获取类型参数。对于构造的泛型类型,使用 typeSymbol.TypeArguments 获取具体的类型实参。例如,List<T> 的 TypeParameters 是 T,而 List<int> 的 TypeArguments 是 int

Q5: 符号的生命周期是怎样的?

A: 符号的生命周期与 Compilation 对象绑定。当 Compilation 被释放时,所有相关的符号也会失效。因此,不要在不同的 Compilation 之间共享符号引用。如果需要跨 Compilation 使用符号信息,应该保存符号的元数据名称或完整限定名称,然后在新的 Compilation 中重新查找。

Q6: 如何获取符号的定义位置?

A: 使用 symbol.Locations.FirstOrDefault() 获取符号的定义位置。可以通过 location.SourceTree?.FilePath 获取文件路径。

Q7: 如何检查方法是否重写了基类方法?

A: 使用 method.IsOverride 检查是否是重写方法,使用 method.OverriddenMethod 获取被重写的基类方法。

Q8: 如何查找所有带特定特性的类?

A: 遍历命名空间,检查每个类型的特性:

csharp
var classesWithAttribute = namespace.GetMembers()
    .OfType<INamedTypeSymbol>()
    .Where(t => t.GetAttributes()
        .Any(a => a.AttributeClass?.Name == "MyAttribute"));

Q9: 如何获取符号的 XML 文档注释?

A: 使用 symbol.GetDocumentationCommentXml() 获取 XML 格式的文档注释,然后解析 XML 获取具体内容。

Q10: 如何检查类型是否可以为 null?

A: 检查类型是引用类型还是可空值类型:

csharp
bool canBeNull = type.IsReferenceType || 
    (type is INamedTypeSymbol namedType &&
     namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T);

下一步学习

完成符号系统的学习后,建议继续学习以下内容:

  1. 语义模型 API - 深入了解如何使用语义模型获取符号信息
  2. 类型系统深入 - 学习复杂类型的处理,包括泛型、可空类型等
  3. 编译 API - 了解如何创建和管理编译单元
  4. 代码生成 API - 学习如何使用符号信息生成代码
  5. 增量生成器管道 - 了解如何在增量生成器中高效使用符号

🔗 相关资源

深入学习

API 参考

实战示例


最后更新: 2025-01-21

基于 MIT 许可发布