Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create an about_Type_Conversions conceptual help topic that describes PowerShell's unusually flexible and automatic type conversions #10688

Open
2 tasks done
mklement0 opened this issue Nov 30, 2023 · 2 comments
Labels
area-about Area - About_ topics issue-doc-idea Issue - request for new content

Comments

@mklement0
Copy link
Contributor

mklement0 commented Nov 30, 2023

Prerequisites

  • Existing Issue: Search the existing issues for this repository. If there is an issue that fits your needs do not file a new one. Subscribe, react, or comment on that issue instead.
  • Descriptive Title: Write the title for this issue as a short synopsis. If possible, provide context. For example, "Document new Get-Foo cmdlet" instead of "New cmdlet."

PowerShell Version

5.1, 7.2, 7.3, 7.4

Summary

PowerShell is unusually flexible with respect to type conversions, both explicit and implicit ones; while the latter ones are usually helpful, there are pitfalls, especially for users coming from languages with stricter type handling.

It would be helpful to systematically describe both how explicit and especially implicit type conversions (coercions) work in PowerShell.

Below is what I have gleaned from my own experience and experiments.

Details

  • PowerShell is unusually flexible with respect to type conversions, both explicit and implicit ones; while the latter ones are usually helpful once understood, there are pitfalls, especially for users coming from languages with stricter type handling.

    • PowerShell not only supports many more explicit type conversions than C#, for instance, using casts, but also applies them automatically (implicitly) in the absence of casts, if the context requires it; specifically, these contexts are:

      • Binding (passing) a value to a cmdlet / function / script / .NET method parameter declared with a specific type (i.e. not declared without a type or, equivalently, as [object]; see further below).

      • A conceptually and technically closely related context is when assigning values to type-constrained variables (also see further below).

      • In the context of using operators (see next point).

      • In Boolean contexts, such as in if-statement conditionals, and PowerShell notably supports coercing any value to [bool] - see about_Booleans.

    • As for conversions PowerShell supports in addition to C#:

      • Numeric types can be freely converted to one another (assuming the target type is wide enough), e.g. [byte] 42.1 ([double] coerced to [byte] 42)

      • A value of any type can be coerced to:

        • [bool], i.e $true or $false, as noted; e.g. [bool] 42 is $true

        • [string], including arrays, with the exceptions during parameter-binding noted below; e.g. [string] @(1, 2, 3) is '1 2 3' by default (the separator - a space by default - can be controlled with the rarely used $OFS preference variable)

      • Single-character [string] instances can be converted to and from [char] (note that PowerShell has no [char] literals).

      • The values of System.Enum-derived types can be converted to and from [string] instances:

        • E.g., [System.PlatformId] 'Unix' is the same as [System.PlatformId]::Unix
        • Even flag-based enums are handled correctly via , separated values inside a string or even string arrays; e.g., both [System.Reflection.TypeAttributes] 'Public, Abstract' and [System.Reflection.TypeAttributes] ('Public', 'Abstract') are equivalent to [System.Reflection.TypeAttributes]::Public -bor [System.Reflection.TypeAttributes]::Abstract.
      • A single value (non-array) can be converted to an instance of a type if that type has a (public) single-parameter constructor of the same type (or a type that the value can be coerced to); e.g. [regex] 'a|b' is the same as [regex]::new('a|b')

      • A single string value can converted to an instance of a type if the type implements a static ::Parse() method; e.g., [bigint] '42' is the same as [bigint]::Parse('42', [cultureinfo]::InvariantCulture) - [cultureinfo]::InvariantCulture binds to an IFormatProvider-typed parameter - if available - and is used to ensure culture-invariant behavior.

      • Custom conversions can be defined:

      • For the complete story, consult the source code

  • PowerShell operators fundamentally do not guarantee that the result of an expression is of the same type as its operands.

    • 1 / 2 ([int] / [int] -> [double) and '10' - '9' ([string] - [string] -> [int]) are two examples.
    • See the next section for details.
  • PowerShell variables are by default not type-constrained; that is, you can create a variable with an instance of one type, and later assign values of any other type.

    • To type-constrain variables, place a "cast" (type literal) to the LEFT of the variable name in an assignment (e.g. [int] $foo = 42)
    • In addition to immediately converting an assigned value to the specified type, later assignments then enforce that a new value either already is of that type or can be converted to it; e.g, $foo = '43' works too, because PowerShell happily converts a string that can be parsed as an integer to one.
  • Number literals are implicitly typed by default (e.g. 42 is of type [int] and 1.2 is of type [double]), but can be explicitly typed with a type-specifier suffix (e.g., 42L or 42l are of type [long] aka [System.Int64]) - see about_Numeric_Literals.

  • Implicit type conversions also happen during parameter binding when calling PowerShell cmdlets, functions, and scripts, as well as .NET methods; e.g.:

    • & { param([Int] $Integer) $Integer } -Integer ' -10 ' - the [string] instance ' -10 'is automatically converted to [int] -10.
    • [datetime]::FromFileTime('0') is the same as [datetime]::FromFileTime(0), i.e. string '0' is converted to [int] 0
    • Caveats:
      • With .NET methods in particular, it is better to pass the exact type expected, if needed with a cast, as there can be ambiguity otherwise, with PowerShell potentially selecting the wrong overload from among the matching ones. In the worst-case scenario, the introduction of additional method overloads in future .NET versions can break existing code - see String method Split - bug with multiple split characters PowerShell/PowerShell#11720 (comment) for an example.
      • The automatic coercion of arrays to [string]-typed parameters - which is usually undesired - can be treacherous:
        • & { param([string] $String) $String } -String 1, 2 quietly accepts the array argument and stringifies it, i.e. passes the equivalent of "$(1, 2)", which is "1 2" by default.
        • You can avoid this obscure behavior by making your script or function an advanced one:
          • & { [CmdletBinding()] param([string] $String) $String } -String 1, 2 fails, because it refuses to bind an array to a (non-array) [string] parameter.
        • Unfortunately, you can not avoid this behavior in .NET method calls; the same behavior as in non-advanced function applies; e.g.:
          • (Get-Date).ToString(@(1, 2)) quietly passes "1 2" to the string-typed format parameter.

Type coercions (conversions) performed by PowerShell's operators:

  • In numeric operations, even if both operands are of the same numeric type, the result may be a different type, due to automatic, on-demand type-widening; namely:

    • Widening to [double] to support fractional results in integer division:

      • E.g. 3 / 2 , despite having two [int] operands, yields 1.5, i.e, a [double]
      • Conversely, to get true integer division, use [int] [Math]::Truncate(3 / 2) or [Math]::DivRem(3, 2)[0] ([Math]::DivRem(3, 2, [ref] $null) in Windows PowerShell).
    • Widening to [double] to support results that would result in overflow with the operand type.

      • E.g, [int]::MaxValue + 1 returns 2147483648 - as a [double] - instead of overflowing.
      • Note: It is unfortunate that [double] is invariably used when this widening occurs, even though the next larger integer type ([long] aka [System.Int64] in this case). would suffice and be preferable. Sadly, it only works that way in number literals, where, for instance, 2147483647 (the value of [int]::MaxValue) is an [int], where as 2147483648 (i.e. [int]::MaxValue + 1), implicitly becomes a [long]
  • In operations where an implicit type conversion is required in order for the operation to succeed:

    • Typically, it is the LHS operand of PowerShell operators that determines the data type used in the operation and converts (coerces) the RHS operand to the required type; e.g.:

      • 10 - ' +9' yields [int] 1, because the [string]-typed RHS was implicitly converted to [int]
      • 10 -eq ' +10 ' yields $true for the same reason, and the same goes even for 10 -eq '0xa'
    • There are exceptions, however:

      • Arithmetic operators (+, -, *, /) with non-numeric operands:

        • - and / convert both operands from strings to numbers on demand; e.g., ' 10' - '2' yields [int] 8;

        • By contrast, this does not happen with + and *, which have string-specific semantics (concatenation and replication).

        • Using [bool] values with arithmetic operators causes them to be coerced to [int], with $true becoming 1, and $false, 0

          • Curiously, as of v7.4.0 this doesn't work with * - see Allow multiplying bools PowerShell/PowerShell#20816
          • E.g., $false - $true is -1, because it is the equivalent of 0 - 1.
          • The implicit to-integer coercion can also be used as a shortcut in index expressions, such as in the following (imperfect) emulation of a ternary conditional: ('even', 'uneven')[1 -eq [datetime]::Now.Second % 2]; that is, the Boolean result of the index expression was coerced to 0 or 1 to select the array element of interest.
        • For other LHS types, arithmetic operators only succeed if a given type custom-defines these operators via operator overloading.

      • Polymorphic comparison operators such as -eq, -lt, -gt, ... (as distinct from string-only operators such as -match and -like):

        • For non-strings and non-primitive types, the behavior depends on whether the LHS type implements interfaces such as IEquatable and IComparable.

        • The collection-based comparison operators, namely -in and -contains (and their negated variants), perform per-element -eq comparisons until a match is found, and it is each individual element of the collection-valued operand that drives any coercion; e.g.:

          • $true -in 'true', 'false' and 'true', 'false' -contains $true are both$true, because 'true' -eq $true yields $true (the equivalent of 'true' -eq [string] $true)
          • '1/1/70' -in 'one', [datetime]::UnixEpoch, 'two' is $true, because [datetime]::UnixEpoch -eq '1/1/70' is $true (the equivalent of [datetime]::UnixEpoch -eq [datetime] '1/1/70')

Proposed Content Type

About Topic

Proposed Title

about_Type_Conversions

Related Articles

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Operators

@mklement0 mklement0 added issue-doc-idea Issue - request for new content needs-triage Waiting - Needs triage labels Nov 30, 2023
@sdwheeler sdwheeler added area-about Area - About_ topics and removed needs-triage Waiting - Needs triage labels Dec 1, 2023
@mikeclayton
Copy link

mikeclayton commented Dec 21, 2023

A couple of other cases where PowerShell's unrelenting attempts to convert function parameters into something that an overload supports give surprising results:

  • "source target destination".Split("target") - in PowerShell 5.1 there's no String.Split(String) overload, but because string implements IEnumerable<char>, PowerShell 5.1 binds to Split(Char[]) and invokes the equivalent of Split(@("t", "a", "r", "g", "e", "t")) which gives:
sou
c
         
    
    
    
    
d
s
in
    
ion

(PowerShell Core returns the "expected" result because the underlying dotnet core has additional overloads and so targets string[] Split(string separator, System.StringSplitOptions options = System.StringSplitOptions.None) instead.

See https://stackoverflow.com/questions/76241804/how-does-powershell-split-consecutive-strings-not-a-single-letter for this issue

  • $bytes = [byte[]] @(1..16 | % { 0x00 }); $guid = new-object System.Guid($bytes) - gives the error New-Object: Cannot find an overload for "Guid" and the argument count: "16". because it treats the $bytes array as a list of individual parameters even though $bytes is a Byte[] array and System.Guid has a Guid(Byte[]) constructor.

The workaround is to wrap $bytes in an outer array ($guid = new-object System.Guid(@(, $bytes))) so PowerShell looks for constructor with parameters that match the contents of the outer array - the first item of which is our original Byte[] instance and that matches the Guid(Byte[]] constructor.

See https://stackoverflow.com/questions/73223637/passing-an-array-parameter-value for this issue

@mklement0
Copy link
Contributor Author

Thanks, @mikeclayton.

  • The first case is actually covered in the initial post under Caveats, and the tl;dr is:
    For long-term stability:

    • Avoid .NET APIs in favor of PowerShell-native solutions.
    • Otherwise, be sure to use casts - if and as necessary - to unambiguously select the intended method overload of interest. Doing so prevents the code from breaking should future .NET versions introduce additional overloads that cause PowerShell's overload resolution algorithm to then choose a different overload.
  • The second case isn't so much a problem of type conversions, as it is a consequence of command [parsing] mode:

    • new-object System.Guid($bytes) is actually - a very common - instance of pseudo method syntax, which typically, but not always still works as intended, but is generally best avoided.

    • What it translates to is:

      [byte[]] $bytes = 1..16
      New-Object -TypeName System.Guid -ArgumentList $bytes # !! BROKEN
      
    • Given that -ArgumentList is [object[]]-typed, a single argument that happens to be an array - of any type - binds to it element by element. The problem usually goes away with at least two arguments - specified directly, with , - which creates a jagged array, each element of which unambiguously becomes an element of the -ArgumentList argument.

      • For an example of where even an ostensibly jagged array can break the invocation - due to mistakenly conceiving of the array as being formulated in expression [parsing] mode (as it would apply only to true method syntax) - see this Stack Overflow answer.
    • An alternative to your (, $bytes) workaround is to use the PSv5+ intrinsic, static ::new() method, where method syntax truly applies; it additionally avoids the - usually, but not always - benign [psobject] instance New-Object wraps its output object in (as any cmdlet does):

      [byte[]] $bytes = 1..16
      [System.Guid]::new($bytes)  # OK
      

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-about Area - About_ topics issue-doc-idea Issue - request for new content
Projects
None yet
Development

No branches or pull requests

3 participants