.NET Framework反射获取XML注释成员名时Type.GetGenericArguments结果异常
我们有一个基于.NET Framework的遗留WinForms应用,其中包含一个从配套XML注释文件读取程序集文档的模块。该代码多年运行正常,但近期发现一处缺陷。
复现问题的类代码如下:
public class Class1<T1, T2> { public struct MyStruct<TS1, TS2> { public TS1 StructField1; } public void Method1(T1 arg1) { } public void Method2(T1 arg1, T2 arg2) { } public void Method3(MyStruct<string, T1> arg1) { } public void Method4(MyStruct<T2, int> arg1) { } }
Method3和Method4在XML注释文件中的记录:
<member name="M:TestSimple.Class1`2.Method3(TestSimple.Class1{`0,`1}.MyStruct{System.String,`0})"> <summary> Method3 summary. </summary> <param name="arg1">Argument #1.</param> </member> <member name="M:TestSimple.Class1`2.Method4(TestSimple.Class1{`0,`1}.MyStruct{`1,System.Int32})"> <summary> Method4 summary. </summary> <param name="arg1">Argument #1.</param> </member>
要访问这些注释,需要为Method3和Method4分别生成如下字符串:
M:TestSimple.Class1`2.Method3(TestSimple.Class1{`0,`1}.MyStruct{System.String,`0})
和
M:TestSimple.Class1`2.Method4(TestSimple.Class1{`0,`1}.MyStruct{`1,System.Int32})
在构造该字符串的反射代码中,处理MyStruct的参数描述字符串时出现问题:.NET的Type.GetGenericArguments()方法返回的泛型参数列表不正确——MyStruct应返回2个参数,但实际返回4个。即使按照文档使用Type.IsGenericParameter过滤也无效,Method3的参数列表中T1重复,Method4中T2重复。
当前使用的GetGenericClosedConstructedTypeArgumentsString方法实现:
private static string GetGenericClosedConstructedTypeArgumentsString(Type type) { Type[] myArgumentTypes = type.GetGenericArguments(); StringBuilder myResult = new StringBuilder(); myResult.Append(ConstsXmlHelp.cGenericMethodParameterArgumentListStart); foreach (Type myArgumentType in myArgumentTypes) { if (myArgumentType.IsGenericParameter) { if (myArgumentType.DeclaringType != null && myArgumentType.DeclaringType != type) { #region Check whether the current argument type is declared in an enclosing type bool myIsArgumentTypeDeclaredInEnclosingType = false; Type myEnclosingType = type.DeclaringType; while (myEnclosingType != null) { if (myEnclosingType == myArgumentType.DeclaringType) { myIsArgumentTypeDeclaredInEnclosingType = true; break; } myEnclosingType = myEnclosingType.DeclaringType; } #endregion if (myIsArgumentTypeDeclaredInEnclosingType) continue; } } if (myResult.Length > ConstsXmlHelp.cGenericMethodParameterArgumentListStart.Length) myResult.Append(ConstsXmlHelp.cParameterDelimeter); // GetFullName() below returns the generic or string // argument equivalent like `0, `1, or System.Int32 myResult.Append(GetFullName(myArgumentType)); } myResult.Append(ConstsXmlHelp.cGenericMethodParameterArgumentListEnd); return myResult.ToString(); } internal static class ConstsXmlHelp { public const string cParameterDelimeter = ","; public const string cGenericMethodParameterArgumentListStart = "{"; public const string cGenericMethodParameterArgumentListEnd = "}"; }
- 如何可靠获取此类嵌套泛型结构体的参数列表?
- .NET中是否有标准方法可以根据Type实例生成XML注释文件中对应的成员名?
1. 可靠获取嵌套泛型结构体的参数列表
问题根源是Type.GetGenericArguments()会返回所有层级泛型类型的参数:对于嵌套在泛型类中的泛型结构体,它会先返回外层类的泛型参数,再返回结构体自身的泛型参数,因此出现4个参数的异常。
正确逻辑需要区分泛型类型定义和构造泛型类型:
- 泛型类型定义(如
MyStruct<TS1, TS2>本身):直接用Type.GetGenericArguments()即可获取自身的泛型参数。 - 构造泛型类型(如
MyStruct<string, T1>):先通过Type.GetGenericTypeDefinition()获取其原始泛型定义,再从所有参数中截取与该定义匹配的数量的参数。
修改后的参数获取方法:
private static Type[] GetOwnGenericArguments(Type constructedType) { if (!constructedType.IsGenericType) return Array.Empty<Type>(); var genericTypeDef = constructedType.GetGenericTypeDefinition(); var allArgs = constructedType.GetGenericArguments(); // 只保留当前泛型类型定义对应的参数数量 return allArgs.Skip(allArgs.Length - genericTypeDef.GetGenericArguments().Length).ToArray(); }
将原方法中的Type[] myArgumentTypes = type.GetGenericArguments();替换为上述方法调用,即可得到MyStruct自身的2个泛型参数,同时避免混入外层Class1的参数。原代码中复杂的外层参数过滤逻辑也可同步移除。
2. .NET中的标准方法生成XML注释成员名
.NET Framework没有公开的官方API直接生成XML注释规范的成员名称,但可以通过以下两种方式实现:
方式一:使用System.Reflection.Metadata(推荐)
在.NET Framework 4.6+或.NET Core/.NET 5+中,可借助System.Reflection.Metadata包读取程序集元数据,生成符合规范的成员名称。示例代码:
using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Reflection; public static string GetXmlMemberName(MemberInfo member) { using var stream = new FileStream(member.DeclaringType.Assembly.Location, FileMode.Open, FileAccess.Read); using var peReader = new PEReader(stream); var metadataReader = peReader.GetMetadataReader(); if (member is MethodInfo method) { var methodDefHandle = (MethodDefinitionHandle)MetadataTokens.EntityHandle(method.MetadataToken); var methodDef = metadataReader.GetMethodDefinition(methodDefHandle); var fullName = $"{metadataReader.GetString(method.DeclaringType.Namespace)}.{metadataReader.GetString(method.DeclaringType.Name)}`{method.DeclaringType.GetGenericArguments().Length}.{metadataReader.GetString(methodDef.Name)}"; // 完整实现需补充泛型参数、参数类型的格式化逻辑,此处为简化示例 return $"M:{fullName}"; } // 可扩展处理类型、属性等其他成员 return string.Empty; }
该方式能精准处理嵌套泛型、泛型参数编号等细节,生成符合XML注释规范的字符串。
方式二:复用成熟库的逻辑
DocFX、Sandcastle等文档生成库都包含完整的XML注释成员名生成逻辑,可直接参考其实现,或引入相关库的核心功能。例如Sandcastle的XmlCommentsReflectionImporter类就封装了全场景的成员名称生成逻辑。
若不想引入外部库,也可基于反射自行实现,但需覆盖嵌套泛型、泛型方法、数组类型等所有边缘场景,代码量较大。
内容的提问来源于stack exchange,提问作者Tecman

