Все, что вы хотели знать о хеш-таблицах


Я хочу сделать шаг назад и поговорить о хеш-таблицах. Теперь я использую их постоянно. Я рассказывал о них кому-то вчера вечером после встречи нашей группы пользователей и понял, что у меня такое же замешательство по поводу них, как и у него. Хэш-таблицы очень важны в PowerShell, поэтому полезно иметь четкое представление о них.

Примечание

Оригинальная версия этой статьи появилась в блоге @KevinMarquette. Команда PowerShell благодарит Кевина за то, что он поделился с нами этим контентом. Пожалуйста, посетите его блог на PowerShellExplained.com.

Хэш-таблица как коллекция вещей

Я хочу, чтобы вы сначала увидели Хеш-таблицу как коллекцию в традиционном определении хеш-таблицы. Это определение дает вам фундаментальное понимание того, как они работают, когда позже привыкнут к более сложным вещам. Игнорирование этого понимания часто является источником путаницы.

Что такое массив?

Прежде чем я перейду к тому, что такое Hashtable, мне нужно сначала упомянуть массивы. Для целей данного обсуждения массив — это список или коллекция значений или объектов.

$array = @(1,2,3,5,7,11)

Поместив элементы в массив, вы можете либо использовать foreach для перебора списка, либо использовать индекс для доступа к отдельным элементам массива.

foreach($item in $array)
{
    Write-Output $item
}

Write-Output $array[3]

Таким же образом вы также можете обновлять значения с помощью индекса.

$array[2] = 13

Я только что коснулся поверхности массивов, но это должно поместить их в правильный контекст при переходе к хеш-таблицам.

Что такое хеш-таблица?

Я собираюсь начать с базового технического описания того, что такое хеш-таблицы в общем смысле, прежде чем перейду к другим способам их использования в PowerShell.

Хэш-таблица — это структура данных, очень похожая на массив, за исключением того, что каждое значение (объект) сохраняется с использованием ключа. Это базовое хранилище ключей/значений. Сначала мы создаем пустую хеш-таблицу.

$ageList = @{}

Обратите внимание, что для определения хеш-таблицы вместо круглых скобок используются фигурные скобки. Затем мы добавляем элемент, используя такой ключ:

$key = 'Kevin'
$value = 36
$ageList.add( $key, $value )

$ageList.add( 'Alex', 9 )

Имя человека — это ключ, а его возраст — это значение, которое я хочу сохранить.

Использование скобок для доступа

Добавив значения в хеш-таблицу, вы можете извлечь их обратно, используя тот же ключ (вместо использования числового индекса, как для массива).

$ageList['Kevin']
$ageList['Alex']

Когда мне нужен возраст Кевина, я использую его имя, чтобы получить к нему доступ. Мы также можем использовать этот подход для добавления или обновления значений в хеш-таблицу. Это похоже на использование функции add() выше.

$ageList = @{}

$key = 'Kevin'
$value = 36
$ageList[$key] = $value

$ageList['Alex'] = 9

Есть еще один синтаксис, который вы можете использовать для доступа и обновления значений, о котором я расскажу в следующем разделе. Если вы переходите на PowerShell с другого языка, эти примеры должны соответствовать тому, как вы, возможно, использовали хеш-таблицы раньше.

Создание хеш-таблиц со значениями

На данный момент я создал пустую хеш-таблицу для этих примеров. Вы можете предварительно заполнить ключи и значения при их создании.

$ageList = @{
    Kevin = 36
    Alex  = 9
}

В качестве таблицы поиска

Реальная ценность этого типа хеш-таблиц заключается в том, что вы можете использовать их в качестве таблицы поиска. Вот простой пример.

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

$server = $environments[$env]

