about_Classes

Краткое описание

Описывает, как использовать классы для создания собственных пользовательских типов.

Подробное описание

Начиная с версии 5.0 PowerShell имеет формальный синтаксис для определения классов и других пользовательских типов. Добавление классов позволяет разработчикам и ИТ-специалистам принимать PowerShell для более широкого спектра вариантов использования.

Объявление класса — это схема, используемая для создания экземпляров объектов во время выполнения. При определении класса имя класса — это имя типа. Например, если вы объявляете класс с именем Device и инициализируете переменную $dev в новый экземпляр Device, $dev является объектом или экземпляром типа Device. Каждый экземпляр устройства может иметь разные значения в его свойствах.

Поддерживаемые сценарии

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

Синтаксис

Синтаксис определения

Определения классов используют следующий синтаксис:

class <class-name> [: [<base-class>][,<interface-list>]] {
    [[<attribute>] [hidden] [static] <property-definition> ...]
    [<class-name>([<constructor-argument-list>])
      {<constructor-statement-list>} ...]
    [[<attribute>] [hidden] [static] <method-definition> ...]
}

Синтаксис создания экземпляров

Чтобы создать экземпляр класса, используйте один из следующих синтаксисов:

[$<variable-name> =] New-Object -TypeName <class-name> [
  [-ArgumentList] <constructor-argument-list>]
[$<variable-name> =] [<class-name>]::new([<constructor-argument-list>])
[$<variable-name> =] [<class-name>]@{[<class-property-hashtable>]}

Примечание.

При использовании синтаксиса квадратные [<class-name>]::new() скобки вокруг имени класса являются обязательными. Квадратные скобки сигнализируют определение типа для PowerShell.

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

Примеры

Пример 1. Минимальное определение

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

class Device {
    [string]$Brand
}

$dev = [Device]::new()
$dev.Brand = "Fabrikam, Inc."
$dev
Brand
-----
Fabrikam, Inc.

Пример 2. Класс с элементами экземпляра

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

class Book {
    # Class properties
    [string]   $Title
    [string]   $Author
    [string]   $Synopsis
    [string]   $Publisher
    [datetime] $PublishDate
    [int]      $PageCount
    [string[]] $Tags
    # Default constructor
    Book() { $this.Init(@{}) }
    # Convenience constructor from hashtable
    Book([hashtable]$Properties) { $this.Init($Properties) }
    # Common constructor for title and author
    Book([string]$Title, [string]$Author) {
        $this.Init(@{Title = $Title; Author = $Author })
    }
    # Shared initializer method
    [void] Init([hashtable]$Properties) {
        foreach ($Property in $Properties.Keys) {
            $this.$Property = $Properties.$Property
        }
    }
    # Method to calculate reading time as 2 minutes per page
    [timespan] GetReadingTime() {
        if ($this.PageCount -le 0) {
            throw 'Unable to determine reading time from page count.'
        }
        $Minutes = $this.PageCount * 2
        return [timespan]::new(0, $Minutes, 0)
    }
    # Method to calculate how long ago a book was published
    [timespan] GetPublishedAge() {
        if (
            $null -eq $this.PublishDate -or
            $this.PublishDate -eq [datetime]::MinValue
        ) { throw 'PublishDate not defined' }

        return (Get-Date) - $this.PublishDate
    }
    # Method to return a string representation of the book
    [string] ToString() {
        return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))"
    }
}

В следующем фрагменте кода создается экземпляр класса и показано, как он работает. После создания экземпляра класса Book в примере используются GetReadingTime() методы и GetPublishedAge() методы для записи сообщения о книге.

$Book = [Book]::new(@{
    Title       = 'The Hobbit'
    Author      = 'J.R.R. Tolkien'
    Publisher   = 'George Allen & Unwin'
    PublishDate = '1937-09-21'
    PageCount   = 310
    Tags        = @('Fantasy', 'Adventure')
})

$Book
$Time = $Book.GetReadingTime()
$Time = @($Time.Hours, 'hours and', $Time.Minutes, 'minutes') -join ' '
$Age  = [Math]::Floor($Book.GetPublishedAge().TotalDays / 365.25)

