powershell 对于处理属于函数参数集的基于集合的参数,这是一个好的“模式”吗?

rqmkfv5c  于 2023-08-05  发布在  Shell
关注(0)|答案(2)|浏览(142)

我已经写了很多年的高级函数了,现在甚至已经写了很多模块。但有一个问题我一直没能找到答案。
让我们以Microsoft在MSMQ模块中提供的Cmdlet为例,并将其“重新实现”为高级PowerShell函数:Send-MsmqQueue。但此函数与MSMQ模块提供的函数略有不同,因为它不仅接受$InputObject参数的多个MSMQ队列,还接受$Name参数的多个MSMQ队列名称,其中这两个参数属于不同的参数集。(此函数的Cmdlet版本通常只接受$Name参数的单个字符串值。)我不会展示一个 * 完整 * 的重新实现,只是足以说明我有时会在出现这种情况时做什么。(注意:另一个细微的区别是,我将使用来自System.Messaging命名空间的类,而不是PowerShell提供的Microsoft.Msmq.PowerShell.Commands命名空间中的类。因此,假设在某处隐式地执行了Add-Type -AssemblyName System.Messaging。)

function Send-MsmqQueue {
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    [OutputType([Messaging.Message])]
    Param (
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'InputObject')
        ]
        [Messaging.MessageQueue[]] $InputObject,

        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ParameterSetName = 'Name')
        ]
        [string[]] $Name,

        # Below is the original parameter name, not mine ;)
        [Messaging.Message] $MessageObject

        # All other normal Send-MsmqQueue parameters elided as they are not
        # needed to illustrate the premise of my question.
    )

    Process {
        # When I have parameters defined as above, the first thing I do in my
        # Process block is "homogenize" the data so I don't have to implement
        # two foreach loops or do the branching on each foreach loop iteration
        # which can obscure the main logic that is being executed, i.e., I get
        # this done all "up-front".
        #
        # One aspect of my question is, from purely a PowerShell perspective,
        # is this hurting performance in any meaningful way? (I know that when it
        # comes to specific implementation details, there are INFINITE ways to
        # write non-performant code, so from purely a PowerShell perspective,
        # as far as the language design/inner-workings, is this hurting
        # performance?
        #
        # NOTE: I don't normally need the wrapping "force this thing to be an
        # array" construct (,<array_items>), BUT, in this case, the C#
        # System.Messaging.MessageQueue class implements IEnumerable,
        # which PowerShell (unhelpfully) iterates over automatically, and results
        # in the messages in the queues being iterated over instead of the queues
        # themselves, so this is an implementation detail specific to this
        # particular function.
        $Queues = (,@(
            if ($PSCmdlet.ParameterSetName -ieq 'Name') {
                # Handle when the parameter is NOT passed by the pipeline...
                foreach ($n in $Name) { [Messaging.MessageQueue]::new($n) }
            } else {
                $InputObject
            }
        ))

        # I like using 'foreach (...) { ... }' instead of ForEach-Object because
        # oftentimes, I will need to break or continue based on implementation
        # details, and using ForEach-Object in combination with break/continue
        # causes the pipeline to prematurely exit.
        foreach ($q in $Queues) {
            $q.Send($MessageObject)
            # Normally, I wouldn't return this, especially since it wasn't
            # modified, but this is a re-implementation of MSFT's Send-MsmqQueue,
            # and it returns the sent message.
            $MessageObject
        }
    }
}

字符串
正如我在这个问题的介绍中所述,我已经编写了许多函数,这些函数采用不同的基于集合的参数,这些参数属于不同的参数集,可以通过管道传输到函数中,这就是我使用的模式。我希望有人可以从PowerShell语言/风格的Angular 确认这是可以的,并且/或者帮助我理解为什么我不应该这样做,以及我应该考虑什么。
谢谢你,谢谢

jv4diomz

jv4diomz1#

