I've got very weird behaviour which I suppose is related to dot-sourcing somehow, but I cannot wrap my head around it. Here's what I have:
A script sourced.ps1 which contains the two functions and a class:
class MyData {
[string] $Name
}
function withClass() {
$initialData = @{
Name1 = "1";
Name2 = "2";
}
$list = New-Object Collections.Generic.List[MyData]
foreach ($item in $initialData.Keys) {
$d = [MyData]::new()
$d.Name = $item
$list.Add($d)
}
}
function withString() {
$initialData = @{
Name1 = "1";
Name2 = "2";
}
$list = New-Object Collections.Generic.List[string]
foreach ($item in $initialData.Keys) {
$list.Add($item)
}
}
I also have a script caller.ps1 which dot-sources the one above and calls the function:
$ErrorActionPreference = 'Stop'
. ".\sourced.ps1"
withClass
I then call the caller.ps1 by executing .\caller.ps1 in the shell (Win terminal with PS Core).
Here's the behaviour I cannot explain: if I call .\caller.ps1, then .\sourced.ps1 and then caller.ps1 again, I get the error:
Line |
14 | $list.Add($d)
| ~~~~~~~~~~~~~
| Cannot find an overload for "Add" and the argument count: "1".
However, if I change the caller.ps1 to call withString function instead, everything works fine no matter how many times I call caller.ps1 and sourced.ps1.
Furthermore, if I first call caller.ps1 with withString, then change it to withClass, there is no error whatsoever.
I suppose using modules would be more correct, but I'm interested in the reason for such weird behaviour in the first place.
CodePudding user response:
Written as of PowerShell 7.2.1
A given script file that is both dot-sourced and directly executed (in either order, irrespective of how often) creates successive versions of the
classdefinitions in it - these are distinct .NET types, even though their structure is identical. Arguably, there's no good reason to do this, and the behavior may be a bug.These versions, which have the same full name (PowerShell
classdefinitions created in the top-level scope of scripts have only a name, no namespace) but are housed in different dynamic (in-memory) assemblies that differ by the last component of their version number, shadow each other, and which one is effect depends on the context:- Other scripts that dot-source such a script consistently see the new version.
- Inside the script itself, irrespective of whether it is itself executed directly or dot-sourced:
- In PowerShell code, the original version stays in effect.
- Inside binary cmdlets, notably
New-Object, the new version takes effect. - If you mix these two ways to access the class inside the script, type mismatches can occur, which is what happened in your case - see sample code below.
While you can technically avoid such errors by consistently using
::new()orNew-Objectto reference the class, it is better to avoid performing both direct execution and dot-sourcing of script files that containclassdefinitions to begin with.
Sample code:
Save the code to a script file, say,
demo.ps1Execute it twice.
- First, by direct execution:
.\demo.ps1 - Then, via dot-sourcing:
. .\demo.ps1
- First, by direct execution:
The type-mismatch error that you saw will occur during that second execution.
- Note: The error message,
Cannot find an overload for "Add" and the argument count: "1", is a bit obscure; what it is trying to express that is that the.Add()method cannot be called with the argument of the given type, because it expects an instance of the new version of[MyData], whereas::new()created an instance of the original version.
- Note: The error message,
# demo.ps1
# Define a class
class MyData { }
# Use New-Object to instantiate a generic list based on that class.
# This passes the type name as a *string*, and instantiation of the
# type happens *inside the cmdlet*.
# On the second execution, this will use the *new* [MyData] version.
Write-Verbose -Verbose 'Constructing list via New-Object'
$list = New-Object System.Collections.Generic.List[MyData]
# Use ::new() to create an instance of [MyData]
# Even on the second execution this will use the *original* [MyData] version
$myDataInstance = [MyData]::new()
# Try to add the instance to the list.
# On the second execution this will *fail*, because the [MyData] used
# by the list and the one that $myDataInstance is an instance of differ.
$list.Add($myDataInstance)
Note that if you used $myDataInstance = New-Object MyData, the type mismatch would go away.
Similarly, it would also go away if you stuck with ::new() and also used it to instantiate the list: $list = [Collections.Generic.List[MyData]]::new()