"It takes $Time to read $Book,`nwhich was published $Age years ago."
Title       : The Hobbit
Author      : J.R.R. Tolkien
Synopsis    :
Publisher   : George Allen & Unwin
PublishDate : 9/21/1937 12:00:00 AM
PageCount   : 310
Tags        : {Fantasy, Adventure}

It takes 10 hours and 20 minutes to read The Hobbit by J.R.R. Tolkien (1937),
which was published 86 years ago.

Пример 3. Класс со статическими элементами

Класс BookList в этом примере основан на классе Book в примере 2. Хотя класс BookList не может быть помечен как статический, реализация определяет только статическое свойство Books и набор статических методов для управления этим свойством.

class BookList {
    # Static property to hold the list of books
    static [System.Collections.Generic.List[Book]] $Books
    # Static method to initialize the list of books. Called in the other
    # static methods to avoid needing to explicit initialize the value.
    static [void] Initialize()             { [BookList]::Initialize($false) }
    static [bool] Initialize([bool]$force) {
        if ([BookList]::Books.Count -gt 0 -and -not $force) {
            return $false
        }

        [BookList]::Books = [System.Collections.Generic.List[Book]]::new()

        return $true
    }
    # Ensure a book is valid for the list.
    static [void] Validate([book]$Book) {
        $Prefix = @(
            'Book validation failed: Book must be defined with the Title,'
            'Author, and PublishDate properties, but'
        ) -join ' '
        if ($null -eq $Book) { throw "$Prefix was null" }
        if ([string]::IsNullOrEmpty($Book.Title)) {
            throw "$Prefix Title wasn't defined"
        }
        if ([string]::IsNullOrEmpty($Book.Author)) {
            throw "$Prefix Author wasn't defined"
        }
        if ([datetime]::MinValue -eq $Book.PublishDate) {
            throw "$Prefix PublishDate wasn't defined"
        }
    }
    # Static methods to manage the list of books.
    # Add a book if it's not already in the list.
    static [void] Add([Book]$Book) {
        [BookList]::Initialize()
        [BookList]::Validate($Book)
        if ([BookList]::Books.Contains($Book)) {
            throw "Book '$Book' already in list"
        }

        $FindPredicate = {
            param([Book]$b)

            $b.Title -eq $Book.Title -and
            $b.Author -eq $Book.Author -and
            $b.PublishDate -eq $Book.PublishDate
        }.GetNewClosure()
        if ([BookList]::Books.Find($FindPredicate)) {
            throw "Book '$Book' already in list"
        }

        [BookList]::Books.Add($Book)
    }
    # Clear the list of books.
    static [void] Clear() {
      [BookList]::Initialize()
      [BookList]::Books.Clear()
    }
    # Find a specific book using a filtering scriptblock.
    static [Book] Find([scriptblock]$Predicate) {
        [BookList]::Initialize()
        return [BookList]::Books.Find($Predicate)
    }
    # Find every book matching the filtering scriptblock.
    static [Book[]] FindAll([scriptblock]$Predicate) {
        [BookList]::Initialize()
        return [BookList]::Books.FindAll($Predicate)
    }
    # Remove a specific book.
    static [void] Remove([Book]$Book) {
        [BookList]::Initialize()
        [BookList]::Books.Remove($Book)
    }
    # Remove a book by property value.
    static [void] RemoveBy([string]$Property, [string]$Value) {
        [BookList]::Initialize()
        $Index = [BookList]::Books.FindIndex({
            param($b)
            $b.$Property -eq $Value
        }.GetNewClosure())
        if ($Index -ge 0) {
            [BookList]::Books.RemoveAt($Index)
        }
    }
}

Теперь, когда bookList определен, книгу из предыдущего примера можно добавить в список.

$null -eq [BookList]::Books

[BookList]::Add($Book)

[BookList]::Books
True

Title       : The Hobbit
Author      : J.R.R. Tolkien
Synopsis    :
Publisher   : George Allen & Unwin
PublishDate : 9/21/1937 12:00:00 AM
PageCount   : 310
Tags        : {Fantasy, Adventure}

