about_Classes_Inheritance
Краткое описание
Описывает, как определить классы, расширяющие другие типы.
Подробное описание
Классы PowerShell поддерживают наследование, которое позволяет определить дочерний класс, который повторно использует (наследует), расширяет или изменяет поведение родительского класса. Класс, члены которого наследуются, называется базовым классом. Класс, который наследует члены базового класса, называется производным классом.
PowerShell поддерживает только одно наследование. Класс может наследовать только от одного класса. Но зато поддерживается транзитивное наследование, которое позволяет определить иерархию наследования для набора типов. Другими словами, тип D может наследовать от типа C, который наследует от типа B, который наследует от типа A базового класса. Так как наследование является транзитивным, члены типа A доступны для типа D.
Производные классы не наследуют все члены базового класса. Следующие элементы не наследуются:
- Статические конструкторы, которые инициализируют статические данные класса.
- Конструкторы экземпляров, которые вызываются для создания нового экземпляра класса. Каждый класс должен определять собственные конструкторы.
Можно расширить класс, создав новый класс, производный от существующего класса. Производный класс наследует свойства и методы базового класса. При необходимости можно добавить или переопределить члены базового класса.
Классы также могут наследоваться от интерфейсов, определяющих контракт. Класс, наследующий от интерфейса, должен реализовать этот контракт. Когда это делается, класс можно использовать, как и любой другой класс, реализующий этот интерфейс. Если класс наследует от интерфейса, но не реализует интерфейс, PowerShell вызывает ошибку синтаксического анализа для класса.
Некоторые операторы PowerShell зависят от класса, реализуемого определенным интерфейсом.
Например, оператор проверяет только равенство ссылок, -eq
если класс не реализует интерфейс System.IEquatable . Операторы -le
, -ge
-lt
и -gt
операторы работают только над классами, реализующими интерфейс System.IComparable.
Производный :
класс использует синтаксис для расширения базового класса или реализации интерфейсов. Производный класс всегда должен оставаться в объявлении класса.
В этом примере показан базовый синтаксис наследования классов PowerShell.
Class Derived : Base {...}
В этом примере показано наследование с объявлением интерфейса, поступающим после базового класса.
Class Derived : Base, Interface {...}
Синтаксис
Наследование класса использует следующие синтаксисы:
Один синтаксис строки
class <derived-class-name> : <base-class-or-interface-name>[, <interface-name>...] {
<derived-class-body>
}
Например:
# Base class only
class Derived : Base {...}
# Interface only
class Derived : System.IComparable {...}
# Base class and interface
class Derived : Base, System.IComparable {...}
Синтаксис с несколькими линиями
class <derived-class-name> : <base-class-or-interface-name>[,
<interface-name>...] {
<derived-class-body>
}
Например:
class Derived : Base,
System.IComparable,
System.IFormattable,
System.IConvertible {
# Derived class definition
}
Примеры
Пример 1. Наследование и переопределение из базового класса
В следующем примере показано поведение унаследованных свойств без переопределения. Запустите блоки кода в порядке после чтения их описания.
Определение базового класса
Первый блок кода определяет PublishedWork как базовый класс. Он имеет два статических свойства, List и Artists. Затем он определяет статический метод для добавления работ в статическое RegisterWork()
свойство List и художников в свойство Artists , записывая сообщение для каждой новой записи в списках.
Класс определяет три свойства экземпляра, описывающие опубликованную работу.
Наконец, он определяет Register()
методы и ToString()
методы экземпляра.
class PublishedWork {
static [PublishedWork[]] $List = @()
static [string[]] $Artists = @()
static [void] RegisterWork([PublishedWork]$Work) {
$wName = $Work.Name
$wArtist = $Work.Artist
if ($Work -notin [PublishedWork]::List) {
Write-Verbose "Adding work '$wName' to works list"
[PublishedWork]::List += $Work
} else {
Write-Verbose "Work '$wName' already registered."
}
if ($wArtist -notin [PublishedWork]::Artists) {
Write-Verbose "Adding artist '$wArtist' to artists list"
[PublishedWork]::Artists += $wArtist
} else {
Write-Verbose "Artist '$wArtist' already registered."
}
}
static [void] ClearRegistry() {
Write-Verbose "Clearing PublishedWork registry"
[PublishedWork]::List = @()
[PublishedWork]::Artists = @()
}
[string] $Name
[string] $Artist
[string] $Category
[void] Init([string]$WorkType) {
if ([string]::IsNullOrEmpty($this.Category)) {
$this.Category = "${WorkType}s"
}
}
PublishedWork() {
$WorkType = $this.GetType().FullName
$this.Init($WorkType)
Write-Verbose "Defined a published work of type [$WorkType]"
}
PublishedWork([string]$Name, [string]$Artist) {
$WorkType = $this.GetType().FullName
$this.Name = $Name
$this.Artist = $Artist
$this.Init($WorkType)
Write-Verbose "Defined '$Name' by $Artist as a published work of type [$WorkType]"
}
PublishedWork([string]$Name, [string]$Artist, [string]$Category) {
$WorkType = $this.GetType().FullName
$this.Name = $Name
$this.Artist = $Artist
$this.Init($WorkType)
Write-Verbose "Defined '$Name' by $Artist ($Category) as a published work of type [$WorkType]"
}
[void] Register() { [PublishedWork]::RegisterWork($this) }
[string] ToString() { return "$($this.Name) by $($this.Artist)" }
}
Определение производного класса без переопределения
Первый производный класс — Альбом. Он не переопределяет какие-либо свойства или методы. Он добавляет новое свойство экземпляра, "Жанры", которое не существует в базовом классе.
class Album : PublishedWork {
[string[]] $Genres = @()
}
В следующем блоке кода показано поведение производного класса Album .
Во-первых, он задает $VerbosePreference
таким образом, чтобы сообщения из методов класса выводились в консоль. Он создает три экземпляра класса, отображает их в таблице, а затем регистрирует их с помощью унаследованного статического RegisterWork()
метода. Затем он вызывает тот же статический метод непосредственно в базовом классе.
$VerbosePreference = 'Continue'
$Albums = @(
[Album]@{
Name = 'The Dark Side of the Moon'
Artist = 'Pink Floyd'
Genres = 'Progressive rock', 'Psychedelic rock'
}
[Album]@{
Name = 'The Wall'
Artist = 'Pink Floyd'
Genres = 'Progressive rock', 'Art rock'
}
[Album]@{
Name = '36 Chambers'
Artist = 'Wu-Tang Clan'
Genres = 'Hip hop'
}
)
$Albums | Format-Table
$Albums | ForEach-Object { [Album]::RegisterWork($_) }
$Albums | ForEach-Object { [PublishedWork]::RegisterWork($_) }
VERBOSE: Defined a published work of type [Album]
VERBOSE: Defined a published work of type [Album]
VERBOSE: Defined a published work of type [Album]
Genres Name Artist Category
------ ---- ------ --------
{Progressive rock, Psychedelic rock} The Dark Side of the Moon Pink Floyd Albums
{Progressive rock, Art rock} The Wall Pink Floyd Albums
{Hip hop} 36 Chambers Wu-Tang Clan Albums
VERBOSE: Adding work 'The Dark Side of the Moon' to works list
VERBOSE: Adding artist 'Pink Floyd' to artists list
VERBOSE: Adding work 'The Wall' to works list
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Adding work '36 Chambers' to works list
VERBOSE: Adding artist 'Wu-Tang Clan' to artists list
VERBOSE: Work 'The Dark Side of the Moon' already registered.
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Work 'The Wall' already registered.
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Work '36 Chambers' already registered.
VERBOSE: Artist 'Wu-Tang Clan' already registered.
Обратите внимание, что несмотря на то, что класс Album не определил значение для категорий или конструкторов, свойство было определено конструктором базового класса по умолчанию.
В подробном обмене сообщениями второй вызов RegisterWork()
метода сообщает, что работы и художники уже зарегистрированы. Несмотря на то, что первый вызов RegisterWork()
был для производного класса Album , он использовал унаследованный статический метод из базового класса PublishedWork . Этот метод обновил статические свойства List и Artist в базовом классе, который производный класс не переопределяет.
Следующий блок кода очищает реестр и вызывает метод экземпляра Register()
для объектов Album .
[PublishedWork]::ClearRegistry()
$Albums.Register()
VERBOSE: Clearing PublishedWork registry
VERBOSE: Adding work 'The Dark Side of the Moon' to works list
VERBOSE: Adding artist 'Pink Floyd' to artists list
VERBOSE: Adding work 'The Wall' to works list
VERBOSE: Artist 'Pink Floyd' already registered.
VERBOSE: Adding work '36 Chambers' to works list
VERBOSE: Adding artist 'Wu-Tang Clan' to artists list
Метод экземпляра в объектах Album имеет тот же эффект, что и вызов статического метода в производном или базовом классе.
Следующий блок кода сравнивает статические свойства базового класса и производный класс, показывающий, что они одинаковы.
[pscustomobject]@{
'[PublishedWork]::List' = [PublishedWork]::List -join ",`n"
'[Album]::List' = [Album]::List -join ",`n"
'[PublishedWork]::Artists' = [PublishedWork]::Artists -join ",`n"
'[Album]::Artists' = [Album]::Artists -join ",`n"
'IsSame::List' = (
[PublishedWork]::List.Count -eq [Album]::List.Count -and
[PublishedWork]::List.ToString() -eq [Album]::List.ToString()
)
'IsSame::Artists' = (
[PublishedWork]::Artists.Count -eq [Album]::Artists.Count -and
[PublishedWork]::Artists.ToString() -eq [Album]::Artists.ToString()
)
} | Format-List
[PublishedWork]::List : The Dark Side of the Moon by Pink Floyd,
The Wall by Pink Floyd,
36 Chambers by Wu-Tang Clan
[Album]::List : The Dark Side of the Moon by Pink Floyd,
The Wall by Pink Floyd,
36 Chambers by Wu-Tang Clan
[PublishedWork]::Artists : Pink Floyd,
Wu-Tang Clan
[Album]::Artists : Pink Floyd,
Wu-Tang Clan
IsSame::List : True
IsSame::Artists : True
Определение производного класса с переопределениями
Следующий блок кода определяет класс Иллюстрации , наследуемый от базового класса PublishedWork . Новый класс расширяет базовый класс путем определения свойства среднего экземпляра со значением Unknown
по умолчанию.
В отличие от класса производного альбома , Иллюстрация переопределяет следующие свойства и методы:
- Он переопределяет свойство static Artists . Определение совпадает, но класс Иллюстрации объявляет его напрямую.
- Он переопределяет свойство экземпляра категории , задав значение
Illustrations
по умолчанию. - Он переопределяет
ToString()
метод экземпляра, поэтому строковое представление иллюстрации включает в себя среду, с помощью которую он был создан.
Класс также определяет статический метод, чтобы сначала вызвать метод базового классаRegisterWork()
, а затем добавить художника в переопределенное статичное RegisterIllustration()
свойство Artists в производном классе.
Наконец, класс переопределяет все три конструктора:
- Конструктор по умолчанию пуст, за исключением подробного сообщения, указывающего, что он создал иллюстрацию.
- Следующий конструктор принимает два строковых значения для имени и художника, создавшего иллюстрацию. Вместо реализации логики настройки свойств Name и Artist конструктор вызывает соответствующий конструктор из базового класса.
- Последний конструктор принимает три строковых значения для имени, художника и носителя иллюстрации. Оба конструктора пишут подробное сообщение, указывающее, что они создали иллюстрацию.
class Illustration : PublishedWork {
static [string[]] $Artists = @()
static [void] RegisterIllustration([Illustration]$Work) {
$wArtist = $Work.Artist
[PublishedWork]::RegisterWork($Work)
if ($wArtist -notin [Illustration]::Artists) {
Write-Verbose "Adding illustrator '$wArtist' to artists list"
[Illustration]::Artists += $wArtist
} else {
Write-Verbose "Illustrator '$wArtist' already registered."
}
}
[string] $Category = 'Illustrations'
[string] $Medium = 'Unknown'
[string] ToString() {
return "$($this.Name) by $($this.Artist) ($($this.Medium))"
}
Illustration() {
Write-Verbose 'Defined an illustration'
}
Illustration([string]$Name, [string]$Artist) : base($Name, $Artist) {
Write-Verbose "Defined '$Name' by $Artist ($($this.Medium)) as an illustration"
}
Illustration([string]$Name, [string]$Artist, [string]$Medium) {
$this.Name = $Name
$this.Artist = $Artist
$this.Medium = $Medium
Write-Verbose "Defined '$Name' by $Artist ($Medium) as an illustration"
}
}
В следующем блоке кода показано поведение производного класса Иллюстрации . Он создает три экземпляра класса, отображает их в таблице, а затем регистрирует их с помощью унаследованного статического RegisterWork()
метода. Затем он вызывает тот же статический метод непосредственно в базовом классе. Наконец, он записывает сообщения, показывающие список зарегистрированных художников для базового класса и производного класса.
$Illustrations = @(
[Illustration]@{
Name = 'The Funny Thing'
Artist = 'Wanda Gág'
Medium = 'Lithography'
}
[Illustration]::new('Millions of Cats', 'Wanda Gág')
[Illustration]::new(
'The Lion and the Mouse',
'Jerry Pinkney',
'Watercolor'
)
)
$Illustrations | Format-Table
$Illustrations | ForEach-Object { [Illustration]::RegisterIllustration($_) }
$Illustrations | ForEach-Object { [PublishedWork]::RegisterWork($_) }
"Published work artists: $([PublishedWork]::Artists -join ', ')"
"Illustration artists: $([Illustration]::Artists -join ', ')"
VERBOSE: Defined a published work of type [Illustration]
VERBOSE: Defined an illustration
VERBOSE: Defined 'Millions of Cats' by Wanda Gág as a published work of type [Illustration]
VERBOSE: Defined 'Millions of Cats' by Wanda Gág (Unknown) as an illustration
VERBOSE: Defined a published work of type [Illustration]
VERBOSE: Defined 'The Lion and the Mouse' by Jerry Pinkney (Watercolor) as an illustration
Category Medium Name Artist
-------- ------ ---- ------
Illustrations Lithography The Funny Thing Wanda Gág
Illustrations Unknown Millions of Cats Wanda Gág
Illustrations Watercolor The Lion and the Mouse Jerry Pinkney
VERBOSE: Adding work 'The Funny Thing' to works list
VERBOSE: Adding artist 'Wanda Gág' to artists list
VERBOSE: Adding illustrator 'Wanda Gág' to artists list
VERBOSE: Adding work 'Millions of Cats' to works list
VERBOSE: Artist 'Wanda Gág' already registered.
VERBOSE: Illustrator 'Wanda Gág' already registered.
VERBOSE: Adding work 'The Lion and the Mouse' to works list
VERBOSE: Adding artist 'Jerry Pinkney' to artists list
VERBOSE: Adding illustrator 'Jerry Pinkney' to artists list
VERBOSE: Work 'The Funny Thing' already registered.
VERBOSE: Artist 'Wanda Gág' already registered.
VERBOSE: Work 'Millions of Cats' already registered.
VERBOSE: Artist 'Wanda Gág' already registered.
VERBOSE: Work 'The Lion and the Mouse' already registered.
VERBOSE: Artist 'Jerry Pinkney' already registered.
Published work artists: Pink Floyd, Wu-Tang Clan, Wanda Gág, Jerry Pinkney
Illustration artists: Wanda Gág, Jerry Pinkney
Подробные сообщения из создания экземпляров показывают, что:
- При создании первого экземпляра конструктор базового класса по умолчанию был вызван до конструктора производного класса по умолчанию.
- При создании второго экземпляра явным образом наследуемый конструктор был вызван для базового класса до конструктора производного класса.
- При создании третьего экземпляра конструктор базового класса по умолчанию был вызван до конструктора производного класса.
Подробные сообщения из RegisterWork()
метода указывают, что работы и художники уже зарегистрированы. Это связано с тем, что RegisterIllustration()
метод, который RegisterWork()
вызывается внутри метода.
Однако при сравнении значения статического свойства Artist для базового класса и производного класса значения отличаются. Свойство Artists для производного класса включает только иллюстраторов, а не художников альбомов. Переопределяя свойство Artist в производном классе, не позволяет классу возвращать статическое свойство базового класса.
Окончательный блок кода вызывает ToString()
метод для записей статического свойства List в базовом классе.
[PublishedWork]::List | ForEach-Object -Process { $_.ToString() }
The Dark Side of the Moon by Pink Floyd
The Wall by Pink Floyd
36 Chambers by Wu-Tang Clan
The Funny Thing by Wanda Gág (Lithography)
Millions of Cats by Wanda Gág (Unknown)
The Lion and the Mouse by Jerry Pinkney (Watercolor)
Экземпляры альбома возвращают только имя и художник в их строке. Экземпляры Иллюстрации также включали средний в скобки, так как этот класс переопределяет ToString()
метод.
Пример 2. Реализация интерфейсов
В следующем примере показано, как класс может реализовать один или несколько интерфейсов. В примере расширяется определение класса Temperature для поддержки дополнительных операций и поведения.
Определение начального класса
Перед реализацией любых интерфейсов класс "Температура" определяется с двумя свойствами, градусами и шкалой. Он определяет конструкторы и три метода экземпляра для возврата экземпляра в виде степени определенного масштаба.
Класс определяет доступные шкалы с перечислением TemperatureScale .
class Temperature {
[float] $Degrees
[TemperatureScale] $Scale
Temperature() {}
Temperature([float] $Degrees) { $this.Degrees = $Degrees }
Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
Temperature([float] $Degrees, [TemperatureScale] $Scale) {
$this.Degrees = $Degrees
$this.Scale = $Scale
}
[float] ToKelvin() {
switch ($this.Scale) {
Celsius { return $this.Degrees + 273.15 }
Fahrenheit { return ($this.Degrees + 459.67) * 5/9 }
}
return $this.Degrees
}
[float] ToCelsius() {
switch ($this.Scale) {
Fahrenheit { return ($this.Degrees - 32) * 5/9 }
Kelvin { return $this.Degrees - 273.15 }
}
return $this.Degrees
}
[float] ToFahrenheit() {
switch ($this.Scale) {
Celsius { return $this.Degrees * 9/5 + 32 }
Kelvin { return $this.Degrees * 9/5 - 459.67 }
}
return $this.Degrees
}
}
enum TemperatureScale {
Celsius = 0
Fahrenheit = 1
Kelvin = 2
}
Однако в этой базовой реализации существует несколько ограничений, как показано в следующем примере выходных данных:
$Celsius = [Temperature]::new()
$Fahrenheit = [Temperature]::new([TemperatureScale]::Fahrenheit)
$Kelvin = [Temperature]::new(0, 'Kelvin')
$Celsius, $Fahrenheit, $Kelvin
"The temperatures are: $Celsius, $Fahrenheit, $Kelvin"
[Temperature]::new() -eq $Celsius
$Celsius -gt $Kelvin
Degrees Scale
------- -----
0.00 Celsius
0.00 Fahrenheit
0.00 Kelvin
The temperatures are: Temperature, Temperature, Temperature
False
InvalidOperation:
Line |
11 | $Celsius -gt $Kelvin
| ~~~~~~~~~~~~~~~~~~~~
| Cannot compare "Temperature" because it is not IComparable.
Выходные данные показывают, что экземпляры температуры:
- Неправильно отображаться в виде строк.
- Не удается правильно проверить эквивалентность.
- Не удается сравнить.
Эти три проблемы можно решить, реализуя интерфейсы для класса.
Реализация IFormattable
Первый интерфейс для класса Temperature — System.IFormattable. Этот интерфейс позволяет отформатировать экземпляр класса в виде разных строк. Для реализации интерфейса класс должен наследоваться от System.IFormattable и определить метод экземпляра ToString()
.
Метод экземпляра ToString()
должен иметь следующую подпись:
[string] ToString(
[string]$Format,
[System.IFormatProvider]$FormatProvider
) {
# Implementation
}
Подпись, которую требует интерфейс, указана в справочной документации.
Для температуры класс должен поддерживать три формата: C
вернуть экземпляр в Цельсии, F
вернуть его в Фаренхейте, и K
вернуть его в Келвине. Для любого другого формата метод должен вызывать System.FormatException.
[string] ToString(
[string]$Format,
[System.IFormatProvider]$FormatProvider
) {
# If format isn't specified, use the defined scale.
if ([string]::IsNullOrEmpty($Format)) {
$Format = switch ($this.Scale) {
Celsius { 'C' }
Fahrenheit { 'F' }
Kelvin { 'K' }
}
}
# If format provider isn't specified, use the current culture.
if ($null -eq $FormatProvider) {
$FormatProvider = [CultureInfo]::CurrentCulture
}
# Format the temperature.
switch ($Format) {
'C' {
return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
}
'F' {
return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
}
'K' {
return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
}
}
# If we get here, the format is invalid.
throw [System.FormatException]::new(
"Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
)
}
В этой реализации метод по умолчанию использует масштаб экземпляра для формата и текущего языка и региональных параметров при форматировании самого значения числовой степени. Он использует To<Scale>()
методы экземпляра для преобразования градусов, форматирует их в двух десятичные разряды и добавляет соответствующий символ градуса в строку.
При реализации требуемой сигнатуры класс также может определять перегрузки, чтобы упростить возврат отформатированного экземпляра.
[string] ToString([string]$Format) {
return $this.ToString($Format, $null)
}
[string] ToString() {
return $this.ToString($null, $null)
}
В следующем коде показано обновленное определение температуры:
class Temperature : System.IFormattable {
[float] $Degrees
[TemperatureScale] $Scale
Temperature() {}
Temperature([float] $Degrees) { $this.Degrees = $Degrees }
Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
Temperature([float] $Degrees, [TemperatureScale] $Scale) {
$this.Degrees = $Degrees
$this.Scale = $Scale
}
[float] ToKelvin() {
switch ($this.Scale) {
Celsius { return $this.Degrees + 273.15 }
Fahrenheit { return ($this.Degrees + 459.67) * 5 / 9 }
}
return $this.Degrees
}
[float] ToCelsius() {
switch ($this.Scale) {
Fahrenheit { return ($this.Degrees - 32) * 5 / 9 }
Kelvin { return $this.Degrees - 273.15 }
}
return $this.Degrees
}
[float] ToFahrenheit() {
switch ($this.Scale) {
Celsius { return $this.Degrees * 9 / 5 + 32 }
Kelvin { return $this.Degrees * 9 / 5 - 459.67 }
}
return $this.Degrees
}
[string] ToString(
[string]$Format,
[System.IFormatProvider]$FormatProvider
) {
# If format isn't specified, use the defined scale.
if ([string]::IsNullOrEmpty($Format)) {
$Format = switch ($this.Scale) {
Celsius { 'C' }
Fahrenheit { 'F' }
Kelvin { 'K' }
}
}
# If format provider isn't specified, use the current culture.
if ($null -eq $FormatProvider) {
$FormatProvider = [CultureInfo]::CurrentCulture
}
# Format the temperature.
switch ($Format) {
'C' {
return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
}
'F' {
return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
}
'K' {
return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
}
}
# If we get here, the format is invalid.
throw [System.FormatException]::new(
"Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
)
}
[string] ToString([string]$Format) {
return $this.ToString($Format, $null)
}
[string] ToString() {
return $this.ToString($null, $null)
}
}
enum TemperatureScale {
Celsius = 0
Fahrenheit = 1
Kelvin = 2
}
Выходные данные для перегрузки метода отображаются в следующем блоке.
$Temp = [Temperature]::new()
"The temperature is $Temp"
$Temp.ToString()
$Temp.ToString('K')
$Temp.ToString('F', $null)
The temperature is 0.00°C
0.00°C
273.15°K
32.00°F
Реализация IEquatable
Теперь, когда класс Temperature может быть отформатирован для удобочитаемости, пользователи должны иметь возможность проверить, равны ли два экземпляра класса. Для поддержки этого теста класс должен реализовать интерфейс System.IEquatable .
Чтобы реализовать интерфейс, класс должен наследоваться от System.IEquatable и определить метод экземпляра Equals()
. Метод Equals()
должен иметь следующую подпись:
[bool] Equals([object]$Other) {
# Implementation
}
Подпись, которую требует интерфейс, указана в справочной документации.
Для температуры класс должен поддерживать только сравнение двух экземпляров класса. Для любого другого значения или типа, включая $null
возвращаемое $false
значение. При сравнении двух температур метод должен преобразовывать оба значения в Кельвин, так как температура может быть эквивалентна даже с разными шкалами.
[bool] Equals([object]$Other) {
# If the other object is null, we can't compare it.
if ($null -eq $Other) {
return $false
}
# If the other object isn't a temperature, we can't compare it.
$OtherTemperature = $Other -as [Temperature]
if ($null -eq $OtherTemperature) {
return $false
}
# Compare the temperatures as Kelvin.
return $this.ToKelvin() -eq $OtherTemperature.ToKelvin()
}
При реализации метода интерфейса обновлено определение для temperature :
class Temperature : System.IFormattable, System.IEquatable[object] {
[float] $Degrees
[TemperatureScale] $Scale
Temperature() {}
Temperature([float] $Degrees) { $this.Degrees = $Degrees }
Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
Temperature([float] $Degrees, [TemperatureScale] $Scale) {
$this.Degrees = $Degrees
$this.Scale = $Scale
}
[float] ToKelvin() {
switch ($this.Scale) {
Celsius { return $this.Degrees + 273.15 }
Fahrenheit { return ($this.Degrees + 459.67) * 5 / 9 }
}
return $this.Degrees
}
[float] ToCelsius() {
switch ($this.Scale) {
Fahrenheit { return ($this.Degrees - 32) * 5 / 9 }
Kelvin { return $this.Degrees - 273.15 }
}
return $this.Degrees
}
[float] ToFahrenheit() {
switch ($this.Scale) {
Celsius { return $this.Degrees * 9 / 5 + 32 }
Kelvin { return $this.Degrees * 9 / 5 - 459.67 }
}
return $this.Degrees
}
[string] ToString(
[string]$Format,
[System.IFormatProvider]$FormatProvider
) {
# If format isn't specified, use the defined scale.
if ([string]::IsNullOrEmpty($Format)) {
$Format = switch ($this.Scale) {
Celsius { 'C' }
Fahrenheit { 'F' }
Kelvin { 'K' }
}
}
# If format provider isn't specified, use the current culture.
if ($null -eq $FormatProvider) {
$FormatProvider = [CultureInfo]::CurrentCulture
}
# Format the temperature.
switch ($Format) {
'C' {
return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
}
'F' {
return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
}
'K' {
return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
}
}
# If we get here, the format is invalid.
throw [System.FormatException]::new(
"Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
)
}
[string] ToString([string]$Format) {
return $this.ToString($Format, $null)
}
[string] ToString() {
return $this.ToString($null, $null)
}
[bool] Equals([object]$Other) {
# If the other object is null, we can't compare it.
if ($null -eq $Other) {
return $false
}
# If the other object isn't a temperature, we can't compare it.
$OtherTemperature = $Other -as [Temperature]
if ($null -eq $OtherTemperature) {
return $false
}
# Compare the temperatures as Kelvin.
return $this.ToKelvin() -eq $OtherTemperature.ToKelvin()
}
}
enum TemperatureScale {
Celsius = 0
Fahrenheit = 1
Kelvin = 2
}
В следующем блоке показано, как работает обновленный класс:
$Celsius = [Temperature]::new()
$Fahrenheit = [Temperature]::new(32, 'Fahrenheit')
$Kelvin = [Temperature]::new([TemperatureScale]::Kelvin)
@"
Temperatures are: $Celsius, $Fahrenheit, $Kelvin
`$Celsius.Equals(`$Fahrenheit) = $($Celsius.Equals($Fahrenheit))
`$Celsius -eq `$Fahrenheit = $($Celsius -eq $Fahrenheit)
`$Celsius -ne `$Kelvin = $($Celsius -ne $Kelvin)
"@
Temperatures are: 0.00°C, 32.00°F, 0.00°K
$Celsius.Equals($Fahrenheit) = True
$Celsius -eq $Fahrenheit = True
$Celsius -ne $Kelvin = True
Реализация IComparable
Последний интерфейс для класса Temperature — System.IComparable. Когда класс реализует этот интерфейс, пользователи могут использовать -lt
-le
операторы , -gt
а также -ge
операторы для сравнения экземпляров класса.
Чтобы реализовать интерфейс, класс должен наследоваться от System.IComparable и определить метод экземпляра Equals()
. Метод Equals()
должен иметь следующую подпись:
[int] CompareTo([Object]$Other) {
# Implementation
}
Подпись, которую требует интерфейс, указана в справочной документации.
Для температуры класс должен поддерживать только сравнение двух экземпляров класса. Так как базовый тип свойства Degrees , даже при преобразовании в другой масштаб, является числом с плавающей запятой, метод может полагаться на базовый тип для фактического сравнения.
[int] CompareTo([object]$Other) {
# If the other object's null, consider this instance "greater than" it
if ($null -eq $Other) {
return 1
}
# If the other object isn't a temperature, we can't compare it.
$OtherTemperature = $Other -as [Temperature]
if ($null -eq $OtherTemperature) {
throw [System.ArgumentException]::new(
"Object must be of type 'Temperature'."
)
}
# Compare the temperatures as Kelvin.
return $this.ToKelvin().CompareTo($OtherTemperature.ToKelvin())
}
Окончательное определение класса Temperature :
class Temperature : System.IFormattable,
System.IComparable,
System.IEquatable[object] {
# Instance properties
[float] $Degrees
[TemperatureScale] $Scale
# Constructors
Temperature() {}
Temperature([float] $Degrees) { $this.Degrees = $Degrees }
Temperature([TemperatureScale] $Scale) { $this.Scale = $Scale }
Temperature([float] $Degrees, [TemperatureScale] $Scale) {
$this.Degrees = $Degrees
$this.Scale = $Scale
}
[float] ToKelvin() {
switch ($this.Scale) {
Celsius { return $this.Degrees + 273.15 }
Fahrenheit { return ($this.Degrees + 459.67) * 5 / 9 }
}
return $this.Degrees
}
[float] ToCelsius() {
switch ($this.Scale) {
Fahrenheit { return ($this.Degrees - 32) * 5 / 9 }
Kelvin { return $this.Degrees - 273.15 }
}
return $this.Degrees
}
[float] ToFahrenheit() {
switch ($this.Scale) {
Celsius { return $this.Degrees * 9 / 5 + 32 }
Kelvin { return $this.Degrees * 9 / 5 - 459.67 }
}
return $this.Degrees
}
[string] ToString(
[string]$Format,
[System.IFormatProvider]$FormatProvider
) {
# If format isn't specified, use the defined scale.
if ([string]::IsNullOrEmpty($Format)) {
$Format = switch ($this.Scale) {
Celsius { 'C' }
Fahrenheit { 'F' }
Kelvin { 'K' }
}
}
# If format provider isn't specified, use the current culture.
if ($null -eq $FormatProvider) {
$FormatProvider = [CultureInfo]::CurrentCulture
}
# Format the temperature.
switch ($Format) {
'C' {
return $this.ToCelsius().ToString('F2', $FormatProvider) + '°C'
}
'F' {
return $this.ToFahrenheit().ToString('F2', $FormatProvider) + '°F'
}
'K' {
return $this.ToKelvin().ToString('F2', $FormatProvider) + '°K'
}
}
# If we get here, the format is invalid.
throw [System.FormatException]::new(
"Unknown format: '$Format'. Valid Formats are 'C', 'F', and 'K'"
)
}
[string] ToString([string]$Format) {
return $this.ToString($Format, $null)
}
[string] ToString() {
return $this.ToString($null, $null)
}
[bool] Equals([object]$Other) {
# If the other object is null, we can't compare it.
if ($null -eq $Other) {
return $false
}
# If the other object isn't a temperature, we can't compare it.
$OtherTemperature = $Other -as [Temperature]
if ($null -eq $OtherTemperature) {
return $false
}
# Compare the temperatures as Kelvin.
return $this.ToKelvin() -eq $OtherTemperature.ToKelvin()
}
[int] CompareTo([object]$Other) {
# If the other object's null, consider this instance "greater than" it
if ($null -eq $Other) {
return 1
}
# If the other object isn't a temperature, we can't compare it.
$OtherTemperature = $Other -as [Temperature]
if ($null -eq $OtherTemperature) {
throw [System.ArgumentException]::new(
"Object must be of type 'Temperature'."
)
}
# Compare the temperatures as Kelvin.
return $this.ToKelvin().CompareTo($OtherTemperature.ToKelvin())
}
}
enum TemperatureScale {
Celsius = 0
Fahrenheit = 1
Kelvin = 2
}
С полным определением пользователи могут форматировать и сравнивать экземпляры класса в PowerShell, как любой встроенный тип.
$Celsius = [Temperature]::new()
$Fahrenheit = [Temperature]::new(32, 'Fahrenheit')
$Kelvin = [Temperature]::new([TemperatureScale]::Kelvin)
@"
Temperatures are: $Celsius, $Fahrenheit, $Kelvin
`$Celsius.Equals(`$Fahrenheit) = $($Celsius.Equals($Fahrenheit))
`$Celsius.Equals(`$Kelvin) = $($Celsius.Equals($Kelvin))
`$Celsius.CompareTo(`$Fahrenheit) = $($Celsius.CompareTo($Fahrenheit))
`$Celsius.CompareTo(`$Kelvin) = $($Celsius.CompareTo($Kelvin))
`$Celsius -lt `$Fahrenheit = $($Celsius -lt $Fahrenheit)
`$Celsius -le `$Fahrenheit = $($Celsius -le $Fahrenheit)
`$Celsius -eq `$Fahrenheit = $($Celsius -eq $Fahrenheit)
`$Celsius -gt `$Kelvin = $($Celsius -gt $Kelvin)
"@
Temperatures are: 0.00°C, 32.00°F, 0.00°K
$Celsius.Equals($Fahrenheit) = True
$Celsius.Equals($Kelvin) = False
$Celsius.CompareTo($Fahrenheit) = 0
$Celsius.CompareTo($Kelvin) = 1
$Celsius -lt $Fahrenheit = False
$Celsius -le $Fahrenheit = True
$Celsius -eq $Fahrenheit = True
$Celsius -gt $Kelvin = True
Пример 3. Наследование от универсального базового класса
В этом примере показано, как можно наследить универсальный класс, например System.Collections.Generic.List.
Использование встроенного класса в качестве параметра типа
Выполните следующий блок кода. В нем показано, как новый класс может наследовать от универсального типа, пока параметр типа уже определен во время синтаксического анализа.
class ExampleStringList : System.Collections.Generic.List[string] {}
$List = [ExampleStringList]::New()
$List.AddRange([string[]]@('a','b','c'))
$List.GetType() | Format-List -Property Name, BaseType
$List
Name : ExampleStringList
BaseType : System.Collections.Generic.List`1[System.String]
a
b
c
Использование настраиваемого класса в качестве параметра типа
Следующий блок кода сначала определяет новый класс ExampleItem с одним свойством экземпляра и методом ToString()
. Затем он определяет класс ExampleItemList, наследующий от базового класса System.Collections.Generic.List с ExampleItem в качестве параметра типа.
Скопируйте весь блок кода и запустите его как одну инструкцию.
class ExampleItem {
[string] $Name
[string] ToString() { return $this.Name }
}
class ExampleItemList : System.Collections.Generic.List[ExampleItem] {}
ParentContainsErrorRecordException: An error occurred while creating the pipeline.
Выполнение всего блока кода вызывает ошибку, так как PowerShell еще не загружает класс ExampleItem в среду выполнения. Вы еще не можете использовать имя класса в качестве параметра типа для базового класса System.Collections.Generic.List .
Выполните следующие блоки кода в том порядке, в который они определены.
class ExampleItem {
[string] $Name
[string] ToString() { return $this.Name }
}
class ExampleItemList : System.Collections.Generic.List[ExampleItem] {}
На этот раз PowerShell не вызывает никаких ошибок. Теперь оба класса определены. Выполните следующий блок кода, чтобы просмотреть поведение нового класса.
$List = [ExampleItemList]::New()
$List.AddRange([ExampleItem[]]@(
[ExampleItem]@{ Name = 'Foo' }
[ExampleItem]@{ Name = 'Bar' }
[ExampleItem]@{ Name = 'Baz' }
))
$List.GetType() | Format-List -Property Name, BaseType
$List
Name : ExampleItemList
BaseType : System.Collections.Generic.List`1[ExampleItem]
Name
----
Foo
Bar
Baz
Создание универсального шаблона с параметром пользовательского типа в модуле
В следующих блоках кода показано, как определить класс, наследующий от универсального базового класса, использующего настраиваемый тип для параметра типа.
Сохраните следующий блок кода как GenericExample.psd1
.
@{
RootModule = 'GenericExample.psm1'
ModuleVersion = '0.1.0'
GUID = '2779fa60-0b3b-4236-b592-9060c0661ac2'
}
Сохраните следующий блок кода как GenericExample.InventoryItem.psm1
.
class InventoryItem {
[string] $Name
[int] $Count
InventoryItem() {}
InventoryItem([string]$Name) {
$this.Name = $Name
}
InventoryItem([string]$Name, [int]$Count) {
$this.Name = $Name
$this.Count = $Count
}
[string] ToString() {
return "$($this.Name) ($($this.Count))"
}
}
Сохраните следующий блок кода как GenericExample.psm1
.
using namespace System.Collections.Generic
using module ./GenericExample.InventoryItem.psm1
class Inventory : List[InventoryItem] {}
# Define the types to export with type accelerators.
$ExportableTypes =@(
[InventoryItem]
[Inventory]
)
# 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()
Совет
Корневой модуль добавляет пользовательские типы в акселераторы типов PowerShell. Этот шаблон позволяет пользователям модуля немедленно получить доступ к IntelliSense и автозавершение для пользовательских типов без необходимости использовать инструкцию using module
.
Дополнительные сведения об этом шаблоне см. в разделе "Экспорт с акселераторами типов" about_Classes.
Импортируйте модуль и проверьте выходные данные.
Import-Module ./GenericExample.psd1
$Inventory = [Inventory]::new()
$Inventory.GetType() | Format-List -Property Name, BaseType
$Inventory.Add([InventoryItem]::new('Bucket', 2))
$Inventory.Add([InventoryItem]::new('Mop'))
$Inventory.Add([InventoryItem]@{ Name = 'Broom' ; Count = 4 })
$Inventory
Name : Inventory
BaseType : System.Collections.Generic.List`1[InventoryItem]
Name Count
---- -----
Bucket 2
Mop 0
Broom 4
Модуль загружается без ошибок, так как класс InventoryItem определен в файле модуля, отличном от класса Inventory . Оба класса доступны пользователям модуля.
Наследование базового класса
Когда класс наследует от базового класса, он наследует свойства и методы базового класса. Он не наследует конструкторы базового класса напрямую, но он может вызывать их.
Если базовый класс определен в .NET, а не в PowerShell, обратите внимание:
- Классы PowerShell не могут наследоваться от запечатанных классов.
- При наследовании от универсального базового класса параметр типа для универсального класса не может быть производным. Использование производного класса в качестве параметра типа вызывает ошибку синтаксического анализа.
Сведения о том, как работает наследование и переопределение для производных классов, см . в примере 1.
Конструкторы производных классов
Производные классы не наследуют непосредственно конструкторы базового класса. Если базовый класс определяет конструктор по умолчанию и производный класс не определяет конструкторы, новые экземпляры производного класса используют конструктор базового класса по умолчанию. Если базовый класс не определяет конструктор по умолчанию, производный класс должен явно определить хотя бы один конструктор.
Конструкторы производных классов могут вызывать конструктор из базового класса с ключевым словом base
. Если производный класс явно не вызывает конструктор из базового класса, он вызывает конструктор по умолчанию для базового класса.
Чтобы вызвать неdefault base конструктор, добавьте : base(<parameters>)
после параметров конструктора и перед блоком тела.
class <derived-class> : <base-class> {
<derived-class>(<derived-parameters>) : <base-class>(<base-parameters>) {
# initialization code
}
}
При определении конструктора, вызывающего конструктор базового класса, параметры могут быть любым из следующих элементов:
- Переменная любого параметра в конструкторе производных классов.
- Любое статическое значение.
- Любое выражение, которое оценивает значение типа параметра.
Класс Иллюстрации в примере 1 показывает, как производный класс может использовать конструкторы базового класса.
Методы производного класса
Если класс является производным от базового класса, он наследует методы базового класса и их перегрузки. Все перегрузки методов, определенные в базовом классе, включая скрытые методы, доступны в производном классе.
Производный класс может переопределить перегрузку наследуемого метода, переопределив его в определении класса. Чтобы переопределить перегрузку, типы параметров должны совпадать с базовым классом. Выходной тип перегрузки может отличаться.
В отличие от конструкторов, методы не могут использовать : base(<parameters>)
синтаксис для вызова перегрузки базового класса для метода. Переопределенная перегрузка в производном классе полностью заменяет перегрузку, определенную базовым классом. Чтобы вызвать метод базового класса для экземпляра, приведение переменной экземпляра ($this
) к базовому классу перед вызовом метода.
В следующем фрагменте кода показано, как производный класс может вызывать метод базового класса.
class BaseClass {
[bool] IsTrue() { return $true }
}
class DerivedClass : BaseClass {
[bool] IsTrue() { return $false }
[bool] BaseIsTrue() { return ([BaseClass]$this).IsTrue() }
}
@"
[BaseClass]::new().IsTrue() = $([BaseClass]::new().IsTrue())
[DerivedClass]::new().IsTrue() = $([DerivedClass]::new().IsTrue())
[DerivedClass]::new().BaseIsTrue() = $([DerivedClass]::new().BaseIsTrue())
"@
[BaseClass]::new().IsTrue() = True
[DerivedClass]::new().IsTrue() = False
[DerivedClass]::new().BaseIsTrue() = True
Расширенный пример, показывающий, как производный класс может переопределить унаследованные методы, см . в классе Иллюстрации в примере 1.
Свойства производного класса
Если класс является производным от базового класса, он наследует свойства базового класса. Все свойства, определенные в базовом классе, включая скрытые свойства, доступны в производном классе.
Производный класс может переопределить унаследованное свойство, переопределив его в определении класса. Свойство в производном классе использует переопределенный тип и значение по умолчанию, если таковые имеются. Если унаследованное свойство определило значение по умолчанию и переопределенное свойство не имеет, унаследованное свойство не имеет значения по умолчанию.
Если производный класс не переопределяет статическое свойство, доступ к статическому свойству через производный класс обращается к статическому свойству базового класса. Изменение значения свойства через производный класс изменяет значение базового класса. Любой другой производный класс, который не переопределяет статическое свойство, также использует значение свойства в базовом классе. Обновление значения унаследованного статического свойства в классе, который не переопределяет свойство, может иметь непредвиденные последствия для классов, производных от одного базового класса.
В примере 1 показано, как производные классы, наследующие, расширяющие и переопределяющие свойства базового класса.
Производный от универсальных шаблонов
Если класс является производным от универсального, параметр типа должен быть уже определен, прежде чем PowerShell анализирует производный класс. Если параметр типа для универсального является классом Или перечислением PowerShell, определенным в одном файле или блоке кода, PowerShell вызывает ошибку.
Чтобы получить класс из универсального базового класса с пользовательским типом в качестве параметра типа, определите класс или перечисление для параметра типа в другом файле или модуле и используйте using module
инструкцию для загрузки определения типа.
Пример, показывающий, как наследовать от универсального базового класса, см . в примере 3.
Полезные классы для наследования
Существует несколько классов, которые могут быть полезны для наследования при создании модулей PowerShell. В этом разделе перечислены несколько базовых классов и то, для чего можно использовать класс, производный от них.
- System.Attribute — производные классы для определения атрибутов, которые можно использовать для переменных, параметров, определений перечисления и т. д.
- System.Management.Automation.ArgumentTransformationAttribute — производные классы для обработки входных данных для переменной или параметра в определенный тип данных.
- System.Management.Automation.ValidateArgumentsAttribute — производные классы для применения пользовательской проверки к переменным, параметрам и свойствам класса.
- System.Collections.Generic.List — производные классы для упрощения создания списков определенного типа данных и управления ими.
- System.Exception — производные классы для определения пользовательских ошибок.
Реализация интерфейсов
Класс PowerShell, реализующий интерфейс, должен реализовать все члены этого интерфейса. Опущение элементов интерфейса реализации приводит к ошибке синтаксического анализа в скрипте.
Примечание.
PowerShell не поддерживает объявление новых интерфейсов в скрипте PowerShell.
Вместо этого интерфейсы должны быть объявлены в коде .NET и добавлены в сеанс с командлетом или оператором Add-Type
using assembly
.
Когда класс реализует интерфейс, его можно использовать как любой другой класс, реализующий этот интерфейс. Некоторые команды и операции ограничивают поддерживаемые типы классами, реализующими определенный интерфейс.
Пример реализации интерфейсов см . в примере 2.
Полезные интерфейсы для реализации
Существует несколько классов интерфейса, которые могут быть полезны для наследования при создании модулей PowerShell. В этом разделе перечислены несколько базовых классов и то, для чего можно использовать класс, производный от них.
- System.IEquatable — этот интерфейс позволяет пользователям сравнивать два экземпляра класса. Если класс не реализует этот интерфейс, PowerShell проверяет эквивалентность между двумя экземплярами с помощью ссылочного равенства. Другими словами, экземпляр класса равен только самому себе, даже если значения свойств на двух экземплярах одинаковы.
- System.IComparable — этот интерфейс позволяет пользователям сравнивать экземпляры класса с
-le
операторами сравнения ,-lt
-ge
а также-gt
операторами сравнения. Если класс не реализует этот интерфейс, эти операторы вызывают ошибку. - System.IFormattable — этот интерфейс позволяет пользователям форматировать экземпляры класса в разные строки. Это полезно для классов, имеющих несколько стандартных строковых представлений, таких как бюджетные элементы, библиографии и температуры.
- System.IConvertible — этот интерфейс позволяет пользователям преобразовывать экземпляры класса в другие типы среды выполнения. Это полезно для классов, имеющих базовое числовое значение или которые можно преобразовать в один.
Ограничения
PowerShell не поддерживает определение интерфейсов в коде скрипта.
Обходное решение. Определение интерфейсов в C# и ссылка на сборку, определяющую интерфейсы.
Классы PowerShell могут наследовать только от одного базового класса.
Обходное решение. Наследование классов является транзитивным. Производный класс может наследовать от другого производного класса, чтобы получить свойства и методы базового класса.
При наследовании от универсального класса или интерфейса параметр типа для универсального объекта должен быть уже определен. Класс не может определить себя как параметр типа для класса или интерфейса.
Обходное решение. Чтобы получить производный от универсального базового класса или интерфейса, определите пользовательский тип в другом
.psm1
файле и используйтеusing module
инструкцию для загрузки типа. При наследовании от универсального типа не существует обходного решения для пользовательского типа.
См. также
PowerShell