@@ -29,6 +29,18 @@ internal class ClassBase : ManagedType, IDeserializationCallback
2929 internal readonly Dictionary < int , MethodObject > richcompare = new ( ) ;
3030 internal MaybeType type ;
3131
32+ // Reflecting over a managed type's full member set (with FlattenHierarchy) plus the
33+ // snake_case conversion is expensive, and the result never changes for a given type.
34+ // Compute it once per type.
35+ private static readonly ConcurrentDictionary < Type , HashSet < string > > _candidateNameCache = new ( ) ;
36+
37+ // A miss-heavy workload probes the same missing names over and over (e.g. a per-bar
38+ // getattr(self, "_optional", None) on a .NET-derived object). Memoize the fully-built
39+ // " Did you mean: ...?" hint (empty when there is nothing to suggest) per
40+ // (type, missing-name) so repeats are a dictionary lookup instead of an O(members)
41+ // reflection + Levenshtein scan on every miss.
42+ private static readonly ConcurrentDictionary < ( Type Type , string Name ) , string > _suggestionCache = new ( ) ;
43+
3244 internal ClassBase ( Type tp )
3345 {
3446 if ( tp is null ) throw new ArgumentNullException ( nameof ( type ) ) ;
@@ -730,26 +742,13 @@ private static string GetErrorMessage(BorrowedReference value, string fallbackNa
730742 return $ "object has no attribute '{ fallbackName } '";
731743 }
732744
733- // Reflecting over a managed type's full member set (with FlattenHierarchy) plus the
734- // snake_case conversion is expensive, and the result never changes for a given type.
735- // Compute it once per type.
736- private static readonly ConcurrentDictionary < Type , string [ ] > _candidateNameCache = new ( ) ;
737-
738- // A miss-heavy workload probes the same missing names over and over (e.g. a per-bar
739- // getattr(self, "_optional", None) on a .NET-derived object). Memoize the fully-built
740- // " Did you mean: ...?" hint (empty when there is nothing to suggest) per
741- // (type, missing-name) so repeats are a dictionary lookup instead of an O(members)
742- // reflection + Levenshtein scan on every miss.
743- private static readonly ConcurrentDictionary < ( Type Type , string Name ) , string > _suggestionCache = new ( ) ;
744-
745745 // The deduplicated snake_case member names of a type, cached so the reflection and
746746 // string conversion happen at most once per type rather than on every attribute miss.
747- private static string [ ] GetCandidateMemberNames ( Type type )
747+ private static HashSet < string > GetCandidateMemberNames ( Type type )
748748 {
749749 return _candidateNameCache . GetOrAdd ( type , static t =>
750750 {
751- var seen = new HashSet < string > ( StringComparer . Ordinal ) ;
752- var names = new List < string > ( ) ;
751+ var names = new HashSet < string > ( StringComparer . Ordinal ) ;
753752
754753 var members = t . GetMembers ( BindingFlags . Public | BindingFlags . Instance
755754 | BindingFlags . Static | BindingFlags . FlattenHierarchy ) ;
@@ -769,14 +768,10 @@ private static string[] GetCandidateMemberNames(Type type)
769768
770769 // Suggest the snake_case alias, since that is the fork's PEP8-style
771770 // public API surface (members are exposed in both Pascal and snake case).
772- var candidate = ToSnakeCaseMemberName ( member ) ;
773- if ( seen . Add ( candidate ) )
774- {
775- names . Add ( candidate ) ;
776- }
771+ names . Add ( ToSnakeCaseMemberName ( member ) ) ;
777772 }
778773
779- return names . ToArray ( ) ;
774+ return names ;
780775 } ) ;
781776 }
782777
0 commit comments