一个基本的性能决策是您是否希望对参数传递进行优化,而不是 * 流水线输入*:

  • 将参数 * 声明为数组 *(例如[string[]] $Name)允许通过 argument(参数值)高效传递 * 多个 * 输入对象。
  • 然而,这样做 * 会损害管道性能 *,因为随后会为每个管道输入对象创建一个单元素数组,如以下示例所示:它输出String[],用于通过管道传递的数组的 * 每个 * 标量字符串元素:
'one', 'two' | 
  & {
    param(
      [Parameter(Mandatory, ValueFromPipeline)]
      [string[]] $Name
    )
    process {
      $Name.GetType().Name # -> 'String[]' *for each* input string
    }
  }

字符串

*注意:为简洁起见,上面的示例以及本答案中的所有其他示例都使用script block代替function定义。也就是说,函数声明(function foo { ... })后跟其调用(... | foo)被缩短为功能等效的... | & { ... }

有关相关讨论,请参见GitHub issue #4242
对于 array 参数,您确实需要确保自己进行逐个元素的处理,特别是在process块中,如果它们也是 * 管道绑定 * 的话。
对于**“均质化”不同类型的参数值**,从而只需要 * 一个 * 处理循环,有两种基本的优化可能:

*仅声明 * 单个 * 参数,并依赖PowerShell将其他类型的值 * 自动 * 转换为该参数的类型,或实现自动应用的 * 自定义转换 *,从而完全消除了“同质化”的需要:

  • 如果参数类型有一个公共的单参数构造函数,它接受另一个类型的示例作为它的(唯一的)参数,或者-如果另一个类型是[string],如果该类型有一个静态::Parse()方法,带有一个[string]参数,则**转换是 * 自动的 *;例如:
# Sample class with a single-parameter
# public constructor that accepts [int] values.
class Foo {
  [int] $n
  Foo([int] $val) {
    $this.n = $val
  }
}

# [int] values (whether provided via the pipeline or as an argument)
# auto-convert to [Foo] instances
42, 43 | & {
  [CmdletBinding()]
  param(
    [Parameter(ValueFromPipeline)]
    [Foo[]] $Foo
  )
  process {
    $Foo # Diagnostic output.
  }
}

  • 在您的例子中,[Messaging.MessageQueue] * 确实 * 有一个接受字符串的公共单参数构造函数(正如您的[Messaging.MessageQueue]::new($n)调用所证明的那样),因此您可以简单地 * 省略 * $Name参数声明,并依赖于[string]输入的自动转换。
  • A * 一般警告 *:
  • 这种自动转换--也发生在 casts(例如[Foo[]] (0x2a, 43),见下文)和(很少使用的)内在.ForEach()的类型转换形式(例如(0x2a, 43).ForEach([Foo]))--在匹配构造函数的参数类型方面比调用单元素构造函数更严格。
  • 我不清楚确切的规则,但例如,使用[double]值,[Foo]::new(42.1)成功(即自动执行到[int]的转换),但[Foo] 42.1(42.1).ForEach([Foo])都失败(后者目前产生模糊的错误消息)。
  • 如果转换 * 不是 * 自动的,实现一个 * 自定义 * 转换,然后PowerShell自动应用,方法是使用从抽象ArgumentTransformationAttribute类派生的自定义属性装饰参数;例如:
using namespace System.Management.Automation

# Sample class with a single-parameter
# public constructor that accepts [int] values.
class Foo {
  [int] $n
  Foo([int] $val) {
    $this.n = $val
  }
}

# A sample argument-conversion (transformation) attribute class that
# converts strings that can be interpreted as [int] to [Foo] instances.
class CustomTransformationAttribute : ArgumentTransformationAttribute  {
  [object] Transform([EngineIntrinsics] $engineIntrinsics, [object] $inputData) {            
    # Note: If the inputs were passed as an *array argument*, $inputData is an array.
    return $(foreach ($o in $inputData) {
      if ($null -ne ($int = $o -as [int])) { [Foo]::new($int) }
      else                                 { $o }
    })
  }
}

