Repository navigation
[FEATURE] Add constructed generic candidate support and popup performance improvements - #100
daichotomy-inc wants to merge 2 commits into
Conversation
Add support for discovering and constructing closed generic candidate types (e.g. ConstantValueProvider<int>) when the field type is a closed generic interface or base class (Unity 2023.2+). Previously, only non-generic concrete classes that explicitly closed the type parameter were shown in the popup. Changes: - Relax intrinsic type policy to allow constructed generics through - Infer type arguments and construct closed types via MakeGenericType - Add generic-aware display names for popup menu and inline labels - Merge dual assembly scans into a single pass - Remove redundant filtering in TypeCandiateService - Cache attribute lookups, type paths, and nicified names - Pre-sort type array once at construction instead of on every Show
|
I have completed a separate investigation of the performance suggestions against main The performance concerns are real, but they involve different paths:
I am closing this combined PR because the constructed-generic serialization/type-resolution/Editor-stability policy from #96 remains in force. The performance changes also need a separate implementation that preserves the public service constructor and policy guarantees, public The investigation's Linux EditMode matrix passed on 2021.3.45f2 and 2023.2.22f1, including Verification. The package implementation is unchanged. Thank you for the proposal and the concrete performance observations. |
Description
This PR adds support for constructed generic candidate types in the SubclassSelector popup when the field type is a closed generic interface or base class (Unity 2023.2+), along with several performance improvements to the popup display path.
Motivation
Currently, when a field is declared as
[SerializeReference, SubclassSelector] IValueProvider<int>, the popup correctly discovers non-generic concrete classes that explicitly close the type parameter (e.g.class ConcreteIntProvider : IValueProvider<int>). However, it does not discover generic classes likeConstantValueProvider<T> : IValueProvider<T>— even though the type arguments can be inferred from the field type and the constructed typeConstantValueProvider<int>is fully serializable.This means users are forced to write one boilerplate subclass per type argument:
This PR removes that requirement by constructing closed generic types at editor time via
MakeGenericType, and also addresses a few performance bottlenecks that become more noticeable when scanning all loaded assemblies.Example
A note on design decisions
I understand that the existing
!candiateType.IsGenericTypefilter inDefaultIntrinsicTypePolicyand the overall architecture were likely deliberate choices. If any of these changes conflict with design goals I'm not aware of — such as keeping the intrinsic policy conservative, avoidingMakeGenericTypein editor code, or maintaining the separation of filtering responsibilities between provider and service — I'm completely open to adjusting or withdrawing parts of this PR. I've tried to keep the changes minimal and contained to the 2023.2+ code path where possible.Changes made
1. Constructed generic candidate support (Unity 2023.2+)
DefaultIntrinsicTypePolicy.cs!candiateType.IsGenericTypeto!candiateType.ContainsGenericParameters. This allows closed constructed generics (e.g.ConstantValueProvider<int>) to pass through while still blocking open generic definitions (e.g.ConstantValueProvider<T>). This is the only change that affects the non-2023.2 code path.Unity_2023_2_OrNewer_TypeCandiateProvider.csGetTypesWithGenericto detect open generic type definitions in the assembly scan, attempt to infer and map their type parameters from the field's base type, and construct closed generic types viaMakeGenericType.TryCloseGenericType(walks interfaces and base class chain to find matching generic definitions),TryMapTypeArguments(maps generic parameters from the candidate to concrete type arguments from the field type), andTryMakeGenericTypeSafe(wrapsMakeGenericTypein a try-catch for constraint violations).2. Generic-aware display names
TypeMenuUtility.csGetNiceGenericNameandGetNiceGenericFullNamehelpers that produce human-readable names for constructed generic types (e.g.ConstantValueProvider<Int32>instead ofConstantValueProvider`1[[System.Int32, ...]]).GetSplittedTypePathto use these helpers for the popup menu paths.OrderByTypeto useGetNiceGenericNameas the fallback sort key.SubclassSelectorDrawer.csGetTypeNameto useGetNiceGenericNameso that the inline label displays correctly for constructed generic types (the backtick in rawtype.NamecausedObjectNames.NicifyVariableNameto produce an empty string).3. Performance improvements
These changes address noticeable lag (~500ms) when opening the popup, particularly when generic field types trigger a full assembly scan.
TypeCandiateService.csintrinsicTypePolicy.IsAllowedandtypeCompatibilityPolicy.IsCompatiblefiltering inGetDisplayableTypes, since both providers already apply these filters internally. Simplified the constructor to only requireITypeCandiateProvider.TypeSearchService.csTypeCandiateServiceconstruction to match the simplified constructor.TypeMenuUtility.csDictionary<Type, AddTypeMenuAttribute>cache forGetAttributeto avoid repeatedAttribute.GetCustomAttributereflection calls (invoked multiple times per type during sorting and path resolution on every popup open).Dictionary<Type, string[]>cache forGetSplittedTypePathresults.Dictionary<string, string>cache forObjectNames.NicifyVariableNameresults via a newCachedNicifyVariableNamemethod. All caches include null guards for safety.AdvancedTypePopup.csOrderByTypesorting fromBuildRoot(called by Unity on everyShow) intoSetTypes(called once at construction), so the type array is sorted once and reused across popup opens.All static caches are naturally cleared on domain reload (script recompilation), so newly added subclasses are always picked up.