В этом примере вы указываете среду для переменной $env, и она выбирает правильный сервер. Вы можете использовать switch($env){... для такого выбора, но хеш-таблица — хороший вариант.

Это становится еще лучше, если вы динамически создаете таблицу поиска, чтобы использовать ее позже. Так что подумайте об использовании этого подхода, когда вам нужно что-то перекрестно ссылаться. Я думаю, мы бы увидели это еще больше, если бы PowerShell не был так хорош в фильтрации в канале с помощью Where-Object. Если вы когда-нибудь оказываетесь в ситуации, когда производительность имеет значение, необходимо рассмотреть этот подход.

Я не скажу, что это быстрее, но оно соответствует правилу: если производительность имеет значение, протестируйте ее.

Множественный выбор

Обычно вы представляете хеш-таблицу как пару ключ/значение, где вы предоставляете один ключ и получаете одно значение. PowerShell позволяет вам предоставлять массив ключей для получения нескольких значений.

$environments[@('QA','DEV')]
$environments[('QA','DEV')]
$environments['QA','DEV']

В этом примере я использую ту же хеш-таблицу поиска, что и выше, и предоставляю три разных стиля массива для получения совпадений. Это скрытая жемчужина PowerShell, о которой большинство людей не знают.

Итерация хеш-таблиц

Поскольку хеш-таблица представляет собой набор пар ключ/значение, вы выполняете итерацию по ней иначе, чем для массива или обычного списка элементов.

Первое, на что следует обратить внимание, это то, что если вы передаете хеш-таблицу по конвейеру, канал обрабатывает ее как один объект.

PS> $ageList | Measure-Object
count : 1

Несмотря на то, что свойство .count сообщает вам, сколько значений оно содержит.

PS> $ageList.count
2

Эту проблему можно обойти, используя свойство .values, если вам нужны только значения.

PS> $ageList.values | Measure-Object -Average
Count   : 2
Average : 22.5

Часто бывает более полезно перечислить ключи и использовать их для доступа к значениям.

PS> $ageList.keys | ForEach-Object{
    $message = '{0} is {1} years old!' -f $_, $ageList[$_]
    Write-Output $message
}
Kevin is 36 years old
Alex is 9 years old

Вот тот же пример с циклом foreach(){....

foreach($key in $ageList.keys)
{
    $message = '{0} is {1} years old' -f $key, $ageList[$key]
    Write-Output $message
}

Мы просматриваем каждый ключ в хеш-таблице, а затем используем его для доступа к значению. Это распространенный шаблон при работе с хеш-таблицами как с коллекцией.

GetEnumerator()

Это подводит нас к GetEnumerator() для перебора нашей хеш-таблицы.

$ageList.GetEnumerator() | ForEach-Object{
    $message = '{0} is {1} years old!' -f $_.key, $_.value
    Write-Output $message
}

Перечислитель выдает вам каждую пару ключ/значение одну за другой. Он был разработан специально для этого случая использования. Спасибо Марку Краусу за напоминание мне об этом.

BadEnumeration

Одна важная деталь заключается в том, что вы не можете изменять хеш-таблицу во время ее перечисления. Если мы начнем с нашего базового примера $environments:

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

И попытка установить для каждого ключа одно и то же значение сервера не удалась.

$environments.Keys | ForEach-Object {
    $environments[$_] = 'SrvDev03'
}

An error occurred while enumerating through a collection: Collection was modified;
enumeration operation may not execute.
+ CategoryInfo          : InvalidOperation: tableEnumerator:HashtableEnumerator) [],
 RuntimeException
+ FullyQualifiedErrorId : BadEnumeration

Это также потерпит неудачу, хотя похоже, что все должно быть в порядке:

foreach($key in $environments.keys) {
    $environments[$key] = 'SrvDev03'
}

Collection was modified; enumeration operation may not execute.
    + CategoryInfo          : OperationStopped: (:) [], InvalidOperationException
    + FullyQualifiedErrorId : System.InvalidOperationException

Хитрость в этой ситуации заключается в клонировании ключей перед перечислением.

$environments.Keys.Clone() | ForEach-Object {
    $environments[$_] = 'SrvDev03'
}

Хэш-таблица как набор свойств

До сих пор все объекты, которые мы помещали в нашу хеш-таблицу, были объектами одного и того же типа. Во всех этих примерах я использовал возраст, а ключом было имя человека. Это отличный способ взглянуть на это, когда каждый из вашей коллекции объектов имеет имя. Другой распространенный способ использования хеш-таблиц в PowerShell — хранение коллекции свойств, где ключом является имя свойства. Я рассмотрю эту идею в следующем примере.

Доступ на основе свойств

Использование доступа на основе свойств меняет динамику хеш-таблиц и способы их использования в PowerShell. Вот наш обычный пример выше, где ключи рассматриваются как свойства.

$ageList = @{}
$ageList.Kevin = 35
$ageList.Alex = 9

Как и в приведенных выше примерах, в этом примере эти ключи добавляются, если они еще не существуют в хеш-таблице. В зависимости от того, как вы определили свои ключи и каковы ваши значения, это либо немного странно, либо идеально подходит. До этого момента пример со списком возрастов отлично работал. Нам нужен новый пример, чтобы это было правильно в дальнейшем.

$person = @{
    name = 'Kevin'
    age  = 36
}

И мы можем добавлять атрибуты и получать к ним доступ в $person вот так.

$person.city = 'Austin'
$person.state = 'TX'

Внезапно эта хеш-таблица начинает ощущаться и действовать как объект. Это по-прежнему набор вещей, поэтому все приведенные выше примеры по-прежнему применимы. Просто мы подходим к этому с другой точки зрения.

Проверка ключей и значений

В большинстве случаев вы можете просто проверить значение примерно так:

if( $person.age ){...}

Это просто, но стало для меня источником многих ошибок, потому что я упустил одну важную деталь в своей логике. Я начал использовать его, чтобы проверить наличие ключа. Если значение было $false или ноль, этот оператор неожиданно возвращал $false.

if( $person.age -ne $null ){...}

Это решает эту проблему для нулевых значений, но не для $null и несуществующих ключей. В большинстве случаев вам не нужно проводить это различие, но для этого есть функции.

if( $person.ContainsKey('age') ){...}

У нас также есть ContainsValue() для ситуации, когда вам нужно проверить значение, не зная ключа или перебирая всю коллекцию.

Удаление и очистка ключей

Вы можете удалить ключи с помощью функции .Remove().

$person.remove('age')

Присвоение им значения $null просто оставляет вам ключ со значением $null.

Распространенный способ очистить хеш-таблицу — просто инициализировать ее пустой хеш-таблицей.

$person = @{}

Хотя это работает, попробуйте вместо этого использовать функцию clear().

$person.clear()

Это один из тех случаев, когда использование функции создает самодокументируемый код и делает содержание кода очень понятным.

Все самое интересное

Заказанные хеш-таблицы

По умолчанию хеш-таблицы не упорядочиваются (или не сортируются). В традиционном контексте порядок не имеет значения, если вы всегда используете ключ для доступа к значениям. Вы можете обнаружить, что хотите, чтобы свойства оставались в том порядке, в котором вы их определили. К счастью, есть способ сделать это с помощью ключевого слова ordered.

$person = [ordered]@{
    name = 'Kevin'
    age  = 36
}

Теперь, когда вы перечисляете ключи и значения, они остаются в этом порядке.

Встроенные хеш-таблицы

Когда вы определяете хеш-таблицу в одной строке, вы можете разделить пары ключ/значение точкой с запятой.

$person = @{ name = 'kevin'; age = 36; }

Это пригодится, если вы создаете их в канале.

Пользовательские выражения в общих командах конвейера

Существует несколько командлетов, которые поддерживают использование хеш-таблиц для создания пользовательских или вычисляемых свойств. Обычно это можно увидеть с помощью Select-Object и Format-Table. Хэш-таблицы имеют специальный синтаксис, который в полностью развернутом виде выглядит следующим образом.

$property = @{
    name = 'totalSpaceGB'
    expression = { ($_.used + $_.free) / 1GB }
}

имя — это то, как командлет пометит этот столбец. выражение — это блок сценария, который выполняется, где $_ — это значение объекта в канале. Вот этот скрипт в действии:

$drives = Get-PSDrive | Where Used
$drives | Select-Object -Property name, $property

Name     totalSpaceGB
----     ------------
C    238.472652435303

Я поместил это в переменную, но ее можно легко определить в строке, и вы можете сократить name до n и expression до e пока вы этим занимаетесь.

$drives | Select-Object -property name, @{n='totalSpaceGB';e={($_.used + $_.free) / 1GB}}

Лично мне не нравится, как долго это делает команды, и это часто способствует плохому поведению, на которое я не буду впадать. Я с большей вероятностью создам новую хеш-таблицу или pscustomobject со всеми нужными мне полями и свойствами вместо того, чтобы использовать этот подход в сценариях. Но существует множество кода, который делает это, поэтому я хотел, чтобы вы об этом знали. О создании pscustomobject я расскажу позже.

Пользовательское выражение сортировки

Коллекцию легко отсортировать, если объекты содержат данные, которые вы хотите отсортировать. Вы можете добавить данные в объект перед его сортировкой или создать собственное выражение для Sort-Object.

Get-ADUser | Sort-Object -Property @{ e={ Get-TotalSales $_.Name } }

В этом примере я беру список пользователей и использую специальный командлет для получения дополнительной информации только для сортировки.

Сортировка списка хэш-таблиц

Если у вас есть список хеш-таблиц, которые вы хотите отсортировать, вы обнаружите, что Sort-Object не рассматривает ваши ключи как свойства. Мы можем обойти это, используя собственное выражение сортировки.

$data = @(
    @{name='a'}
    @{name='c'}
    @{name='e'}
    @{name='f'}
    @{name='d'}
    @{name='b'}
)

$data | Sort-Object -Property @{e={$_.name}}

Распределение хеш-таблиц в командлетах

Это одна из моих любимых особенностей хеш-таблиц, которую многие люди не замечают на раннем этапе. Идея состоит в том, что вместо предоставления всех свойств командлету в одной строке вы можете сначала упаковать их в хеш-таблицу. Затем вы можете передать хэш-таблицу функции особым образом. Ниже приведен пример создания области DHCP обычным способом.

Add-DhcpServerV4Scope -Name 'TestNetwork' -StartRange '10.0.0.2' -EndRange '10.0.0.254' -SubnetMask '255.255.255.0' -Description 'Network for testlab A' -LeaseDuration (New-TimeSpan -Days 8) -Type "Both"

Без использования сплаттинга все эти вещи необходимо определить в одной строке. Он либо прокручивается за пределы экрана, либо переносится туда, где ему хочется. Теперь сравните это с командой, использующей сплаттинг.

$DHCPScope = @{
    Name          = 'TestNetwork'
    StartRange    = '10.0.0.2'
    EndRange      = '10.0.0.254'
    SubnetMask    = '255.255.255.0'
    Description   = 'Network for testlab A'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type          = "Both"
}
Add-DhcpServerV4Scope @DHCPScope

Использование знака @ вместо $ вызывает операцию splat.

Просто найдите минутку и оцените, насколько легко читать этот пример. Это одна и та же команда со всеми одинаковыми значениями. Второй легче понять и поддерживать в дальнейшем.

Я использую сплаттинг каждый раз, когда команда становится слишком длинной. Я определяю слишком долго как заставляющее мое окно прокручиваться вправо. Если я нажму три свойства функции, скорее всего, я перепишу ее, используя хеш-таблицу с разметкой.

Сплаттинг для необязательных параметров

Один из наиболее распространенных способов использования сплаттинга — это работа с необязательными параметрами, которые поступают из другого места моего сценария. Допустим, у меня есть функция, которая оборачивает вызов Get-CIMInstance, имеющий необязательный аргумент $Credential.

$CIMParams = @{
    ClassName = 'Win32_Bios'
    ComputerName = $ComputerName
}

if($Credential)
{
    $CIMParams.Credential = $Credential
}

Get-CIMInstance @CIMParams

Я начинаю с создания хеш-таблицы с общими параметрами. Затем я добавляю $Credential, если он существует. Поскольку здесь я использую сплаттинг, мне нужно лишь один раз вызвать Get-CIMInstance в моем коде. Этот шаблон проектирования очень чистый и легко обрабатывает множество дополнительных параметров.

Честно говоря, вы могли бы написать свои команды, чтобы разрешить значения $null для параметров. Вы просто не всегда можете контролировать другие команды, которые вызываете.

Несколько знаков

Вы можете объединить несколько хеш-таблиц в один и тот же командлет. Если мы вернемся к нашему исходному примеру со знаками:

$Common = @{
    SubnetMask  = '255.255.255.0'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type = "Both"
}

$DHCPScope = @{
    Name        = 'TestNetwork'
    StartRange  = '10.0.0.2'
    EndRange    = '10.0.0.254'
    Description = 'Network for testlab A'
}

Add-DhcpServerv4Scope @DHCPScope @Common

Я буду использовать этот метод, когда у меня есть общий набор параметров, которые я передаю множеству команд.

Сплаттинг для чистого кода

Нет ничего плохого в выделении одного параметра, если это сделает ваш код чище.

$log = @{Path = '.\logfile.log'}
Add-Content "logging this command" @log

Сплаттинг исполняемых файлов

Splatting также работает с некоторыми исполняемыми файлами, использующими синтаксис /param:value. Например, Robocopy.exe имеет такие параметры.

$robo = @{R=1;W=1;MT=8}
robocopy source destination @robo

Не знаю, насколько это все полезно, но мне показалось интересным.

Добавление хеш-таблиц

Хэш-таблицы поддерживают оператор сложения для объединения двух хеш-таблиц.

$person += @{Zip = '78701'}

Это работает только в том случае, если две хеш-таблицы не имеют общего ключа.

Вложенные хеш-таблицы

Мы можем использовать хеш-таблицы в качестве значений внутри хеш-таблицы.

$person = @{
    name = 'Kevin'
    age  = 36
}
$person.location = @{}
$person.location.city = 'Austin'
$person.location.state = 'TX'

Я начал с базовой хеш-таблицы, содержащей два ключа. Я добавил ключ под названием location с пустой хэш-таблицей. Затем я добавил два последних элемента в эту хеш-таблицу location. Мы также можем сделать все это онлайн.

$person = @{
    name = 'Kevin'
    age  = 36
    location = @{
        city  = 'Austin'
        state = 'TX'
    }
}

При этом создается та же хеш-таблица, которую мы видели выше, и доступ к свойствам можно получить таким же образом.

$person.location.city
Austin

Есть много способов подойти к структуре ваших объектов. Вот второй способ взглянуть на вложенную хеш-таблицу.

$people = @{
    Kevin = @{
        age  = 36
        city = 'Austin'
    }
    Alex = @{
        age  = 9
        city = 'Austin'
    }
}

Это смешивает концепцию использования хэш-таблиц как коллекции объектов и коллекции свойств. К значениям по-прежнему легко получить доступ, даже если они вложены с использованием любого подхода, который вы предпочитаете.

PS> $people.kevin.age
36
PS> $people.kevin['city']
Austin
PS> $people['Alex'].age
9
PS> $people['Alex']['City']
Austin

Я склонен использовать свойство точки, когда рассматриваю его как свойство. Обычно это вещи, которые я определил статически в своем коде и знаю их наизусть. Если мне нужно пройти по списку или программно получить доступ к ключам, я использую скобки, чтобы указать имя ключа.

foreach($name in $people.keys)
{
    $person = $people[$name]
    '{0}, age {1}, is in {2}' -f $name, $person.age, $person.city
}

Возможность вкладывать хеш-таблицы дает вам большую гибкость и возможности.

Просмотр вложенных хеш-таблиц

Как только вы начнете вкладывать хеш-таблицы, вам понадобится простой способ просмотра их с консоли. Если я возьму последнюю хеш-таблицу, я получу вывод, который выглядит вот так, и он очень глубокий:

PS> $people
Name                           Value
----                           -----
Kevin                          {age, city}
Alex                           {age, city}

Для просмотра этих вещей я использую команду ConvertTo-JSON, потому что она очень понятна, и я часто использую JSON для других целей.

PS> $people | ConvertTo-Json
{
    "Kevin":  {
                "age":  36,
                "city":  "Austin"
            },
    "Alex":  {
                "age":  9,
                "city":  "Austin"
            }
}

Даже если вы не знаете JSON, вы сможете увидеть то, что ищете. Для подобных структурированных данных существует команда Format-Custom, но мне все же больше нравится представление JSON.

Создание объектов

Иногда вам просто нужно иметь объект, и использование хеш-таблицы для хранения свойств просто не дает результата. Чаще всего вы хотите видеть ключи как имена столбцов. pscustomobject упрощает эту задачу.

$person = [pscustomobject]@{
    name = 'Kevin'
    age  = 36
}

$person

name  age
----  ---
Kevin  36

Даже если вы изначально не создаете его как pscustomobject, вы всегда можете привести его позже, когда это необходимо.

$person = @{
    name = 'Kevin'
    age  = 36
}

[pscustomobject]$person

name  age
----  ---
Kevin  36

У меня уже есть подробное описание pscustomobject, которое вам следует прочитать после этого. Он основан на многих вещах, изученных здесь.

Чтение и запись хеш-таблиц в файл

Сохранение в CSV

Одной из трудностей, о которых я говорил выше, является попытка получить хеш-таблицу для сохранения в формате CSV. Преобразуйте вашу хеш-таблицу в pscustomobject, и она будет правильно сохранена в CSV. Будет полезно, если вы начнете с pscustomobject, чтобы порядок столбцов сохранялся. Но при необходимости вы можете преобразовать его во встроенный pscustomobject.

$person | ForEach-Object{ [pscustomobject]$_ } | Export-CSV -Path $path

Опять же, ознакомьтесь с моей статьей об использовании pscustomobject.

Сохранение вложенной хеш-таблицы в файл

Если мне нужно сохранить вложенную хеш-таблицу в файл, а затем снова прочитать ее, я использую для этого командлеты JSON.

$people | ConvertTo-JSON | Set-Content -Path $path
$people = Get-Content -Path $path -Raw | ConvertFrom-JSON

В этом методе есть два важных момента. Во-первых, JSON записывается многострочно, поэтому мне нужно использовать опцию -Raw, чтобы прочитать его обратно в одну строку. Во-вторых, импортированный объект больше не является [хэш-таблицей]. Теперь это [pscustomobject], и это может вызвать проблемы, если вы этого не ожидаете.

Следите за глубоко вложенными хеш-таблицами. Преобразовав его в JSON, вы можете не получить ожидаемых результатов.

@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json

{
  "a": {
    "b": {
      "c": "System.Collections.Hashtable"
    }
  }
}

Используйте параметр Depth, чтобы убедиться, что вы расширили все вложенные хеш-таблицы.

@{ a = @{ b = @{ c = @{ d = "e" }}}} | ConvertTo-Json -Depth 3

{
  "a": {
    "b": {
      "c": {
        "d": "e"
      }
    }
  }
}

Если вам нужно, чтобы при импорте это была [hashtable], вам нужно использовать команды Export-CliXml и Import-CliXml.

Преобразование JSON в хэш-таблицу

Если вам нужно преобразовать JSON в [hashtable], я знаю один способ сделать это с помощью JavaScriptSerializer в .NET.

[Reflection.Assembly]::LoadWithPartialName("System.Web.Script.Serialization")
$JSSerializer = [System.Web.Script.Serialization.JavaScriptSerializer]::new()
$JSSerializer.Deserialize($json,'Hashtable')

Начиная с PowerShell v6, поддержка JSON использует NewtonSoft JSON.NET и добавляет поддержку хеш-таблиц.

'{ "a": "b" }' | ConvertFrom-Json -AsHashtable

Name      Value
----      -----
a         b

В PowerShell 6.2 добавлен параметр Depth в ConvertFrom-Json. Значение Глубина по умолчанию — 1024.

Чтение напрямую из файла

Если у вас есть файл, содержащий хеш-таблицу с использованием синтаксиса PowerShell, есть способ импортировать ее напрямую.

$content = Get-Content -Path $Path -Raw -ErrorAction Stop
$scriptBlock = [scriptblock]::Create( $content )
$scriptBlock.CheckRestrictedLanguage( $allowedCommands, $allowedVariables, $true )
$hashtable = ( & $scriptBlock )

Он импортирует содержимое файла в scriptblock, а затем проверяет, нет ли в нем других команд PowerShell, прежде чем выполнить его.

В связи с этим знаете ли вы, что манифест модуля (файл psd1) — это всего лишь хеш-таблица?

Ключами может быть любой объект

В большинстве случаев ключи представляют собой просто строки. Таким образом, мы можем заключить в кавычки что угодно и сделать это ключом.

$person = @{
    'full name' = 'Kevin Marquette'
    '#' = 3978
}
$person['full name']

Вы можете делать некоторые странные вещи, о которых вы, возможно, даже не подозревали.

$person.'full name'

$key = 'full name'
$person.$key

Если вы можете что-то сделать, это не значит, что вы должны это делать. Последнее выглядит просто как ошибка, ожидающая своего появления, и ее легко может неправильно понять любой, кто читает ваш код.

Технически ваш ключ не обязательно должен быть строкой, но о нем легче думать, если вы используете только строки. Однако индексирование не работает со сложными ключами.

$ht = @{ @(1,2,3) = "a" }
$ht

Name                           Value
----                           -----
{1, 2, 3}                      a

Доступ к значению в хеш-таблице по его ключу не всегда работает. Например:

$key = $ht.keys[0]
$ht.$($key)
a
$ht[$key]
a

Если ключ представляет собой массив, необходимо обернуть переменную $key в подвыражение, чтобы ее можно было использовать с нотацией доступа к членам (.). Или вы можете использовать нотацию индекса массива ([]).

Использование в автоматических переменных

$PSBoundParameters

$PSBoundParameters — это автоматическая переменная, которая существует только внутри контекста функции. Он содержит все параметры, с которыми была вызвана функция. Это не совсем хеш-таблица, но достаточно близкая к ней, чтобы вы могли относиться к ней как к таковой.

Это включает в себя удаление клавиш и распределение их по другим функциям. Если вы пишете прокси-функции, обратите на это внимание.

Дополнительные сведения см. в разделе about_Automatic_Variables.

PSBoundParameters попался

Важно помнить, что сюда входят только те значения, которые передаются в качестве параметров. Если у вас также есть параметры со значениями по умолчанию, но они не передаются вызывающей стороной, $PSBoundParameters не содержит этих значений. Это обычно упускается из виду.

$PSDefaultParameterValues

Эта автоматическая переменная позволяет назначать значения по умолчанию любому командлету без изменения самого командлета. Взгляните на этот пример.

$PSDefaultParameterValues["Out-File:Encoding"] = "UTF8"

При этом в хеш-таблицу $PSDefaultParameterValues добавляется запись, которая устанавливает UTF8 в качестве значения по умолчанию для параметра Out-File -Encoding. Это зависит от сеанса, поэтому вам следует поместить его в свой $profile.

Я часто использую это для предварительного назначения значений, которые я печатаю довольно часто.

$PSDefaultParameterValues[ "Connect-VIServer:Server" ] = 'VCENTER01.contoso.local'

Здесь также принимаются подстановочные знаки, поэтому вы можете задавать значения массово. Вот несколько способов использовать это:

$PSDefaultParameterValues[ "Get-*:Verbose" ] = $true
$PSDefaultParameterValues[ "*:Credential" ] = Get-Credential

Более подробную информацию можно найти в замечательной статье Майкла Соренса «Автоматические настройки по умолчанию».

Регулярное выражение $Соответствует

При использовании оператора -match автоматически создается переменная $matches с результатами сопоставления. Если в вашем регулярном выражении есть какие-либо подвыражения, эти подвыражения также будут перечислены.

$message = 'My SSN is 123-45-6789.'

$message -match 'My SSN is (.+)\.'
$Matches[0]
$Matches[1]

Именованные совпадения

Это одна из моих любимых функций, о которой большинство людей не знают. Если вы используете именованное совпадение с регулярным выражением, вы можете получить доступ к этому совпадению по имени в совпадениях.

$message = 'My Name is Kevin and my SSN is 123-45-6789.'

if($message -match 'My Name is (?<Name>.+) and my SSN is (?<SSN>.+)\.')
{
    $Matches.Name
    $Matches.SSN
}

В приведенном выше примере (?.*) является именованным подвыражением. Затем это значение помещается в свойство $Matches.Name.

Групповой объект-AsHashtable

Одна малоизвестная особенность Group-Object заключается в том, что он может превратить некоторые наборы данных в хеш-таблицу.

Import-CSV $Path | Group-Object -AsHashtable -Property email

Это добавит каждую строку в хеш-таблицу и будет использовать указанное свойство в качестве ключа для доступа к ней.

Копирование хеш-таблиц

Важно знать, что хеш-таблицы — это объекты. И каждая переменная — это просто ссылка на объект. Это означает, что для создания действительной копии хеш-таблицы требуется больше работы.

Назначение ссылочных типов

Если у вас есть одна хеш-таблица и вы присваиваете ее второй переменной, обе переменные указывают на одну и ту же хеш-таблицу.

PS> $orig = @{name='orig'}
PS> $copy = $orig
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [copy]

Это подчеркивает, что они одинаковы, поскольку изменение значений в одном также изменит значения в другом. Это также применимо при передаче хэш-таблиц в другие функции. Если эти функции вносят изменения в эту хеш-таблицу, ваш оригинал также изменяется.

Мелкие копии, один уровень

Если у нас есть простая хеш-таблица, как в примере выше, мы можем использовать .Clone(), чтобы сделать неглубокую копию.

PS> $orig = @{name='orig'}
PS> $copy = $orig.Clone()
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [orig]

Это позволит нам внести некоторые базовые изменения в один, которые не повлияют на другой.

Мелкие копии, вложенные

Причина, по которой ее называют поверхностной копией, заключается в том, что она копирует только свойства базового уровня. Если одно из этих свойств является ссылочным типом (например, другая хеш-таблица), то эти вложенные объекты все равно будут указывать друг на друга.

PS> $orig = @{
        person=@{
            name='orig'
        }
    }
PS> $copy = $orig.Clone()
PS> $copy.person.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.person.name
PS> 'Orig: [{0}]' -f $orig.person.name

Copy: [copy]
Orig: [copy]

Итак, вы можете видеть, что хотя я клонировал хеш-таблицу, ссылка на person не была клонирована. Нам нужно сделать глубокую копию, чтобы действительно иметь вторую хеш-таблицу, которая не связана с первой.

Глубокие копии

Есть несколько способов сделать глубокую копию хеш-таблицы (и сохранить ее как хеш-таблицу). Вот функция, использующая PowerShell для рекурсивного создания глубокой копии:

function Get-DeepClone
{
    [CmdletBinding()]
    param(
        $InputObject
    )
    process
    {
        if($InputObject -is [hashtable]) {
            $clone = @{}
            foreach($key in $InputObject.keys)
            {
                $clone[$key] = Get-DeepClone $InputObject[$key]
            }
            return $clone
        } else {
            return $InputObject
        }
    }
}

Он не обрабатывает другие ссылочные типы или массивы, но это хорошая отправная точка.

Другой способ — использовать .Net для его десериализации с помощью CliXml, как в этой функции:

function Get-DeepClone
{
    param(
        $InputObject
    )
    $TempCliXmlString = [System.Management.Automation.PSSerializer]::Serialize($obj, [int32]::MaxValue)
    return [System.Management.Automation.PSSerializer]::Deserialize($TempCliXmlString)
}

Для очень больших хеш-таблиц функция десериализации работает быстрее по мере масштабирования. Однако есть некоторые моменты, которые следует учитывать при использовании этого метода. Поскольку он использует CliXml, он требует много памяти, и если вы клонируете огромные хеш-таблицы, это может стать проблемой. Еще одним ограничением CliXml является ограничение глубины в 48. Это означает, что если у вас есть хеш-таблица с 48 слоями вложенных хеш-таблиц, клонирование завершится неудачно, и хеш-таблица вообще не будет выведена.

Что-нибудь еще?

Я быстро прошёл большую часть территории. Я надеюсь, что вы уйдете, узнавая что-то новое или лучше понимая это каждый раз, когда читаете это. Поскольку я рассмотрел весь спектр этой функции, есть аспекты, которые сейчас могут к вам не относиться. Это совершенно нормально и ожидаемо, в зависимости от того, как много вы работаете с PowerShell.