# [string] values (whether provided via the pipeline or as an argument)
# that can be interpreted as [int] now auto-convert to [Foo] instances,
#  thanks to the custom [ArgumentTransformationAttribute]-derived attribute.
'0x2a', '43' | & {
  [CmdletBinding()]
  param(
    [Parameter(ValueFromPipeline)]
    [CustomTransformation()] # This implements the custom transformation.
    [Foo[]] $Foo
  )
  process {
    $Foo # Diagnostic output.
  }
}

  • 如果您 * 确实 * 想要 * 单独 * 参数,请优化转换过程**:
  • 上面描述的自动类型转换规则也适用于 * 显式转换 *(包括支持 * 数组 * 的值),所以你可以简化代码如下:
if ($PSCmdlet.ParameterSetName -eq 'Name') {
  # Simply use an array cast.
  $Queues = [Messaging.MessageQueue[]] $Name
} else {
  $Queues = $InputObject
}

  • 在需要逐元素构建以实现转换的情况下:
if ($PSCmdlet.ParameterSetName -eq 'Name') {
  # Note the ","
  $Queues = foreach ($n in $Name) { , [Messaging.MessageQueue]::new($n) }
} else {
  $Queues = $InputObject
}

  • 请注意使用了一元形式的,数组构造函数(“逗号”)运算符,就像您的尝试一样,尽管:
  • insideforeach循环,以及
    • 没有 * @(...)封装对象以 Package 在单元素数组中,因为@(...) * 本身 * 将触发枚举。
  • 虽然Write-Output -NoEnumerate ([Messaging.MessageQueue]::new($n)),如Mathias的答案所示,也可以工作,但它 * 较慢 *。它归结为性能/简洁性与可读性/明确地用信号通知意图。
  • 需要将 each[System.Messaging.MessageQueue]示例 Package 在aux中。一元,/的单元素 Package 器使用Write-Output -NoEnumerate源于这样一个事实,即该类型实现了System.Collections.IEnumerable接口,这意味着PowerShell默认情况下会自动 * 枚举 * 该类型的示例。[1]应用任何一种技术都可以确保[System.Messaging.MessageQueue]作为一个整体 * 输出到管道(有关详细信息,请参阅this answer)。
  • 请注意,在第一个代码段中,这是 * 不 * 必要的,因为$Queues = [Messaging.MessageQueue[]] $Name是 * 表达式 *,自动枚举 * 不 * 适用于它。
  • 上面还暗示,如果您希望通过管道 * 传递 * 单个 * [System.Messaging.MessageQueue]示例或包含此类示例 * 的 * 单元素 * 数组,则需要使用相同的技术;例如:
# !! Without `,` this command would *break*, because
 # !! PowerShell would try to enumerate the elements of the queue
 # !! which fails with an empty one.
 , [System.Messaging.MessageQueue]::new('foo') | Get-Member

  • 通过 not 使用if语句作为单个 * 赋值表达式 *($Queue = if ...),而不是在if语句的 branches 中赋值给$Queue,您还可以防止$InputObject受到不必要的枚举。

[1]也有一些例外,特别是字符串和字典。有关详细信息,请参见this answer的底部部分。

pn9klfpd

pn9klfpd2#

这种模式(基于所选参数集“均匀化”输入实体)是完全有效的,并且至少在我个人看来构成了良好的参数设计。
也就是说,您可能希望使用Write-Output -NoEnumerate来避免笨拙的,@(...) unwrapped-wrapped-array解包技巧:

if ($PSCmdlet.ParameterSetName -ieq 'Name') {
    # Handle when the parameter is NOT passed by the pipeline...
    $Queues = foreach ($n in $Name) {
        $queue = [Messaging.MessageQueue]::new($n)
        Write-Output $queue -NoEnumerate
    }
}
else {
    # Input is already [MessageQueue[]], avoid pipeline boundaries entirely
    $Queues = $InputObject 
}

字符串

相关问题