Следующий фрагмент кода вызывает статические методы для класса.

[BookList]::Add([Book]::new(@{
    Title       = 'The Fellowship of the Ring'
    Author      = 'J.R.R. Tolkien'
    Publisher   = 'George Allen & Unwin'
    PublishDate = '1954-07-29'
    PageCount   = 423
    Tags        = @('Fantasy', 'Adventure')
}))

[BookList]::Find({
    param ($b)

    $b.PublishDate -gt '1950-01-01'
}).Title

[BookList]::FindAll({
    param($b)

    $b.Author -match 'Tolkien'
}).Title

[BookList]::Remove($Book)
[BookList]::Books.Title

[BookList]::RemoveBy('Author', 'J.R.R. Tolkien')
"Titles: $([BookList]::Books.Title)"

[BookList]::Add($Book)
[BookList]::Add($Book)
The Fellowship of the Ring

The Hobbit
The Fellowship of the Ring

The Fellowship of the Ring

Titles:

Exception:
Line |
  84 |              throw "Book '$Book' already in list"
     |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Book 'The Hobbit by J.R.R. Tolkien (1937)' already in list

Пример 4. Параллельное выполнение повреждено пространство выполнения

Метод ShowRunspaceId() отчетов о [UnsafeClass] различных идентификаторах потока, но один и тот же идентификатор пространства выполнения. В конечном итоге состояние сеанса повреждено, что приводит к ошибке, например Global scope cannot be removed.

# Class definition with Runspace affinity (default behavior)
class UnsafeClass {
    static [object] ShowRunspaceId($val) {
        return [PSCustomObject]@{
            ThreadId   = [Threading.Thread]::CurrentThread.ManagedThreadId
            RunspaceId = [runspace]::DefaultRunspace.Id
        }
    }
}

$unsafe = [UnsafeClass]::new()

while ($true) {
    1..10 | ForEach-Object -Parallel {
        Start-Sleep -ms 100
        ($using:unsafe)::ShowRunspaceId($_)
    }
}

Примечание.

Этот пример выполняется в бесконечном цикле. Нажмите клавиши CTRL+C, чтобы остановить выполнение.

Свойства класса

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

Дополнительные сведения см. в about_Classes_Properties.

Методы класса

Методы определяют действия, которые может выполнить класс. Методы могут принимать параметры, указывающие входные данные. Методы всегда определяют тип вывода. Если метод не возвращает выходные данные, он должен иметь тип выходных данных Void . Если метод явно не определяет выходной тип, выходной тип метода — Void.

Дополнительные сведения см. в about_Classes_Methods.

Конструкторы классов

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

Дополнительные сведения см. в about_Classes_Constructors.

Скрытые ключевое слово

Ключевое слово hidden скрывает член класса. Элемент по-прежнему доступен пользователю и доступен во всех область, в которых доступен объект. Скрытые члены скрыты от командлета Get-Member и не могут отображаться с помощью завершения вкладки или IntelliSense за пределами определения класса.

Ключевое слово hidden применяется только к членам класса, а не к самому классу.

Скрытые члены класса:

  • Не включен в выходные данные по умолчанию для класса.
  • Не включен в список членов класса, возвращаемых командлетом Get-Member . Чтобы отобразить скрытые члены с Get-Memberпомощью параметра Force, используйте параметр Force .
  • Не отображается в завершении вкладки или IntelliSense, если только не выполняется завершение в классе, определяющем скрытый элемент.
  • Открытые члены класса. Они могут быть доступны, унаследованы и изменены. Скрытие члена не делает его частным. Он скрывает только элемент, как описано в предыдущих пунктах.

Примечание.

При скрытии перегрузки для метода этот метод удаляется из IntelliSense, результатов завершения и выходных данных по умолчанию.Get-Member При скрытии любого конструктора new() параметр удаляется из IntelliSense и результатов завершения.

