如何在PowerShell中创建自定义类型供脚本使用?


88

我希望能够在某些PowerShell脚本中定义和使用自定义类型。例如,假设我们需要一个具有以下结构的对象:

Contact
{
    string First
    string Last
    string Phone
}

我将如何创建它,以便可以在如下函数中使用它:

function PrintContact
{
    param( [Contact]$contact )
    "Customer Name is " + $contact.First + " " + $contact.Last
    "Customer Phone is " + $contact.Phone 
}

这样的事情是否可行,甚至在PowerShell中推荐?

Answers:


133

在PowerShell 3之前

PowerShell的可扩展类型系统最初不允许您创建可以根据参数设置方法进行测试的具体类型。如果您不需要该测试,则可以使用上述任何其他方法。

如果您想要一个可以转换为或进行类型检查的实际类型,例如在示例脚本中……除非将其写入C#或VB.net并进行编译,否则无法完成。在PowerShell 2中,您可以使用“ Add-Type”命令来简化它:

add-type @"
public struct contact {
   public string First;
   public string Last;
   public string Phone;
}
"@

历史注释:在PowerShell 1中,它甚至更难。您必须手动使用CodeDom,PoshCode.org上有一个非常老的函数new-struct脚本,可以帮助您。您的示例变为:

New-Struct Contact @{
    First=[string];
    Last=[string];
    Phone=[string];
}

使用Add-TypeNew-Struct将让您实际测试自己的班级,param([Contact]$contact)并使用$contact = new-object Contact等创建新班级...

在PowerShell 3中

如果不需要可以转换为的“真实”类,则不必使用Steven和其他人在上面演示的Add-Member方法。

从PowerShell 2开始,可以对新对象使用-Property参数:

$Contact = New-Object PSObject -Property @{ First=""; Last=""; Phone="" }

在PowerShell 3中,我们可以使用PSCustomObject加速器添加TypeName:

[PSCustomObject]@{
    PSTypeName = "Contact"
    First = $First
    Last = $Last
    Phone = $Phone
}

您仍然只得到一个对象,因此您应该创建一个New-Contact函数以确保每个对象都相同,但是现在可以通过使用PSTypeName属性装饰参数来轻松地验证参数“是”其中一种类型:

function PrintContact
{
    param( [PSTypeName("Contact")]$contact )
    "Customer Name is " + $contact.First + " " + $contact.Last
    "Customer Phone is " + $contact.Phone 
}

在PowerShell 5中

在PowerShell 5中,一切都发生了变化,最后我们得到了classenum作为用于定义类型的语言关键字(没有,struct但是没关系):

class Contact
{
    # Optionally, add attributes to prevent invalid values
    [ValidateNotNullOrEmpty()][string]$First
    [ValidateNotNullOrEmpty()][string]$Last
    [ValidateNotNullOrEmpty()][string]$Phone

    # optionally, have a constructor to 
    # force properties to be set:
    Contact($First, $Last, $Phone) {
       $this.First = $First
       $this.Last = $Last
       $this.Phone = $Phone
    }
}

我们还提供了一种无需使用New-Object以下对象即可创建对象的新方法:[Contact]::new()-实际上,如果使类保持简单并且不定义构造函数,则可以通过强制转换哈希表来创建对象(尽管没有构造函数,也无法强制必须设置所有属性):

class Contact
{
    # Optionally, add attributes to prevent invalid values
    [ValidateNotNullOrEmpty()][string]$First
    [ValidateNotNullOrEmpty()][string]$Last
    [ValidateNotNullOrEmpty()][string]$Phone
}

$C = [Contact]@{
   First = "Joel"
   Last = "Bennett"
}

好答案!只需添加一个注释即可,这种样式对于脚本来说非常容易,并且仍然可以在PowerShell 5中使用:New-Object PSObject -Property @ {prop here ...}
Ryan Shillington,

