codewario
codewario

Reputation: 21418

Can't seem to use generic collection with a PowerShell class

I'm trying to invoke the List[T](IEnumerable) directly adding an item to the initial List like so, where T is a PowerShell class I've written (the below example uses the class name Thing:

$someObject = Get-Thing # returns a single object
$list = [List[Thing]]::new(@( $someObject ))

However, this yields an error suggesting it can't find the overload for this constructor:

Cannot find an overload for "List`1" and the argument count: "1".

Setting List[T] to the Object class works, however:

$someObject = Get-Thing
$list = [List[Object]]::new(@( $someObject ))

While this works, I'm unsure why I'm unable to use my PowerShell class as the type. My understanding is that only context-bound types and (by default) nested types are unable to be used with generics, but the following shows that my class is not a ContextBoundObject:

class Thing {
  $Name

  Thing($name) {
    $this.Name = $name
  }
}
$thing = [Thing]::new('Bender')
$thing -is [System.ContextBoundObject] # ==> False

I'm not certain if a PowerShell class would be a nested type of some sort, and about_Classes does not mention nested types.

Upvotes: 1

Views: 1432

Answers (2)

mklement0
mklement0

Reputation: 437688

To complement Mathias R. Jessen's helpful answer, which explains the problem well and offers an effective solution:

PowerShell's casts are not only syntactically more convenient than constructor calls, but also more flexible when it comes to on-demand type conversions.

Indeed, using a cast instead of calling a constructor, via the static ::new() method, does work:

using namespace System.Collections.Generic

class Thing { [string] $Name; Thing([string] $name) { $this.Name = $name } }

# Both of the following work:

# Single [Thing] instance.
$list = [List[Thing]] [Thing]::new('one')

# Multiple [Thing] instances, as an array, via the grouping operator, (...)
# @(...), the array subexpression operator, works too, but is unnecessary.
$list = [List[Thing]] ([Thing]::new('one'), [Thing]::new('two'))

PowerShell's automatic type conversions, as also used in casts:

Unfortunately, as of this writing the rules aren't documented, but a comment in the source-code provides a high-level overview, as does the (pretty low-level) ETS type converters documentation, which can be summarized as follows, in descending order of precedence:

  • First, engine-internal, fixed conversion rules may be applied (see source-code link above).

    • A notable internal rule concerns to-string conversions: while any .NET type supports it by an explicit call to its .ToString() method (inherited from the root of the object hierarchy, System.Object), PowerShell applies custom rules:

      • If a type has a culture-sensitive .ToString(<IFormatProvider>) overload, PowerShell passes the invariant culture deliberately, to achieve a culture-invariant representation, whereas a direct .ToString() call would yield a culture-sensitive representation - see this answer for details; e.g., in a culture where , is the decimal mark, [string] 1.2 returns '1.2' (period), whereas (1.2).ToString() returns '1,2' (comma).

      • Collections, including arrays, are stringified by concatenating their (stringified) elements with a space as the separator (by default, can be overridden with preference variable $OFS); e.g., [string] (1, 2) returns 1 2, whereas (1, 2).ToString() returns merely System.Object[].

    • Also, PowerShell converts freely:

      • between different number types (when possible).
      • between numbers and strings (in a culture-invariant manner, recognizing only . as the decimal mark when converting from a string).
      • and allows any data type to be converted to (interpreted as) as Boolean - see the bottom section of this answer for the rules.
      • from a scalar to an array / list type (e.g., [int[]] 42 creates a single-element array, and [System.Collections.Generic.List[int]] 42 creates a single-element list; small caveat: there are bugs with certain list types - see this answer).
  • Next, TypeConverter or (PSTypeConverter) classes that implement custom conversions for specific types are considered.

  • If the input type is a string ([string]), a static ::Parse() method is considered, if present: first, one with a culture-sensitive signature, ::Parse(<string>, <IFormatProvider>), in which case the invariant culture is passed, and, otherwise one with signature ::Parse(<string>).

  • Next, a single-argument constructor is considered, if the input type matches the argument's type or is convertible to it.

  • If an implicit or explicit conversion operator exists for conversion between the input and the target type.

  • Finally, if the input object implements the System.IConvertible interface and the target type is a supported-by-the-implementation primitive .NET type except [IntPtr] and [UIntPtr] or one of the following types: [datetime], [DBNull], [decimal].

Upvotes: 3

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174485

I'm unsure why I'm unable to use my PowerShell class as the type

The array subexpression operator @() returns its results as [object[]] - a type which satisfies the argument type [IEnumerable[object]] - which is why it always works when you use [object] as the type parameter for the receiving collection type.


So, what to do about that?

If the array consists only of [Thing]'s, you can explicitly cast to a more specific collection type that implements [IEnumerable[Thing]]:

$list = [List[Thing]]::new([Thing[]]@( $someObject ))

Upvotes: 3

Related Questions