Дополнительные сведения о ключевое слово см. в about_Hidden. Дополнительные сведения о скрытых свойствах см. в about_Classes_Properties. Дополнительные сведения о скрытых методах см. в about_Classes_Methods. Дополнительные сведения о скрытых конструкторах см. в about_Classes_Constructors.

Статические ключевое слово

Ключевое слово static определяет свойство или метод, который существует в классе и не нуждается в экземпляре.

Статическое свойство всегда доступно независимо от экземпляра класса. Статическое свойство предоставляется всем экземплярам класса. Статический метод доступен всегда. Все статические свойства живут для всего диапазона сеансов.

Ключевое слово static применяется только к членам класса, а не к самому классу.

Дополнительные сведения о статических свойствах см. в about_Classes_Properties. Дополнительные сведения о статических методах см. в about_Classes_Methods. Дополнительные сведения о статических конструкторах см. в about_Classes_Constructors.

Наследование в классах PowerShell

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

PowerShell не поддерживает несколько наследования. Классы не могут наследовать непосредственно от нескольких классов.

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

Дополнительные сведения о производных классах, наследующих от базового класса или реализующих интерфейсы, см. в about_Classes_Inheritance.

Сходство пространства runspace

Пространство выполнения — это операционная среда для команд, вызываемых PowerShell. Эта среда включает команды и данные, которые в настоящее время присутствуют, и все ограничения языка, которые в настоящее время применяются.

Класс PowerShell связан с пространством runspace , где он создан. Использование класса PowerShell в ForEach-Object -Parallel небезопасном режиме. Вызовы метода в классе маршалируются обратно в пространство Runspace , в котором он был создан, что может повредить состояние runspace или вызвать взаимоблокировку.

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

Экспорт классов с акселераторами типов

По умолчанию модули PowerShell не экспортируют классы и перечисления, определенные в PowerShell. Пользовательские типы недоступны за пределами модуля без вызова инструкции using module .

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

Примечание.

Добавление акселераторов типов в сеанс использует внутренний (не общедоступный) API. Использование этого API может вызвать конфликты. В приведенном ниже шаблоне возникает ошибка, если акселератор типов с тем же именем уже существует при импорте модуля. Он также удаляет акселераторы типов при удалении модуля из сеанса.

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

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

# Define the types to export with type accelerators.
$ExportableTypes =@(
    [DefinedTypeName]
)
# Get the internal TypeAccelerators class to use its static methods.
$TypeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)
# Ensure none of the types would clobber an existing type accelerator.
# If a type accelerator with the same name exists, throw an exception.
$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get
foreach ($Type in $ExportableTypes) {
    if ($Type.FullName -in $ExistingTypeAccelerators.Keys) {
        $Message = @(
            "Unable to register type accelerator '$($Type.FullName)'"
            'Accelerator already exists.'
        ) -join ' - '

        throw [System.Management.Automation.ErrorRecord]::new(
            [System.InvalidOperationException]::new($Message),
            'TypeAcceleratorAlreadyExists',
            [System.Management.Automation.ErrorCategory]::InvalidOperation,
            $Type.FullName
        )
    }
}
# Add type accelerators for every exportable type.
foreach ($Type in $ExportableTypes) {
    $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach($Type in $ExportableTypes) {
        $TypeAcceleratorsClass::Remove($Type.FullName)
    }
}.GetNewClosure()

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

Импорт классов вручную из модуля PowerShell

Import-Module#requires и оператор импортирует только функции модуля, псевдонимы и переменные, как определено модулем. Классы не импортируются.

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

Инструкция using module импортирует классы и перечисления из корневого модуля (ModuleToProcess) модуля скрипта или двоичного модуля. Он не последовательно импортирует классы, определенные в вложенных модулях или классах, определенных в скриптах, которые являются точками источника в корневом модуле. Определите классы, доступные пользователям за пределами модуля непосредственно в корневом модуле.

Дополнительные сведения об инструкции using см. в about_Using.

Загрузка нового измененного кода во время разработки

Во время разработки модуля скрипта обычно вносятся изменения в код, а затем загружается новая версия модуля с параметром Import-ModuleForce . Перезагрузка модуля работает только для изменений функций в корневом модуле. Import-Module не перезагрузит вложенные модули. Кроме того, нет способа загрузить обновленные классы.

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

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