2
在早期的PowerShell 5版本中,不能将New-Object与使用类语法创建的类一起使用,但现在可以使用。不过,如果你使用class关键字,你的脚本被限制为仅PS5无论如何,所以我还是会建议使用::新语法,如果对象有一个构造函数参数(这是很多比新物体更快)或否则进行强制转换,语法更简洁,速度更快。
Jaykul

确定不能使用创建的类型进行类型检查Add-Type吗?它似乎可以在Win 2008 R2的PowerShell 2中运行。说我定义contact使用Add-Type在你的答案,然后创建一个实例:$con = New-Object contact -Property @{ First="a"; Last="b"; Phone="c" }。然后,调用此函数有效:function x([contact]$c) { Write-Host ($c | Out-String) $c.GetType() },但是调用此函数失败x([doesnotexist]$c) { Write-Host ($c | Out-String) $c.GetType() }。调用x 'abc'也会失败,并显示有关强制转换的适当错误消息。测试在PS 2和4
jpmc26

当然,您可以检查使用Add-Type@ jpmc26创建的类型,我说的是如果不进行编译就无法做到(即:如果不使用C#编写并调用Add-Type)。当然,从PS3,您可以-有一个[PSTypeName("...")]属性可让您将类型指定为字符串,从而支持对设置了PSTypeNames的PSCustomObjects进行测试...
Jaykul

58

可以在PowerShell中创建自定义类型。
柯克·蒙罗(Kirk Munro)实际上有两个很棒的帖子,对过程进行了详尽的详细介绍。

Manning撰写的《Windows PowerShell In Action》一书还提供了一个代码示例,用于创建特定于域的语言来创建自定义类型。这本书到处都是很棒的,所以我真的推荐它。

如果您只是在寻找一种快速的方法来执行上述操作,则可以创建一个函数来创建自定义对象,例如

function New-Person()
{
  param ($FirstName, $LastName, $Phone)

  $person = new-object PSObject

  $person | add-member -type NoteProperty -Name First -Value $FirstName
  $person | add-member -type NoteProperty -Name Last -Value $LastName
  $person | add-member -type NoteProperty -Name Phone -Value $Phone

  return $person
}

17

这是快捷方式:

$myPerson = "" | Select-Object First,Last,Phone

3
基本上,如果对象尚不具有此属性,则Select-Object cmdlet会将属性添加到给定的对象中。在这种情况下,您要将空白的String对象传递给Select-Object cmdlet。它添加属性并将对象沿管道传递。或者,如果它是管道中的最后一个命令,则输出对象。我应该指出,只有在提示符下工作时,才使用此方法。对于脚本,我总是使用更明确的Add-Member或New-Object cmdlet。
EBGreen

尽管这是一个绝妙的技巧,但您实际上可以将其缩短:$myPerson = 1 | Select First,Last,Phone
RaYell 2015年

这不允许您使用本机类型函数,因为它将每个成员的类型设置为字符串。鉴于以上Jaykul的贡献,将每个成员注释显示NotePropertystring类型,它是Property您在对象中分配的任何类型的注释。这很快并且可以完成工作。
mbrownnyc

如果您想要一个Length属性,这可能会给您带来问题,因为string已经具有该属性,并且您的新对象将获得现有值-您可能不想要。我建议传递一个[int],如@RaYell所示。
FSCKur

9

史蒂芬·穆拉夫斯基(Steven Murawski)的回答很好,但是我喜欢较短的答案(或者更简洁的选择对象而不是使用添加成员语法):

function New-Person() {
  param ($FirstName, $LastName, $Phone)

  $person = new-object PSObject | select-object First, Last, Phone

  $person.First = $FirstName
  $person.Last = $LastName
  $person.Phone = $Phone

  return $person
}

New-Object甚至不需要。这样做是一样的:... = 1 | select-object First, Last, Phone
Roman Kuzmin

1
是的,但是和上面的EBGreen一样-这会创建一种奇怪的基础类型(在您的示例中为Int32。),就像您键入的内容一样:gm。我更喜欢将基础类型设置为PSCustomObject
Nick Meldrum

2
我明白了。尽管如此,这种int方式仍然具有明显的优势:1)它的运行速度更快,虽然不是很多,但是对于该特定功能New-Person,相差20%;2)显然更容易键入。同时,基本上在所有地方都使用这种方法,我从未见过任何缺点。但我同意:在某些情况下,PSCustomObject会更好。
Roman Kuzmin

