Массивы Powershell: когда их использовать; когда их избегать; и проблемы с их использованием

Почему метод .Add класса .NET Framework ArrayList не работает в реализации PowerShell?

Если меня не исправят, я думаю, что общая мораль моей истории может быть такой: не думайте, что нативные методы PowerShell будут такими же, как методы .NET, и будьте осторожны при попытке использовать методы .NET в PowerShell.

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

Первой проблемой, с которой я столкнулся, было создание динамического массива. Я не знал, что делаю, и неправильно вызывал метод .NET .Add для объявления массива @().

Исключение, вызывающее «Добавить» с аргументом (-ами) «1»: «Коллекция имеет фиксированный размер».

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

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

Затем я нашел класс .NET ArrayList. В порядке Хорошо. Теперь у меня был объект динамического массива. Я прочитал документацию, в которой говорилось, что я должен использовать метод .Add для добавления элементов в коллекцию.

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

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

Понедельник, 1 января 0001 г., 00:00:00

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

Get-Date 0

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

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

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

По какой-то причине я сначала подумал, что правильный метод добавления элемента в .NET ArrayList в Powershell — использовать метод .Add. Это задокументировано. Я до сих пор не понимаю, почему это не работает (серьезно - кто-нибудь, пожалуйста, просветите меня). Экспериментируя, я обнаружил, что могу получить точные результаты, используя метод += для добавления объектов в ArrayList.

Не делай этого. Это абсолютно НЕПРАВИЛЬНО. Это приведет к ошибкам, которые я описал выше:

Function Get-DateRangeList {
    [cmdletbinding()]
    Param (
        [datetime] $startDate,
        [datetime] $endDate
    )

    $datesArray = [System.Collections.ArrayList]@()  # Second method

    for ($d = $startDate; $d -le $endDate; $d = $d.AddDays(1)) {
        if ($d.DayOfWeek -ne 'Sunday') {
            $datesArray.Add($d)
        }
    }

    Return $datesArray
}

# Get one week of dates, ending with yesterday's date
$startDate = Get-Date
$endDate = $startDate.AddDays(-1)  # Get yesterday's date as last date in range
$startDate = $endDate.AddDays(-7)  # Get 7th prior date as first date in range

$datesList = Get-DateRangeList  $startDate $endDate

# Loop through the dates
Foreach ($d in $datesList) {
    # Do something with each date, e.g., format the date as part of a list
    # of date-stamped files to retrieve
    $d
}

Ниже приведены три примера кода, которые РАБОТАЮТ. В каждом примере код одинаковый. Все, что я сделал, это прокомментировал/раскомментировал соответствующие строки инстанцирования и строки методов.

Во-первых, используя собственный объект массива PowerShell:

Function Get-DateRangeList {
    [cmdletbinding()]
    Param (
        [datetime] $startDate,
        [datetime] $endDate
    )

    $datesArray = @()  # First method
    #$datesArray = [System.Collections.ArrayList]@()  # Second method
    #$datesArray = New-Object System.Collections.Generic.List[System.Object]  # Third method

    for ($d = $startDate; $d -le $endDate; $d = $d.AddDays(1)) {
        if ($d.DayOfWeek -ne 'Sunday') {
            $datesArray += $d     # First and second method: += is the method to add elements to: Powershell array; or .NET ArrayList (confusing)
            #$datesArray.Add($d)  # Third method: .Add is the method to add elements to: .NET Generic List
        }
    }

    Return $datesArray
}

# Get one week of dates, ending with yesterday's date
$startDate = Get-Date
$endDate = $startDate.AddDays(-1)  # Get yesterday's date as last date in range
$startDate = $endDate.AddDays(-7)  # Get 7th prior date as first date in range

$datesList = Get-DateRangeList  $startDate $endDate

# Loop through the dates
Foreach ($d in $datesList) {
    # Do something with each date, e.g., format the date as part of a list
    # of date-stamped files to retrieve
    "FileName_{0}.txt" -f $d.ToString("yyyyMMdd")
}

Во-вторых, с помощью .NET Framework ArrayList:

Function Get-DateRangeList {
    [cmdletbinding()]
    Param (
        [datetime] $startDate,
        [datetime] $endDate
    )

    #$datesArray = @()  # First method
    $datesArray = [System.Collections.ArrayList]@()  # Second method
    #$datesArray = New-Object System.Collections.Generic.List[System.Object]  # Third method

    for ($d = $startDate; $d -le $endDate; $d = $d.AddDays(1)) {
        if ($d.DayOfWeek -ne 'Sunday') {
            $datesArray += $d     # First and second method: += is the method to add elements to: Powershell array; or .NET ArrayList (confusing)
            #$datesArray.Add($d)  # Third method: .Add is the method to add elements to: .NET Generic List
        }
    }

    Return $datesArray
}

# Get one week of dates, ending with yesterday's date
$startDate = Get-Date
$endDate = $startDate.AddDays(-1)  # Get yesterday's date as last date in range
$startDate = $endDate.AddDays(-7)  # Get 7th prior date as first date in range

$datesList = Get-DateRangeList  $startDate $endDate

# Loop through the dates
Foreach ($d in $datesList) {
    # Do something with each date, e.g., format the date as part of a list
    # of date-stamped files to retrieve
    "FileName_{0}.txt" -f $d.ToString("yyyyMMdd")
}

В-третьих, используя общий список .NET Framework:

Function Get-DateRangeList {
    [cmdletbinding()]
    Param (
        [datetime] $startDate,
        [datetime] $endDate
    )

    #$datesArray = @()  # First method
    #$datesArray = [System.Collections.ArrayList]@()  # Second method
    $datesArray = New-Object System.Collections.Generic.List[System.Object]  # Third method

    for ($d = $startDate; $d -le $endDate; $d = $d.AddDays(1)) {
        if ($d.DayOfWeek -ne 'Sunday') {
            #$datesArray += $d     # First and second method: += is the method to add elements to: Powershell array; or .NET ArrayList (confusing)
            $datesArray.Add($d)  # Third method: .Add is the method to add elements to: .NET Generic List
        }
    }

    Return $datesArray
}

# Get one week of dates, ending with yesterday's date
$startDate = Get-Date
$endDate = $startDate.AddDays(-1)  # Get yesterday's date as last date in range
$startDate = $endDate.AddDays(-7)  # Get 7th prior date as first date in range

$datesList = Get-DateRangeList  $startDate $endDate

# Loop through the dates
Foreach ($d in $datesList) {
    # Do something with each date, e.g., format the date as part of a list
    # of date-stamped files to retrieve
    "FileName_{0}.txt" -f $d.ToString("yyyyMMdd")
}

Все три работают. Почему вы предпочитаете одно другому? Собственный массив PowerShell и класс .NET Framework ArrayList создают коллекции объектов, которые не являются строго типизированными, поэтому вы можете сделать это (в реализации массива Powershell):

$myArray = @(1, 2, 3, "A", "B", "C")

Массив Powershell не будет эффективен для очень большого массива. ArrayList — лучший выбор для очень большой коллекции.

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

Я ценю статью Дейва Вятта на Powershell.org 2013 года по теме: it/" rel="nofollow noreferrer">Производительность PowerShell: оператор += (и когда его следует избегать). В частности, метод += создает новый объект массива при каждом проходе. внутри цикла, добавляя новый элемент, а затем уничтожая старый массив. Это становится очень неэффективным с большой коллекцией.

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

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

Для более похожей на PowerShell реализации списка дат я отсылаю читателей к аккуратная реализация, опубликованная The Surly Admin.


person 504more    schedule 05.12.2015    source источник
comment
Воспользуйтесь конвейером PowerShell, он значительно упрощает выполнение ваших задач.   -  person beatcracker    schedule 05.12.2015
comment
ArrayList.Add возвращает индекс добавленного элемента, а поскольку PowerShell возвращает что-либо даже без оператора return, он возвращает этот индекс, вы каким-то образом устраните это: [void]$datesArray.Add($d). += не добавляет элементы в ArrayList: $a=New-Object Collections.ArrayList;$a+=1;$a.GetType(), поэтому ваш второй пример работает не с ArrayList, а с массивом, как и первый. И, ИМХО, не используйте @(1, 2, 3, "A", "B", "C"): (1, 2, 3, "A", "B", "C") дает тот же результат, берет на один символ меньше для ввода и не делает ненужной копии массива.   -  person user4003407    schedule 06.12.2015


Ответы (2)


Что касается 3-го абзаца OP: Collections.arraylist работает в powershell, например:

# Create arraylist with space for 20 object
$ar = new-object collections.arraylist 20
$ar.add("hello everybody")
$ar.add([datetime]::now)
$ar.add( (gps)[9])
$ar[0]  # returns string
$ar[1]  # returns datetime
$ar[2]  # returns tenth process
$ar.count # returns 3

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

Если вы используете += для списка массивов в PS, он берет элементы из списка массивов и новый элемент и создает массив. Я считаю, что это попытка оградить пользователей от сложности .NET, на которую вы наткнулись. (Я подозреваю, что одним из основных вариантов использования команды разработчиков PS является пользователь, который не знаком с .NET в целом и с arraylist в частности. Очевидно, вы не попадаете в эту категорию.)

Упомяну камень преткновения с PS и массивами. PS в некоторых случаях автоматически разворачивает массивы. Например, если у меня есть массив символов, и я хочу создать строку (используя перегрузку String..ctor([char[]])), то это не работает:

