Search code examples
powershellpropertieshashtable

Can someone explain the difference between Add_member and $hashTable.Something for a hashtable?


Lets say I have a hashtable:

$HashTable = @{}

Now, when I want to add something to the Hashtable I usually do it like this:

$HashTable.Something = 'Something'

When I test $hashTable it will show this output:

$HashTable

Name                           Value                                                                                                                                     
----                           -----                                                                                                                                     
Something                      Something 

A Colleague of mine used Add-Member in stead:

$HashTable | Add-Member -NotePropertyName 'SomethingAddMember' -NotePropertyValue 'SomethingAddMember'

It works, but when you look at $HashTable it will still only show this:

$HashTable

Name                           Value                                                                                                                                     
----                           -----                                                                                                                                     
Something                      Something

When I run this $HashTable.SomethingAddMember it will show up:

$HashTable.SomethingAddMember

Why is it not showing up in the HashTable itself? When I convert it to Json it's there again! How confusing can this be, or am I missing something??

$HashTable | ConvertTo-Json
{
    "Something":  "Something",
    "SomethingAddMember":  "SomethingAddMember"
}

Is it because Add-Member adds a new property to the HashTable instead of the Keys with Values? How can this happen like this?


Solution

  • There's good information here already, but let me break it down conceptually:

    The entries of a hashtable (key-value pairs) are distinct from its .NET type's members (properties, methods, events).

    Hashtable entries are data, accessible via the System.Collections.Hashtable type's members, notably the .Keys and .Values properties and the parameterized .Item property, which PowerShell (like C#) surfaces syntactically via index syntax:

    • That is, you can get an entry's value by using its key inside [...], as syntactic sugar for .Item(...); e.g.,
      @{ foo = 42 }['foo'] is a shortcut to @{ foo = 42 }.Item('foo')

    Additionally, PowerShell allows you to access entry values with dot notation, using ., the member-access operator - even though what you're accessing isn't a member, but a hashtable entry; e.g.
    @{ foo = 42 }.foo is equivalent to @{ foo = 42 }['foo'].

    Note that the inverse is not true: You can not use index notation to access an object's true properties: e.g., ([pscustomobject] @{ foo = 42 })['foo'] does not work as intended: by default, it quietly evaluates to $null, and with Set-StrictMode -Version 2 or higher in effect it causes an error - see this post.

    In other words:

    • PowerShell's . (dot notation) can access both:

    • If there is a name conflict, the entry access takes precedence - e.g, @{ Count = 42 }.Count returns 42 rather than the value of the type-native Count property reflecting the number of entries; to access the latter, prefix the property name with get_ and invoke that as a method: @{ Count = 42 }.get_Count() returns 1, the entry count.

    Note: The members that Add-Member adds to an object aren't technically .NET type members (type-native properties and methods), but in PowerShell they act as such. (Such members, which are a feature of PowerShell's ETS (Extended Type System), are known to PowerShell only, and are of necessity maintained separately from the object they're associated with.)

    In other words: Your Add-Member call added a new property (NoteProperty) rather than an entry to the hashtable instance, alongside the instance's type-native properties such as .Count, .Keys, and .Values.

    With respect to surfacing entries vs. members:

    • Since a hashtable's default output formatting show its entries only, any (ETS) members you've added won't show in the output - you'd have to access the .SomethingAddMember property explicitly; you can also see it via reflection, using Get-Member ($Hashtable | Get-Member SomethingAddMember)

    • While ETS properties generally rarely surface, they do in serialization contexts, such as when using ConvertTo-Json, and in the case of hashtables (dictionaries) you get a mix of ETS properties and entries, which is what you saw:

      • In many contexts, PowerShell treat hashtables (dictionaries) as if they were regular objects, where the hashtable's entries - rather than the hashtable object's true type-native properties - are used in lieu of properties.

      • However, ETS properties are still included,[2] resulting in the aforementioned mix.


    [1] Strangely, dot notation does not work if the key type happens to be [object], though that isn't typical, given that the very point of a generic type/interface is to use specific types - see GitHub issue #15997.

    [2] Since PowerShell 7.2, ConvertTo-Json no longer serializes ETS properties for [datetime] and [string] instances (see GitHub PR #15665), but other serialization cmdlets such as ConvertTo-Csv still do.
    On a related note, as of PowerShell 7.2, when ConvertTo-Json does include ETS properties, it accidentally uses the type-native property's value if the ETS property happens to override the type-native one - see GitHub issue #13998.