@RomanKuzmin如果您实例化全局自定义对象并将其存储为脚本变量,它的速度还是快20%?
jpmc26,2017年

5

没有人惊讶地提到创建自定义对象的简单方法(相对于3或更高版本):

[PSCustomObject]@{
    First = $First
    Last = $Last
    Phone = $Phone
}

该类型将是PSCustomObject,但不是实际的自定义类型。但这可能是创建自定义对象的最简单方法。


另请参阅Will Anderson的这篇博客文章,介绍PSObject和PSCustomObject的区别。
CodeFox

@CodeFox刚刚注意到链接现在已断开
superjos

2
@superjos,谢谢您的提示。我找不到帖子的新位置。至少该职位已由档案馆备份。
CodeFox

2
显然它看起来像变成了Git的书在这里:)
superjos

4

您可以使用PSObject和Add-Member的概念。

$contact = New-Object PSObject

$contact | Add-Member -memberType NoteProperty -name "First" -value "John"
$contact | Add-Member -memberType NoteProperty -name "Last" -value "Doe"
$contact | Add-Member -memberType NoteProperty -name "Phone" -value "123-4567"

输出如下:

[8] » $contact

First                                       Last                                       Phone
-----                                       ----                                       -----
John                                        Doe                                        123-4567

另一个替代方法(我知道)是在C#/ VB.NET中定义一个类型,然后将该程序集加载到PowerShell中以直接使用。

绝对鼓励这种行为,因为它允许其他脚本或脚本的某些部分与实际对象一起使用。


3

这是创建自定义类型并将其存储在集合中的艰难路径。

$Collection = @()

$Object = New-Object -TypeName PSObject
$Object.PsObject.TypeNames.Add('MyCustomType.Contact.Detail')
Add-Member -InputObject $Object -memberType NoteProperty -name "First" -value "John"
Add-Member -InputObject $Object -memberType NoteProperty -name "Last" -value "Doe"
Add-Member -InputObject $Object -memberType NoteProperty -name "Phone" -value "123-4567"
$Collection += $Object

$Object = New-Object -TypeName PSObject
$Object.PsObject.TypeNames.Add('MyCustomType.Contact.Detail')
Add-Member -InputObject $Object -memberType NoteProperty -name "First" -value "Jeanne"
Add-Member -InputObject $Object -memberType NoteProperty -name "Last" -value "Doe"
Add-Member -InputObject $Object -memberType NoteProperty -name "Phone" -value "765-4321"
$Collection += $Object

Write-Ouput -InputObject $Collection

将类型名称添加到对象很不错。
oɔɯǝɹ

0

这里的一个选择,它使用一个类似的想法通过提到的PSTypeName溶液Jaykul(并因此还需要PSv3或以上)。

  1. 创建一个TypeName .Types.ps1xml文件定义您的类型。例如Person.Types.ps1xml
<?xml version="1.0" encoding="utf-8" ?>
<Types>
  <Type>
    <Name>StackOverflow.Example.Person</Name>
    <Members>
      <ScriptMethod>
        <Name>Initialize</Name>
        <Script>
            Param (
                [Parameter(Mandatory = $true)]
                [string]$GivenName
                ,
                [Parameter(Mandatory = $true)]
                [string]$Surname
            )
            $this | Add-Member -MemberType 'NoteProperty' -Name 'GivenName' -Value $GivenName
            $this | Add-Member -MemberType 'NoteProperty' -Name 'Surname' -Value $Surname
        </Script>
      </ScriptMethod>
      <ScriptMethod>
        <Name>SetGivenName</Name>
        <Script>
            Param (
                [Parameter(Mandatory = $true)]
                [string]$GivenName
            )
            $this | Add-Member -MemberType 'NoteProperty' -Name 'GivenName' -Value $GivenName -Force
        </Script>
      </ScriptMethod>
      <ScriptProperty>
        <Name>FullName</Name>
        <GetScriptBlock>'{0} {1}' -f $this.GivenName, $this.Surname</GetScriptBlock>
      </ScriptProperty>
      <!-- include properties under here if we don't want them to be visible by default
      <MemberSet>
        <Name>PSStandardMembers</Name>
        <Members>
        </Members>
      </MemberSet>
      -->
    </Members>
  </Type>