# Fails because PS unrolls the array and thinks that each element is a
# different argument to String..ctor
$stringFromCharArray = new-object string $charArray
# Wrap $charArray to get it to work
$stringFromCharArray = new-object string @(,$charArray)
# This also works
$stringFromCharArray = new-object string (,$charArray)

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

person Χpẘ    schedule 05.12.2015
comment
Если вы используете += в PS, он достаточно умен, чтобы знать, когда ему нужно выделить новый объект, потому что объект в левой части заполнен или доступен только для чтения. AFAIK, оператор .NET. Соглашение о перегрузке требует, чтобы операторы не делали каких-либо заметных изменений своих операндов. Так что $a=$b+$c или $a=$b;$a+=$c должны остаться $b нетронутыми. Это означает, что $a должна быть новой коллекцией при каждом вызове оператора + или +=. - person user4003407; 06.12.2015
comment
@PetSerAl Вы правы в том, что я сказал неточно. Я обновлю свой ответ. Однако ситуация еще хуже, чем просто перераспределение массива. PS преобразует список массивов и новый элемент в обычный массив. - person Χpẘ; 06.12.2015
comment
День 12 с Powershell для меня. Что я вижу в вашем использовании ArrayList, так это создание экземпляра с помощью New-Object. Некоторые люди говорят, что это дорогой способ добиться цели. Я не думаю, что это имеет значение для того, что я делаю. У меня это не работает с датой (или любым другим) примером. $datesArray = new-object collections.arraylist $d = Get-Date $datesArray.Add($d) $datesArray Это создает значение индекса 0, затем дату. Я не думаю, что это правильно и не дает решения. - person 504more; 06.12.2015
comment
@ 504more Это определенно работает с датой и временем. Попробуйте первый фрагмент кода в этом ответе. Я думаю, что ноль, который вы видите, — это возвращаемое значение метода Add. Если вам не нужно возвращаемое значение из метода, вы можете сделать это: $null = $datesArray.Add($d). - person Χpẘ; 07.12.2015
comment
@ user2460798: Верно. Я понятия не имел об использовании левостороннего оператора для исключения вывода, полученного с помощью метода Add — очевидно, без сомнения, для опытных пользователей PS, но нового для меня, поэтому очень полезного. Спасибо. Это неправильно отформатирует: # Создать список массивов с пространством для 20 объектов $ar = new-object collections.arraylist 20 Write-Host Добавление значений ... $null = $ar.add(привет всем) $null = $ar. add([datetime]::now) $null = $ar.add( (gps)[9]) $ar[0] # возвращает строку $ar[1] # возвращает дату и время $ar[2] # возвращает десятый процесс $ ar.count # возвращает 3 Write-Host Unpack array ... $ar - person 504more; 07.12.2015
comment
Не уверен, что вы подразумеваете под форматом правильно. Это выглядит так, как я ожидаю. write-host (действует так, как будто он) выполняет .tostring() для своих аргументов. То, как конкретный тип представляет экземпляр в виде строки, зависит от типа. (У Powershell есть способ переопределить это: см. help about_Format.ps1xml). Если вы не знаете, с каким типом объектов вы имеете дело, я бы не стал полагаться на то, что вывод .tostring() будет выглядеть так, как вы ожидаете. Кстати, реализация Object.tostring() (по сути, реализация по умолчанию для всего, что происходит от Object) — это просто имя типа. - person Χpẘ; 08.12.2015

Большую часть времени я вижу добавление массива, это совершенно не нужно. Конвейер Powershell будет автоматически создавать массивы каждый раз, когда выражение возвращает более одного объекта, и делает это очень эффективно.

Рассмотреть возможность:

Clear-Host 

Function Get-DateRangeList {

    [cmdletbinding()]
    Param (
        [datetime] $startDate,
        [datetime] $endDate
    )

    $datesArray = 
    for ($d = $startDate; $d -le $endDate; $d = $d.AddDays(1)) {

        if ($d.DayOfWeek -ne 'Sunday') {

            $d
        }

    }

    Return ,$datesArray

}


# Get one week of dates, ending with yesterday's date
$startDate = Get-Date
$endDate = $startDate.AddDays(-1)  # Get yesterday's date as last date in range
$startDate = $endDate.AddDays(-7)  # Get 7th prior date as first date in range


$datesList = Get-DateRangeList  $startDate $endDate

# Loop through the dates
Foreach ($d in $datesList) {

    # Do something with each date, e.g., format the date as part of a list of date-stamped files to retrieve
    “FileName_{0}.txt" -f $d.ToString("yyyyMMdd")
}

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

person mjolinor    schedule 05.12.2015
comment
Хорошо. Я ценю пример. Спасибо. - person 504more; 06.12.2015