Skip to content

Inconsistent resolution of ambiguous custom classes in constructors vs generic type arguments #20893

Closed

Description

Prerequisites

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 1

Environment 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.0

Visuals

No response

Activity

  1. changed the title [-]Inconsistent resolution of ambiguous types in constructors[/-] [+]Inconsistent resolution of ambiguous custom ```class```es in constructors[/+] on Dec 11, 2023
  2. changed the title [-]Inconsistent resolution of ambiguous custom ```class```es in constructors[/-] [+]Inconsistent resolution of ambiguous custom ```class```es in constructors vs generic type arguments[/+] on Dec 11, 2023
  3. mklement0 commented on Dec 11, 2023

    @mklement0
    Contributor

    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 with The member 'User' is already defined, i.e. redefinition of the class in the same file is deliberately prevented.

  4. daxian-dbw commented on Jan 31, 2024

    @daxian-dbw
    Member

    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 defined error 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 User class definition simply overrides the first one, after which only the second User class definition is visible to PowerShell when it does type resolution.

    So, in your case (mouse right-click),

    • When the first List is created, there is no ambiguity from PowerShell's point of view, the only visible User type is the first declaration.
    • When the second List is created, again there is no ambiguity from PowerShell's point of view, because the only visible User type to PowerShell at that point is the second User declaration.
  5. mikeclayton commented on Jan 31, 2024

    @mikeclayton
    Author

    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 $test2 from being added to the $users2 list.

    Both objects are created after the second class User { } so from what you're saying, that should should overwrite the first class User {} and both $user2 and $test2 should be constructed using the second User declaration, but if you execute $users2.GetType().GenericTypeArguments[0] -eq $test2.GetType() you can see they're not.

  6. mklement0 commented on Feb 1, 2024

    @mklement0
    Contributor

    Indeed, Michael Clayton (@mikeclayton).

    The explanation would only apply if [System.Collections.Generic.List[User]] weren't recreated after redefining class 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:

    class User { }
    [User].AssemblyQualifiedName
    $users1 = [System.Collections.Generic.List[User]]::new()
    $users1.GetType().GenericTypeArguments[0].AssemblyQualifiedName
    
    class User { }
    [User].AssemblyQualifiedName
    $users2 = [System.Collections.Generic.List[User]]::new()
    $users2.GetType().GenericTypeArguments[0].AssemblyQualifiedName

    The output, as of PowerShell 7.5.0-preview.1, shows that while class User was effectively redefined, the subsequent creation of [System.Collections.Generic.List[User]]::new() mistakenly still operated on the original User definition.

    User, PowerShell Class Assembly, Version=1.0.0.2, Culture=neutral, PublicKeyToken=null
    User, PowerShell Class Assembly, Version=1.0.0.2, Culture=neutral, PublicKeyToken=null
    User, PowerShell Class Assembly, Version=1.0.0.3, Culture=neutral, PublicKeyToken=null
    User, PowerShell Class Assembly, Version=1.0.0.2, Culture=neutral, PublicKeyToken=null
    
  7. mikeclayton commented on Feb 1, 2024

    @mikeclayton
    Author

    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 the List always 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…

  8. mikeclayton commented on Feb 2, 2024

    @mikeclayton
    Author

    Just to add another data point here, I've noticed that if you run this:

    class User { } # assembly version 1.0.0.1
    class User { } # assembly version 1.0.0.2
    $users2 = [System.Collections.Generic.List[User]]::new()
    $test2  = [User]::new()
    $users2.Add($test2)
    

    ... it works fine, with both $users2 and $test2 referencing the 1.0.0.2 version of the User class.

    However, adding a third class User {} then breaks things again...

    class User { } # assembly version 1.0.0.3
    $users3 = [System.Collections.Generic.List[User]]::new()
    $test3  = [User]::new()
    $users3.Add($test2)
    # MethodException: Cannot find an overload for "Add" and the argument count: "1".
    

    If I Use Michael Klement (@mklement0) 's approach to show the assembly versions:

    $users3.GetType().GetGenericArguments()[0].Assembly.FullName
    # PowerShell Class Assembly, Version=1.0.0.2, Culture=neutral, PublicKeyToken=null
    
    $test3.GetType().Assembly.FullName
    # PowerShell Class Assembly, Version=1.0.0.3, Culture=neutral, PublicKeyToken=null
    

    it seems that the version of [User] used by the generic List is evaluated and then cached at the first attempt to resolve the generic type argument (in this case, $users3 references User 1.0.0.2, not 1.0.0.1 or 1.0.03). Any subsequent references to User as 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 $test3 in this case).

  9. microsoft-github-policy-service commented on Feb 3, 2024

    @microsoft-github-policy-service
    Contributor

    This issue has been marked as by-design and has not had any activity for 1 day. It has been closed for housekeeping purposes.

  10. microsoft-github-policy-service commented on Feb 3, 2024

    @microsoft-github-policy-service
    Contributor

    📣 Hey Michael Clayton (@mikeclayton), how did we do? We would love to hear your feedback with the link below! 🗣️

    🔗 https://aka.ms/PSRepoFeedback

  11. ondrejtucny commented on Sep 28, 2026

    @ondrejtucny

    A related case with no user redefinition at all. A module dot-sources Class.ps1 (class Item {}) and Functions.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 next Import-Module -Force then compiles Class.ps1 again into a new PowerShell Class Assembly, but the TypeCache still maps List[Item] to the old Item. Result: Cannot find an overload for "Add" and Cannot convert "List1[Item]" ... to type "Item", only late in long sessions (big Pester runs). Without the flood, the same re-import keeps one Item` type and works. Repro and details: #28122

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Resolution-By DesignThe reported behavior is by design.WG-Interactive-Consolethe console experienceWG-TriagedThe issue has been triaged by the designated WG.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions