For example I have a .NET object $m with the following method overloads:
PS C:\Users\Me> $m.GetBody
OverloadDefinitions
-------------------
T GetBody[T]()
T GetBody[T](System.Runtime.Serialization.XmlObjectSerializer serializer)
If I try to invoke the parameterless method I get:
PS C:\Users\Me> $m.GetBody()
Cannot find an overload for "GetBody" and the argument count: "0".
At line:1 char:1
+ $m.GetBody()
+ ~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
I understand PowerShell v3.0 is supposed to work more easily with generics. Obviously I need to tell it somehow what type I want returned but I cannot figure out the syntax.
It looks like you are trying to invoke a generic method.
In powershell this can be done by:
$nonGenericClass = New-Object NonGenericClass
$method = [NonGenericClass].GetMethod("SimpleGenericMethod")
$gMethod = $method.MakeGenericMethod([string])
# replace [string] with the type you want to use for T.
$gMethod.Invoke($nonGenericClass, "Welcome!")
See this wonderful blog post for more info and additional examples.
For your example you could try:
$Source = #"
public class TestClass
{
public T Test<T>()
{
return default(T);
}
public int X;
}
"#
Add-Type -TypeDefinition $Source -Language CSharp
$obj = New-Object TestClass
$Type = $obj.GetType();
$m = $Type.GetMethod("Test")
$g = new-object system.Guid
$gType = $g.GetType()
$gm = $m.MakeGenericMethod($gType)
$out = $gm.Invoke( $obj, $null)
#$out will be the default GUID (all zeros)
This can be simplified by doing:
$Type.GetMethod("Test").MakeGenericMethod($gType).Invoke( $obj, $null)
This has been testing in powershell 2 and powershell 3.
If you had a more detailed example of how you came across this generic method I would be able to give more details. I have yet to see any microsoft cmdlets return anything that give you generic methods. The only time this comes up is when custom objects or methods from c# or vb.net are used.
To use this without any parameters you can use Invoke with just the first parameter.
$gMethod.Invoke($nonGenericClass)
Calling a generic method on an object instance:
$instance.GetType().GetMethod('MethodName').MakeGenericMethod([TargetType]).Invoke($instance, $parameters)
Calling a static generic method (see also Calling generic static method in PowerShell):
[ClassType].GetMethod('MethodName').MakeGenericMethod([TargetType]).Invoke($null, $parameters)
Note that you will encounter an AmbiguousMatchException when there is also a non-generic version of the method (see How do I distinguish between generic and non generic signatures using GetMethod in .NET?). Use GetMethods() then:
([ClassType].GetMethods() | where {$_.Name -eq "MethodName" -and $_.IsGenericMethod})[0].MakeGenericMethod([TargetType]).Invoke($null, $parameters)
(Mind that there could be more than one method that match the above filter, so make sure to adjust it to find the one you need.)
Hint: You can write complex generic type literals like this (see Generic type of Generic Type in Powershell):
[System.Collections.Generic.Dictionary[int,string[]]]
To call a (parameterless) generic method with overloads from Powershell v3, as shown in the OP example, use the script Invoke-GenericMethod.ps1 from the reference provided by #Chad Carisch, Invoking Generic Methods on Non-Generic Classes in PowerShell.
It should look something like
Invoke-GenericMethod $m GetBody T #()
This is a verified working code sample that I am using:
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Practices.ServiceLocation") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Practices.SharePoint.Common") | Out-Null
$serviceLocator = [Microsoft.Practices.SharePoint.Common.ServiceLocation.SharePointServiceLocator]::GetCurrent()
# Want the PowerShell equivalent of the following C#
# config = serviceLocator.GetInstance<IConfigManager>();
# Cannot find an overload for "GetInstance" and the argument count: "0".
#$config = $serviceLocator.GetInstance()
# Exception calling "GetMethod" with "1" argument(s): "Ambiguous match found."
#$config = $serviceLocator.GetType().GetMethod("GetInstance").MakeGenericMethod([IConfigManager]).Invoke($serviceLocator)
# Correct - using Invoke-GenericMethod
$config = C:\Projects\SPG2013\Main\Scripts\Invoke-GenericMethod $serviceLocator GetInstance Microsoft.Practices.SharePoint.Common.Configuration.IConfigManager #()
$config.CanAccessFarmConfig
Here is an alternate script that I haven't tried but is more recent and being actively maintained, Invoke Generic Methods from PowerShell.
Update: PowerShell (Core) 7.3+ now does support calling
generic methods with explicit type arguments.
Note: As of this writing, only preview versions of 7.3 are available.
# PS v7.3+ only; using [string] as an example type argument.
$m.GetBody[string]()
See the conceptual about_Calling_Generic_Methods help topic
PowerShell (Core) 7.2- and Windows PowerShell:
marsze's helpful answer contains great general information about calling generic methods, but let me address the aspect of calling a parameter-less one specifically, as asked:
As hinted at in the question:
in PSv3+ PowerShell can infer the type from the parameter values (arguments) passed to a generic method,
which by definition cannot work with a parameter-less generic method, because there is nothing to infer the type from.
Prior to PowerShell (Core) 7.3, PowerShell previously had no syntax that would allow you to specify the type explicitly in this scenario.
In such older versions, reflection must be used:
# Invoke $m.GetBody[T]() with [T] instantiated with type [decimal]
$m.GetType().GetMethod('GetBody', [type[]] #()).
MakeGenericMethod([decimal]).
Invoke($m, #())
.GetMethod('GetBody', [type[]] #()) unambiguously finds the parameter-less overload of.GetBody(), due to passing in an empty array of parameter types.
.MakeGenericMethod([decimal]) instantiates the method with example type [decimal].
.Invoke($m, #()) then invokes the type-instantiated method on input object ($m) with no arguments (#(), the empty array).
Related
I would just like to get the validation arguments provided by an IValidateSetValuesGenerator class similar to a return when we use a bad argument (see example command Sample -Verb 'BadVerb')
Below is an example of code:
class verb : System.Management.Automation.IValidateSetValuesGenerator
{
[String[]] GetValidValues()
{
[System.Collections.ArrayList]$Verbs = #()
$VerbsSource = Get-Verb
foreach ($Verb in $VerbsSource)
{
$Verbs.Add([PSCustomObject]#{'verb' = $Verb.Verb})
}
return ($Verbs).Verb
}
}
function Sample
{
[CmdletBinding(SupportsShouldProcess=$true,
PositionalBinding=$false,
ConfirmImpact='Medium')]
[Alias()]
[OutputType([bool])]
Param
(
[Parameter(Mandatory=$true)]
[ValidateSet([verb], ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]
[string]$Verb
)
Begin
{
}
Process
{
}
End
{
return $Verb
}
}
Sample -Verb 'BadVerb'
Sample -Verb 'Get'
Mathias R. Jessen has provided the crucial pointer:
# PSv5+ syntax:
# Construct (create an instance of) the [verb] class and call its
# .GetValidValues() instance method.
[verb]::new().GetValidValues() # Returns a [string[]] array of valid values.
Your [verb] class implements the [System.Management.Automation.IValidateSetValuesGenerator] interface (for use in [ValidateSet] attributes to allow constraining parameter values to a dynamically generated set of valid (permissible) values).
This interface, has a single instance method, .GetValidValues(), which returns the permissible values, and which PowerShell calls behind the scenes during parameter validation.
Therefore, in order to call this method yourself, you need to create an instance of your [verb] class first:
In PowerShell v5+, the best choice is to use the static ::new() method, which is PowerShell's way of exposing public constructors; that is, [verb]::new() is equivalent to new verb() in C#.
In older PowerShell versions you must use the New-Object cmdlet for calling constructors; the equivalent of [verb]::new() is New-Object verb
# PowerShell v5+
[verb]::new().GetValidValues()
# PowerShell v4-, but also works in higher versions.
(New-Object verb).GetValidValues()
Syntax pitfalls:
::new() uses method syntax (as in C#), whereas New-Object, as a cmdlet, uses command syntax,[1] i.e. is invoked like a shell command: no (...) around the list of arguments, whitespace as the argument separator.
The following example - using a constructor with arguments - illustrates the difference:
# PSv5+ ::new() call - method syntax.
# Equivalent of this C# constructor call:
# new Regex("^\w+=.+", RegexOptions.Multiline, new Timespan(1000));
[regex]::new('^\w+=.+', [System.Text.RegularExpressions.RegexOptions]::Multiline, [timespan]:new(1000))
# Equivalent New-Object call - command syntax,
# using verbose *named* parameter binding.
# Note the following:
# * absence of (...) around the list of arguments as a whole
# * use of whitespace to separate arguments (and also parameter names from their arguments)
# * the need to separate the *constructor* arguments with ","
# as they must be passed as an *array*.
# * the need to enclose [System.Text.RegularExpressions.RegexOptions]::Multiline individually in (...)
New-Object -TypeName regex -ArgumentList '^\w+=.+', ([System.Text.RegularExpressions.RegexOptions]::Multiline)
# Equivalent call using *positional* (unnamed) parameter binding.
New-Object regex '^\w+=.+', ([System.Text.RegularExpressions.RegexOptions]::Multiline)
A particular pitfall is when a constructor takes a single argument that is an array or a collection, which with New-Object requires wrapping the array in an aux., transitory array:
$array = 1, 2
# OK: Initialize an ArrayList instance via an array that
# binds as a whole to the `System.Collections.ICollection c`
# constructor parameter.
[System.Collections.ArrayList]::new($array)
# !! BROKEN
# $array is interpreted as *multiple* (two separate) arguments.
New-Object System.Collections.ArrayList -ArgumentList $array
# OK
# Need to wrap the array in an aux. transitory array.
New-Object System.Collections.ArrayList -ArgumentList (, $array)
[1] In PowerShell terms, method syntax is parsed in expression mode, whereas command syntax is parsed in argument mode. See the conceptual about_Parsing help topic.
In addition to mklement0's excellent explanation of [Verb]::new(), allow me to provide you with an example of how you could structure the value generator class so that you don't need an instance to generate the values:
class verb : System.Management.Automation.IValidateSetValuesGenerator
{
[String[]] GetValidValues()
{
return [Verb]::GetValidValues()
}
static [String[]] GetValidValues()
{
[System.Collections.ArrayList]$Verbs = #()
$VerbsSource = Get-Verb
foreach ($Verb in $VerbsSource)
{
$Verbs.Add([PSCustomObject]#{'verb' = $Verb.Verb})
}
return ($Verbs).Verb
}
}
Now we've moved the actual heavy lifting to a static method, and the instance method (which is what PowerShell is going to call) simply calls the static method.
Now you can get the value list without creating an instance of [verb]:
PS ~> [verb]::GetValidValues()
I am trying to use the method outlined here to create a number of custom data types, and rather than having a line for each I would like to define a hash table of the names and types like this
$pxAccelerators = #{
pxListObject = '[System.Collections.Generic.List[Object]]'
pxListString = '[System.Collections.Generic.List[String]]'
pxOrderedDictionary = '[System.Collections.Specialized.OrderedDictionary]'
}
Then I could use something like this
$typeAccelerators = [PowerShell].Assembly.GetType("System.Management.Automation.TypeAccelerators")
foreach ($key in $pxAccelerators.Keys) {
$name = $key
$type = $pxAccelerators.$key
$typeAccelerators::Add($key,$type)
}
to loop through the hash table and add each one.
However, the issue of course is that $type isn't an actual type, it's a string. And $typeAccelerators::Add($key,$type) needs a string and an actual type. So basically I need to coerce a string like '[System.Collections.Specialized.OrderedDictionary]' to the actual type. I have found plenty of references to casting or coercing from one data type to another, but I can't seem to find any reference to how to convert a string to a type as defined BY the string.
I have tried all these stabs in the dark
([System.Type]'[System.Collections.ArrayList]')::new()
[System.Type]'[System.Collections.ArrayList]'
[System.Type]'[System.Collections.ArrayList]' -as [System.Type]
'[System.Collections.ArrayList]' -as ([PowerShell].Assembly.GetType('[System.Collections.ArrayList]')
to no avail.
$type = ([PowerShell].Assembly.GetType('[System.Collections.ArrayList]')) seems to work, in that it doesn't throw an exception. But $type.GetType() does throw You cannot call a method on a null-valued expression..
Interestingly, auto completion with [PowerShell].Assembly.GetType('[System.Collections.ArrayList]'). shows properties like BaseType and FullName are available, suggesting that I have actually produced a type, but using .GetType() on the result throws an exception.
I tried
$pxAccelerators = #{
pxListObject = 'System.Collections.Generic.List[Object]'
pxListString = 'System.Collections.Generic.List[String]'
pxOrderedDictionary = 'System.Collections.Specialized.OrderedDictionary'
}
$typeAccelerators = [PowerShell].Assembly.GetType("System.Management.Automation.TypeAccelerators")
foreach ($key in $pxAccelerators.Keys) {
$name = $key
$type = [PowerShell].Assembly.GetType($pxAccelerators.$key)
$typeAccelerators::Add($key,$type)
}
[PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Get
and the accelerators are being added, but the type to be acclerated is not there, suggesting that the GetType() line is not actually producing a type.
Lastly, I found this which seems to be getting closer. But I can't seem to rock how to access the method without starting from some sort of type already, and [System.Type].GetType('System.Int32') is throwing, so that seems to be a dead end.
Am I trying to do something impossible? Or just missing the proper mechanism?
-as [type] will do
The -as operator gladly takes a type name as it's right-hand side operand.
Change the values of your dictionary to contain just a valid type name, and it becomes as easy as:
$pxAccelerators = #{
pxListObject = 'System.Collections.Generic.List[Object]'
pxListString = 'System.Collections.Generic.List[String]'
pxOrderedDictionary = 'System.Collections.Specialized.OrderedDictionary'
}
$typeAccelerators = [PowerShell].Assembly.GetType("System.Management.Automation.TypeAccelerators")
foreach ($acc in $pxAccelerators.GetEnumerator()) {
$name = $acc.Key
$type = $acc.Value -as [type]
$typeAccelerators::Add($name,$type)
}
Result:
PS ~> [pxOrderedDictionary] -is [type]
True
PS ~> [pxOrderedDictionary].FullName
System.Collections.Specialized.OrderedDictionary
Ultimately, your only problem was that you accidentally enclosed your type-name string values in [...] (e.g, '[System.Collections.ArrayList]'), whereas this notation only works in - unquoted - type literals:
[...] is a PowerShell-specific notation for type literals (e.g., [System.DateTime]); .NET methods do not recognize this, although the string inside [...] (e.g., System.DateTime) uses a superset of the language-agnostic notation recognized by .NET methods such as System.Type.GetType() - see bottom section.
In your case, you don't even need explicit conversion of your type-name strings to [type] objects, because PowerShell implicitly performs this conversion when you call the ::Add() method. Therefore, the following is sufficient:
#{
pxListObject = 'System.Collections.Generic.List[Object]'
pxListString = 'System.Collections.Generic.List[String]'
pxOrderedDictionary = 'System.Collections.Specialized.OrderedDictionary'
}.GetEnumerator() | ForEach-Object {
# Calling ::Add() implicitly converts the type-name *string* to the required
# [type] instance.
[powershell].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Add(
$_.Key,
$_.Value
)
}
Of course, you could have directly used type literals as your hashtable values:
# Note the use of [...] values (type literals) instead of strings.
#{
pxListObject = [System.Collections.Generic.List[Object]]
pxListString = [System.Collections.Generic.List[String]]
pxOrderedDictionary = [System.Collections.Specialized.OrderedDictionary]
}.GetEnumerator() | ForEach-Object {
[powershell].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Add(
$_.Key,
$_.Value
)
}
Caveat:
Type System.Management.Automation.TypeAccelerators is non-public (hence the need to access it via reflection), and relying on non-public APIs is problematic.
The alternative, available in PowerShell v5+, is to use the using namespace statement so you don't have to specify a full type name (see the bottom section for an example) - though note that it invariably makes all types in the specified namespace available by simple name (no namespace part).
As for what you tried:
$type = [PowerShell].Assembly.GetType('[System.Collections.ArrayList]') seems to work, in that it doesn't throw an exception.
As stated, the outer [...] are not a part of the type name; however, removing them wouldn't be enough here, because the specified type isn't part of the same assembly as [PowerShell].
Generally, System.Type.GetType() quietly returns $null if the specified type cannot be found; e.g.: [PowerShell].Assembly.GetType('DefinitelyNoSuchType')
[PowerShell].Assembly.GetType($pxAccelerators.$key)
suggesting that the GetType() line is not actually producing a type.
The reason is that System.Type.GetType() doesn't recognize closed generic types such as System.Collections.Generic.List[String], because the underlying notation only supports open ones (such as System.Collections.Generic.List`1); the former notation is a PowerShell-specific extension to the notation - see the bottom section.
Converting type names stored in strings - without the enclosing [...] - to System.Type ([type]) objects:
Cast to [type]:
# Same as: [System.Collections.Generic.List[string]]; case does not matter.
[type] 'System.Collections.Generic.List[string]'
Casting to [type] does more than calling System.Type.GetType(), because PowerShell's type-name strings support a superset of the latter's notation - see the bottom section.
The above will cause a statement-terminating error (exception) if no such type is found (or the syntax is incorrect); note that only types among currently loaded assemblies are found.
Alternatively, you can use 'System.Collections.Generic.List[string]' -as [type], as shown in Mathias R. Jessen's answer, which quietly returns $null if conversion to a type isn't possible; see -as, the conditional type conversion operator.
Note that both -as and -is also accept type-name strings as their RHS; e.g. 42 -is 'int' or '42' -as 'int'
Also, the New-Object cmdlet accepts strings as its -TypeName argument, so again the type-literal-enclosing [...] must not be used; e.g.,
New-Object -TypeName System.Text.UTF8Encoding
PowerShell's type-literal and type-name notation:
A PowerShell type literal (e.g., [System.DateTime]) is a type name enclosed in [...], but the [ and ] are not part of the name.
Type literals are themselves instance of System.Type ([type]), or, more accurately, instances of the non-public [System.RuntimeType] type, which derives from System.Reflection.TypeInfo, which in turn derives from System.Type.
Type-name strings can generally also be used where type literals (objects representing types) are accepted, with PowerShell converting to [type] implicitly, notably during parameter binding and on the RHS of -as, the conditional type-conversion operator and -is, the type(-inheritance) / interface test operator.
PowerShell implements a superset of the language-agnostic type-name notation used by .NET:
The language-agnostic .NET notation, as used by System.Type.GetType(), for instance, is documented in Specifying fully qualified type names, and notably requires and includes:
Type names must be (at least) namespace-qualified names (e.g., System.Int32 rather than just Int32), and must be specified case-exactly by default; however, an overload with an ignoreCase parameter is available; names can optionally be assembly-qualified so as to also indicate the type's assembly of origin.
Suffix `<num> indicates the arity of generic types, i.e, the number (<num>) of type arguments the generic type requires (e.g, `1 for a type with one generic type parameter, such as System.Collections.Generic.List`1).
Using only a generic type's arity returns a generic type definition.
A generic type definition cannot be directly instantiated; instead, you must call the .MakeGenericType() instance method on the type object returned and pass (closed) type arguments for all generic type parameters, which then returns a closed [constructed] generic type, i.e. a concrete instantiation of the generic type definition from which instances can be constructed.
If you optionally follow the generic arity specifier with a list of type arguments to bind all type parameters defined by the generic type, a constructed generic type is directly returned; if all type arguments are closed themselves, transitively, i.e. have no unbound type parameters, if applicable, the type returned is a closed [constructed] type, which can directly be instantiated (its constructors can be called). The list of type arguments has the form [<type>] or, for multiple type arguments, [<type>, ...]; <type> references are subject to the same rules as the enclosing generic type's name, and may additionally be enclosed in [...] individually.
For instance, the following two type literals, which construct a closed Dictionary<TKey,TValue> type (to use C# notation for a change) with [string] keys and [int] values, are therefore equivalent:
System.Collections.Generic.Dictionary`2[System.String, System.Int32]
System.Collections.Generic.Dictionary`2[[System.String], [System.Int32]]
Suffixes [] / [,] indicate a single-dimensional / two-dimensional array; e.g., System.Int32[].
+ is used to separate nested types from their containing class; e.g., System.Environment+SpecialFolder.
The PowerShell-specific extensions are:
Name matching is invariably case-insensitive; e.g., type System.Text.UTF8Encoding can be targeted as [system.text.utf8encoding].
You may omit the System. component of the full name; e.g., [Text.UTF8Encoding]
You may use the names of PowerShell's type accelerators; e.g., [regex].
In PowerShell v5+ you can also use the using namespace statement so you don't have to specify a full type name:
E.g., placing using namespace System.Text at the top of a script or script module then allows you to refer to [System.Text.UTF8Encoding] as just [UTF8Encoding]
When you pass a list of generic type arguments, you may omit the type's arity specifier.
For instance, the following two type literals, which construct a closed Dictionary<TKey,TValue> type with [string] keys and [int] values, are therefore equivalent:
[System.Collections.Generic.Dictionary[string, int]]
[System.Collections.Generic.Dictionary`2[string, int]]
Example Use of Script Block to Solve the Problem
Maybe I was doing something wrong, but for some reason this dynamically built string didn't seem to like any of the official answers here:
[System.Collections.Generic.HashSet[System.Action[string, object]]]::new()
I gave up and did the following, perhaps this will be useful to someone else:
. ([ScriptBlock]::Create(#"
param(`$MyClass)
`$MyClass | Add-Member -NotePropertyName '$HashSetName' -NotePropertyValue ([System.Collections.Generic.HashSet$EventDef]::new())
"#)) -MyClass $this
I have a powershell class that I need pass in a reference to a UInt64 Variable and return a value from the Method. I'm trying to do something like in the code below but it gives me a syntax error.
Maybe I don't need to add ?[ref]? because all variables are references by default in powershell including uint64? but i just wasn't sure if this is true.... If I follow normal C-language convention then the method gets a copy of the argument not the actual reference to the variable. I seem to recall something in C# about boxing and unboxing int types... boxing and unboxing c# int type How does it work for boxing Int for Powershell class methods?
class PowerHe11 {
# Constructor
PowerHe11() {
}
[void]AccessChannel(
[UInt64]$ichan,
[UInt64]$idata,
[UInt64]$isize,
[UInt64]$ireal,
[ref][UInt64]$otimeout,
[ref][UInt64]$odata,
[ref][UInt64]$ochan,
[ref][UInt64]$osize
) {
$osize = 64; #Return this value to caller of method
}
}
Error message is:
At C:\Users\wmoore\Documents\fpga\zynq_pl\run_ps1\Untitled1.ps1:13 char:11
+ [ref][UInt64]$otimeout,
+ ~~~~~~~~
Multiple type constraints are not allowed on a method parameter.
At C:\Users\wmoore\Documents\fpga\zynq_pl\run_ps1\Untitled1.ps1:14 char:14
+ [ref][UInt64]$odata,
+ ~~~~~~~~
Multiple type constraints are not allowed on a method parameter.
At C:\Users\wmoore\Documents\fpga\zynq_pl\run_ps1\Untitled1.ps1:15 char:14
+ [ref][UInt64]$ochan,
+ ~~~~~~~~
Multiple type constraints are not allowed on a method parameter.
At C:\Users\wmoore\Documents\fpga\zynq_pl\run_ps1\Untitled1.ps1:16 char:14
+ [ref][UInt64]$osize
+ ~~~~~~~~
Multiple type constraints are not allowed on a method parameter.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : MultipleTypeConstraintsOnMethodParam
Your own answer shows the solution; let me add an explanation and background information:
about_Ref is the official help topic.
The short of it:
[ref]'s primary purpose is to support ref and out parameters in .NET methods; e.g., to call the System.Int32.TryParse method, use the following:
[int] $int = 0; [int]::TryParse('42', [ref] $int)
Note that the variable's type must match the type expected by the .NET method, and that the only way to create a variable in PowerShell is to also assign it a value (even if that value is irrelevant, as is the case with the out parameter at hand); the [ref] cast is required (analogous to the need to use ref or out in C#.
You may pass [ref] $null if you're not interested in the value being assigned by the method.
While you can use it in PowerShell code, doing so is awkward, as your example shows:
PowerShell code that receives a [ref] argument must refer to its value via the .Value property, as your answer shows.
You cannot type-constrain such a parameter, so you lose type safety.
Note that in function and scripts - as opposed to custom-class methods - it is syntactically allowed, but pointless to use both [ref] and a target data type (such as [ref][UInt64]$otimeout in your example), because the latter is effectively ignored; e.g.:
function foo { param([ref] [int] $p) $p.Value += '!' }; $bar = 'none'; foo ([ref] $bar); $bar
The call succeeds and $bar contains 'none!', which implies that the [int] type constraint was ignored.
Calling scripts and functions requires argument-mode syntax (shell-like, whitespace-separated arguments, bareword strings allowed), making the [ref]-cast invocation more awkward by requiring (...) around the argument, as shown above (foo ([ref] $bar)).
With respect to use in custom PowerShell classes, note that PowerShell's class support is - by design - not on par with that of OO-focused languages such as C#; that said, enhancements are planned; unfortunately, even the limited feature set still has problem as of PowerShell 7.1 - see GitHub issue #6652.
Technical background on [ref]:
[ref], unlike ref in C#, is not a language keyword: it is a type, namely [System.Management.Automation.PSReference] (whose type accelerator is [ref]).
As such, an expression such as [ref] $var is a cast that implicitly constructs a [ref] instance, which is required to bind to a [ref]-typed parameter.
Caveats:
There is parser magic involved in a [ref] cast, because you cannot directly use the normally equivalent [ref]::new($var) constructor expression as an alternative to cast [ref] $var.
PowerShell needs magic to know that $var refers to a variable object itself rather than to the variable's value, as is normally the case in an expression - and the latter is indeed what happens with [ref]::new($var) - $var's value is wrapped in a [ref] instance.
The true constructor equivalent of [ref] $var is [ref]::new((Get-Variable var))
# BROKEN: Do not use a constructor - [ref]::new($int) -
# in lieu of a cast - [ref] $int
PS> [int] $int = 0; $null = [int]::TryParse('42', [ref]::new($int)); $int
0 # !! $int was NOT updated, because its *value* was wrapped in a [ref] instance
It generally only makes sense to use a [ref] cast with a variable (object) operand.
PowerShell lets you pass any value as the operand, which technically works, but makes no sense unless you explicitly save the [ref] instance in a variable, pass it as the argument, and then use the [ref] instance's .Value property to access the updated value:
# Save a [ref] instance in a variable, pass it, then use .Value to get the
# updated value.
PS> [ref] $intRef = 0; $null = [int]::TryParse('42', $intRef); $intRef.Value
42
class powerhe11 {
# Constructor
powerhe11() {
}
[void]AccessChannel(
[ref]$ichan
) {
$ichan.value = 0xbeef;
Write-Host("ichan: {0}" -f $ichan.value)
}
}
[UInt64]$dude = 0
$ser = [gvmuart]::new()
$ser.AccessChannel([ref]$dude);
Write-Host -BackgroundColor Green "DUDE: $dude"
Can I overload the PowerShell inbuilt class's methods? If yes then how? Any code sample would be great.
Essentially, I am trying to overload the Equals method of a Hashtable Dictionary PowerShell object to do appropriate comparisons.
I know that you can overload a cmdlet or a function in PowerShell and whatever is the latest definition that is taken up. But I am trying to overload not a cmdlet or a function, but trying to overload inbuilt method for existing class. I have already checked this post: Function overloading in PowerShell
Well, you can overload it from C# code in this manner:
$Source = #"
class MyHashTable : HashTable
{
public override Equals(...) { ... }
}
"#
Add-Type -TypeDefinition $Source -Language CSharp
$x = New-Object MyHashTable ...
I need to create a .NET object in PowerShell. The signature for the constructor is:
public ObjCTor(TClient param1, Func<TClient, IInterface> param2)
and the class is defined as:
public class ObjCTor<TClient> : IOtherInt1, IOtherInt2 where TClient : ConcreteBase
The first parameter is easy enough. I'm stumped on the second. How can I pass a PowerShell function as Func<T1, T2>?
UPDATED
PowerShell can convert a ScriptBlock to a delegate, so something like this might work fine:
$sb = { param($t1) $t1 }
New-Object "ObjCTor[CClient, Func[CClient,IInterface]]" -ArgumentList $param1,$sb
Note that PowerShell cannot deduce generic type arguments from a ScriptBlock. In your case, ObjCTor looks like a generic class so type deduction isn't a factor, but if you were calling a generic method, things could get messy.
The syntax supplied by Jason wasn't quiet correct. This works:
$= New-Object "ObjCTor[TClient]" -ArgumentList $param1,$functionToCall
I was also missing the $args[0] as a parameter in my scriptblock/delegate. This is what I should've had:
$functionToCall = { Get-Blah $args[0] }