Repository navigation
Inconsistent resolution of ambiguous custom classes in constructors vs generic type arguments #20893
Description
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
Per this stackoverflow question there appears to be an inconsistency in the heuristics used for resolution of types in constructors vs generic type arguments which results in different types being selected when multiple ambiguous custom classes exist.
The result is that, for example, [my_ambiguous_type]::new() vs [my_generic_type[my_ambiguous_type]]::new() use different instances of the ambiguous my_ambiguous_type that can cause type mismatch errors later in the code.
The obvious solution is "don't define ambiguous types in a PowerShell session then", but it would be good if PowerShell could either identify the ambiguity and report a specific error (which might be a breaking change, I guess) or at least consistently choose the same instance of the ambiguous type for both scenarios.
To reproduce:
class User { }
$users1 = [System.Collections.Generic.List[User]]::new()
$test1 = [User]::new()
$users1.Add($test1)
class User { }
$users2 = [System.Collections.Generic.List[User]]::new()
$test2 = [User]::new()
$users2.Add($test2)
# MethodException: Cannot find an overload for "Add" and the argument count: "1".The call to $users2.Add throws an exception because $users2 = [System.Collections.Generic.List[User]]::new() uses the original User custom class instance as its generic type parameter, but $test2 = [User]::new() uses the latest User type to create a new object.
Some additional symptoms:
# canary just to prove we can compare types
$test1.GetType() -eq $test1.GetType()
# True
# $test2 is of a different type to $test1
$test2.GetType() -eq $test1.GetType()
# False
# our first generic collection can hold instances our *first* User class
$users1.GetType().GenericTypeArguments[0] -eq $test1.GetType()
# True
# but our second generic collection *cannot* hold instances our *second* User class
$users2.GetType().GenericTypeArguments[0] -eq $test2.GetType()
# False
# so what type can it hold? well, instances of our *first* User class
$users2.GetType().GenericTypeArguments[0] -eq $test1.GetType()
# True
I've also tried this with three definitions of a custom class and the object constructor ::new() still uses the most recent type, whereas the generic type argument uses the first custom type.
Expected behavior
Some suggestions:
-
throw an exception when trying to resolve (or maybe when trying to define?) ambiguous custom class types - probably a breaking change
-
just consistently use the most recent definition of the type in both scenarios. This would solve
$users2.Add($test2)but would still cause an error if trying to execute$users2.Add($test1)above. It would at least be less suprising in scenarios where code (or an interactive command) has explicitly redefined a class as the new class type would at least be used consistently for constructors and generic type arguments in any code that is executed after the redefinition.
Actual behavior
The exception MethodException: Cannot find an overload for "Add" and the argument count: "1". when running $users2.Add($test2) because the generic type parameter used to construct the System.Collections.Generic.List object $users2 is the first definition of User but the $test2 variable was created using the second definition of User
Error details
Exception :
Type : System.Management.Automation.MethodException
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : Cannot find an overload for "Add" and the argument count: "1".
HResult : -2146233087
CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : MethodCountCouldNotFindBest
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 1
HistoryId : 8
Line : $users2.Add($test2)
Statement : $users2.Add($test2)
PositionMessage : At line:1 char:1
+ $users2.Add($test2)
+ ~~~~~~~~~~~~~~~~~~~
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, <No file>: line 1
TargetSite : System.Object CallSite.Target(System.Runtime.CompilerServices.Closure,
System.Runtime.CompilerServices.CallSite, System.Object, System.Object)
Message : Cannot find an overload for "Add" and the argument count: "1".
Data : System.Collections.ListDictionaryInternal
Source : Anonymously Hosted DynamicMethods Assembly
HResult : -2146233087
StackTrace :
at CallSite.Target(Closure, CallSite, Object, Object)
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
CategoryInfo : NotSpecified: (:) [], MethodException
FullyQualifiedErrorId : MethodCountCouldNotFindBest
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 1
HistoryId : 8
Line : $users2.Add($test2)
Statement : $users2.Add($test2)
PositionMessage : At line:1 char:1
+ $users2.Add($test2)
+ ~~~~~~~~~~~~~~~~~~~
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, <No file>: line 1Environment data
Name Value
---- -----
PSVersion 7.4.0
PSEdition Core
GitCommitId 7.4.0
OS Microsoft Windows 10.0.19044
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0Visuals
No response
Activity
[-]Inconsistent resolution of ambiguous types in constructors[/-][+]Inconsistent resolution of ambiguous custom ```class```es in constructors[/+][-]Inconsistent resolution of ambiguous custom ```class```es in constructors[/-][+]Inconsistent resolution of ambiguous custom ```class```es in constructors vs generic type arguments[/+]mklement0 commentedon Dec 11, 2023
One thing I've noticed is that the problem only seems to occur when I paste the code directly into an interactive session.
By contrast, as part of a single script (whether dot-sourced in the global scope or not), the second
class User {}definition fails withThe member 'User' is already defined, i.e. redefinition of the class in the same file is deliberately prevented.daxian-dbw commentedon Jan 31, 2024
The Interactive Console WG reviewed this issue today and we believe the behavior is by design.
If you paste the whole repro scripts into the interactive session with
Ctrl+v, then you will see the same'User' is already definederror as you run from a script file. That error is thrown during the parsing when PowerShell finds multiple class symbols are declared with the same name.However, PowerShell allows a predefined PowerShell class to be overridden by a new definition. And that's what happens when you paste the repro scripts into console with mouse right-click. When pasting with mouse right-click, you are essentially pasting each line separately, so the second
Userclass definition simply overrides the first one, after which only the secondUserclass definition is visible to PowerShell when it does type resolution.So, in your case (mouse right-click),
Listis created, there is no ambiguity from PowerShell's point of view, the only visibleUsertype is the first declaration.Listis created, again there is no ambiguity from PowerShell's point of view, because the only visibleUsertype to PowerShell at that point is the secondUserdeclaration.mikeclayton commentedon Jan 31, 2024
Dongbo Wang (@daxian-dbw) - I might be misunderstanding, but that doesn't seem to explain why
$users2 = [System.Collections.Generic.List[User]]::new()and$test2 = [User]::new()are created from different instances of the[User]class which prevents$test2from being added to the$users2list.Both objects are created after the second
class User { }so from what you're saying, that should should overwrite the firstclass User {}and both$user2and$test2should be constructed using the secondUserdeclaration, but if you execute$users2.GetType().GenericTypeArguments[0] -eq $test2.GetType()you can see they're not.mklement0 commentedon Feb 1, 2024
Indeed, Michael Clayton (@mikeclayton).
The explanation would only apply if
[System.Collections.Generic.List[User]]weren't recreated after redefiningclass User(an example of where it does apply just came up in #21151)Here's a more direct demonstration - again, note that you have to submit the commands one by one, from an interactive prompt to see the problem - which makes the problem exotic.
On Windows, be sure to paste the code with a right click to prevent submission of all commands together:
The output, as of PowerShell 7.5.0-preview.1, shows that while class
Userwas effectively redefined, the subsequent creation of[System.Collections.Generic.List[User]]::new()mistakenly still operated on the originalUserdefinition.mikeclayton commentedon Feb 1, 2024
Without wanting to lead things down a blind alley, it’s almost like the “generic type parameter” resolver is caching type references but the
::new()type resolver is doing a fresh lookup on every invocation.If you execute
class User { }many times the pattern is always that theListalways uses the first type declaration for its generic type parameter, whereas::new()uses the most recent type declaration.That would fit the pattern, but it’s complete speculation on my part…
mikeclayton commentedon Feb 2, 2024
Just to add another data point here, I've noticed that if you run this:
... it works fine, with both
$users2and$test2referencing the 1.0.0.2 version of theUserclass.However, adding a third
class User {}then breaks things again...If I Use Michael Klement (@mklement0) 's approach to show the assembly versions:
it seems that the version of
[User]used by the genericListis evaluated and then cached at the first attempt to resolve the generic type argument (in this case,$users3referencesUser1.0.0.2, not 1.0.0.1 or 1.0.03). Any subsequent references toUseras a generic type argument use the cached type reference.However, the
[User]::new()is not cached and resolves dynamically at the time of execution (1.0.0.3 for$test3in this case).microsoft-github-policy-service commentedon Feb 3, 2024
This issue has been marked as by-design and has not had any activity for 1 day. It has been closed for housekeeping purposes.
microsoft-github-policy-service commentedon Feb 3, 2024
📣 Hey Michael Clayton (@mikeclayton), how did we do? We would love to hear your feedback with the link below! 🗣️
🔗 https://aka.ms/PSRepoFeedback
ondrejtucny commentedon Sep 28, 2026
A related case with no user redefinition at all. A module dot-sources
Class.ps1(class Item {}) andFunctions.ps1(a function with a[List[Item]]parameter). The source never changes. After the process compiles more than 1024 distinct scripts, the compiled-script cache (s_cachedScripts) is cleared. The nextImport-Module -Forcethen compilesClass.ps1again into a newPowerShell Class Assembly, but the TypeCache still mapsList[Item]to the oldItem. Result:Cannot find an overload for "Add"andCannot convert "List1[Item]" ... to type "Item", only late in long sessions (big Pester runs). Without the flood, the same re-import keeps oneItem` type and works. Repro and details: #28122