Тип PSReference не поддерживается членами класса

[ref] Акселератор типов сокращен для класса PSReference. Использование [ref] для ввода элемента класса завершается автоматически. API, использующие [ref] параметры, нельзя использовать с членами класса. Класс PSReference был разработан для поддержки COM-объектов. ОБЪЕКТЫ COM имеют случаи, когда необходимо передать значение по ссылке.

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

Ограничения

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

Общие ограничения

  • Члены класса не могут использовать PSReference в качестве типа.

    Обходной путь: нет.

  • Классы PowerShell нельзя выгрузить или перезагрузить в сеансе.

    Обходное решение. Запуск нового сеанса.

  • Классы PowerShell, определенные в модуле, не импортируются автоматически.

    Обходное решение. Добавьте определенные типы в список акселераторов типов в корневом модуле. Это делает типы доступными для импорта модуля.

  • static И hidden ключевое слово применяются только к членам класса, а не к определению класса.

    Обходной путь: нет.

  • Классы PowerShell небезопасны для параллельного выполнения в пространствах выполнения. При вызове методов в классе PowerShell маршалирует вызовы обратно в пространство Runspace , в котором был создан класс, который может повредить состояние runspace или вызвать взаимоблокировку.

    Обходной путь: нет.

Ограничения конструктора

  • Цепочка конструкторов не реализована.

    Обходное решение. Определите скрытые Init() методы и вызовите их из конструкторов.

  • Параметры конструктора не могут использовать атрибуты, включая атрибуты проверки.

    Обходное решение. Переназначение параметров в тексте конструктора с помощью атрибута проверки.

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

    Обходной путь: нет.

  • Если любая перегрузка конструктора скрыта, каждая перегрузка конструктора обрабатывается как скрытая.

    Обходной путь: нет.

Ограничения методов

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

    Обходное решение. Переназначьте параметры в теле метода атрибутом проверки или определите метод в статическом конструкторе с помощью командлета Update-TypeData .

  • Параметры метода не могут определять значения по умолчанию. Параметры всегда обязательны.

    Обходное решение. Определите метод в статическом конструкторе с помощью командлета Update-TypeData .

  • Методы всегда открыты, даже если они скрыты. Их можно переопределить, когда класс наследуется.

    Обходной путь: нет.

  • Если любая перегрузка метода скрыта, каждая перегрузка этого метода обрабатывается как скрытая.

    Обходной путь: нет.

Ограничения свойств

  • Статические свойства всегда изменяются. Классы PowerShell не могут определять неизменяемые статические свойства.

    Обходной путь: нет.

  • Свойства не могут использовать атрибут ValidateScript , так как аргументы атрибута свойства класса должны быть константами.

    Обходное решение. Определите класс, наследующий от типа ValidateArgumentsAttribute , и используйте этот атрибут.

  • Непосредственно объявленные свойства не могут определять пользовательские реализации получения и задания.

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

  • Свойства не могут использовать атрибут Alias . Атрибут применяется только к параметрам, командлетам и функциям.

    Обходное Update-TypeData решение. Используйте командлет для определения псевдонимов в конструкторах классов.

  • При преобразовании класса PowerShell в JSON с помощью командлета ConvertTo-Json выходные данные JSON включают все скрытые свойства и их значения.

    Обходной путь: нет

Ограничения наследования

  • PowerShell не поддерживает определение интерфейсов в коде скрипта.

    Обходное решение. Определение интерфейсов в C# и ссылка на сборку, определяющую интерфейсы.

  • Классы PowerShell могут наследовать только от одного базового класса.

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

  • При наследовании от универсального класса или интерфейса параметр типа для универсального объекта должен быть уже определен. Класс не может определить себя как параметр типа для класса или интерфейса.

    Обходное решение. Чтобы получить производный от универсального базового класса или интерфейса, определите пользовательский тип в другом .psm1 файле и используйте using module инструкцию для загрузки типа. При наследовании от универсального типа не существует обходного решения для пользовательского типа.

См. также