</Types>
  1. 导入您的类型: Update-TypeData -AppendPath .\Person.Types.ps1xml
  2. 创建您的自定义类型的对象: $p = [PSCustomType]@{PSTypeName='StackOverflow.Example.Person'}
  3. 使用您在XML中定义的脚本方法初始化类型: $p.Initialize('Anne', 'Droid')
  4. 看它; 您将看到所有定义的属性:$p | Format-Table -AutoSize
  5. 键入一个变量来更新属性值: $p.SetGivenName('Dan')
  6. 再次查看以查看更新的值: $p | Format-Table -AutoSize

说明

  • PS1XML文件允许您定义类型的自定义属性。
  • 正如文档所暗示的那样,它不限于.net类型。因此,您可以将自己喜欢的内容放在“ / Types / Type / Name”中,使用匹配的“ PSTypeName”创建的任何对象都将继承为此类型定义的成员。
  • 成员通过添加PS1XMLAdd-Member限制为NotePropertyAliasPropertyScriptPropertyCodePropertyScriptMethod,和CodeMethod(或PropertySet/ MemberSet;尽管这些都受到同样的限制)。所有这些属性都是只读的。
  • 通过定义a,ScriptMethod我们可以欺骗上述限制。例如,我们可以定义一个方法(例如Initialize)来创建新属性,并为我们设置它们的值;从而确保我们的对象具有其他脚本运行所需的所有属性。
  • 我们可以使用相同的技巧来允许属性可更新(尽管通过方法而不是直接分配),如示例所示SetGivenName

这种方法并非在所有情况下都理想。但对于将类类行为添加到自定义类型很有用/可以与其他答案中提到的其他方法结合使用。例如,在现实世界中,我可能只会FullName在PS1XML中定义属性,然后使用一个函数来创建具有所需值的对象,如下所示:

更多信息

查看文档或OOTB类型文件Get-Content $PSHome\types.ps1xml以获取启发。

# have something like this defined in my script so we only try to import the definition once.
# the surrounding if statement may be useful if we're dot sourcing the script in an existing 
# session / running in ISE / something like that
if (!(Get-TypeData 'StackOverflow.Example.Person')) {
    Update-TypeData '.\Person.Types.ps1xml'
}

# have a function to create my objects with all required parameters
# creating them from the hash table means they're PROPERties; i.e. updatable without calling a 
# setter method (note: recall I said above that in this scenario I'd remove their definition 
# from the PS1XML)
function New-SOPerson {
    [CmdletBinding()]
    [OutputType('StackOverflow.Example.Person')]
    Param (
        [Parameter(Mandatory)]
        [string]$GivenName
        ,
        [Parameter(Mandatory)]
        [string]$Surname
    )
    ([PSCustomObject][Ordered]@{
        PSTypeName = 'StackOverflow.Example.Person'
        GivenName = $GivenName
        Surname = $Surname
    })
}

# then use my new function to generate the new object
$p = New-SOPerson -GivenName 'Simon' -Surname 'Borg'

# and thanks to the type magic... FullName exists :)
Write-Information "$($p.FullName) was created successfully!" -InformationAction Continue

ps。对于使用VSCode的用户,您可以添加PS1XML支持:code.visualstudio.com/docs/languages/…–
JohnLBevan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.