Powershell 数组:何时使用它们;何时避免它们;以及使用它们的问题

Powershell arrays: When to use them; when to avoid them; and problems using them

为什么 .NET Framework ArrayList class.Add 方法在 PowerShell 实现中不起作用?

除非我另有更正,否则我认为我的故事的总体寓意可能是:不要假设本机 PowerShell 方法将与 .NET 方法相同,并且在尝试 .NET 方法时要小心在 PowerShell 中。

我最初寻求的解决方案是 return 来自函数的日期列表,作为一个数组,以用户定义的日期范围作为参数。然后将引用日期数组来移动和读取以日期戳命名的文件。

我遇到的第一个问题是创建动态数组。我不知道自己在做什么,错误地在 @() 数组声明上调用了 .NET .Add 方法。

Exception calling "Add" with "1" argument(s): "Collection was of a fixed size."

我以为我需要找到一个动态数组类型,但我真正的问题是我没有做对。这让我转向了不同的方向,直到很久以后,我发现应该使用 += 语法将对象添加到 PowerShell 数组中。

无论如何,在我return了解如何正确使用 PowerShell 数组之前,我已经离开了一些其他的切线。

然后我找到了 .NET ArrayList class。好的。现在我有了一个动态数组对象。我阅读了文档,其中说我应该使用 .Add 方法将元素添加到集合中。

然后我开始寻求更深入的理解,因为我经历了几天的挫折感,试图解决问题。

我制作了一个最初看起来有效的实现。它产生了一个日期范围——但它也产生了一些奇怪的行为。我观察到奇怪的日期 returned,例如:

Monday, January 1, 0001 12:00:00 AM

原来,我发现,这是你这样做时得到的结果:

Get-Date 0

ArrayList return 首先是数组元素的索引值列表,然后是数组值。那根本没有任何意义。我开始探索我是否正确地调用了函数,我是否遇到了某种可变范围问题,或者我是否只是疯了。

我现在相当确信,我的挫败感是由于缺乏可靠的初学者参考资料造成的,该参考资料不仅展示了如何执行简单数组实现的几个示例,而且还描​​述了一些注意事项, 以及替代解决方案。

那么,让我在这里解释一下实现 arrays/collections 的三种方法,以及我试图生成的解决方案 - 即日期范围内的日期列表。

出于某种原因,我最初认为在 Powershell 中向 .NET ArrayList 添加元素的正确方法是使用 .Add 方法。是 documented。我仍然不明白这不起作用的原因(说真的 - 有人请赐教)。通过实验,我发现我可以通过使用 += 方法将对象添加到 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
}

现在,下面有三个代码示例可以正常工作。在每个示例中,代码都是相同的。我所做的只是 commented/uncommented 相应的实例化行和方法行。

首先,使用本机 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 Generic List:

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 class 都生成非强类型对象集合,因此您可以这样做(在 Powershell 数组实现中):

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

对于非常大的阵列,Powershell 阵列效率不高。 ArrayList 是非常大的集合的更好选择。

.NET Framework Generic List 似乎是非常大的相同类型对象集合的最佳选择。在我的示例中,我想要一个日期列表。每个日期都是相同的数据类型,所以我不需要混合对象类型。因此,我正在部署的解决方案是上面的第三个工作示例。

我很欣赏 Dave Wyatt 在 2013 年发表的 Powershell.org 文章,主题是:PowerShell Performance: The += Operator (and When to Avoid It). In particular, the += method creates a new array object in each pass within a loop,添加新元素,然后销毁旧数组。对于大型集合,这变得非常低效。

我发布这些解决方案和讨论是希望其他一些初学者更容易找到我正在寻找的答案。

是的 - 没错 - 我不遵守某些人认为严格的 PowerShell 语法礼仪。我在函数中使用 return 语句,所以很明显函数产生了什么。我更喜欢看起来杂乱无章而不是紧凑的可读代码。这是我的偏好,我会坚持下去。

对于日期列表的更类似 PowerShell 的实现,我建议读者参考 tidy implementation posted by The Surly Admin

大多数时候我看到数组加法,这是完全没有必要的。每当一个表达式 returns 超过一个对象时,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")
}

所需要的只是创建和输出您的对象,并将结果分配回您的变量,您将拥有一个数组。

关于 OP 的第 3 段: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

我认为从中得到的收获是更仔细地阅读 arraylist 的 MSDN 文档。

如果您在 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)

当您通过管道传递数组时,也存在类似的问题。如果您希望数组沿着管道传递(相对于数组元素),那么您需要先将其包装在另一个数组中。