about_Classes_Inheritance

about_Classes_Inheritance

다른 타입(class)을 바탕으로 새 타입을 만들어 확장하는 방법, 즉 클래스 상속(inheritance)이 어떻게 동작하는지 알아보는 문서예요. 기존 클래스를 물려받아 기능을 재사용하고, 필요하면 그 동작을 덮어써서(override) 자기 방식으로 바꿀 수 있어요.

출처: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes_inheritance

본문

PowerShell 클래스는 상속을 지원해요. 상속이란 부모 클래스의 동작을 그대로 물려받거나 확장하고, 필요하면 수정해서 쓰는 자식 클래스를 정의하는 기법이에요. 물려받는 쪽을 기반 클래스(base class), 물려받아 쓰는 쪽을 *파생 클래스(derived class)*라고 불러요.

PowerShell은 단일 상속만 지원해요. 그러니까 클래스 하나는 한 개의 클래스에서만 상속받을 수 있는 거죠. 다만 상속은 전이적(transitive)이라서 여러 타입이 연결된 상속 계층을 만들 수 있어요. 예를 들어 타입 DC를, CB를, B가 기반 클래스 A를 상속받는다면, 상속이 전이되므로 A의 멤버들은 모두 D에서도 쓸 수 있어요.

파생 클래스가 기반 클래스의 모든 멤버를 물려받는 건 아니에요. 다음과 같은 멤버는 상속되지 않아요.

인스턴스 생성자 — 클래스의 새 인스턴스를 만들 때 호출하는 생성자예요. 클래스마다 자기 생성자를 직접 정의해야 해요.

정적 생성자 — 클래스의 정적 데이터를 초기화하는 생성자예요.

클래스를 확장하려면 기존 클래스에서 파생된 새 클래스를 만들면 돼요. 파생 클래스는 기반 클래스의 속성과 메서드를 물려받고, 필요에 따라 기반 클래스 멤버를 추가하거나 덮어쓸 수 있어요.

클래스는 계약(contract)을 정의하는 인터페이스에서도 상속받을 수 있어요. 인터페이스에서 상속받은 클래스는 그 계약을 반드시 구현해야 해요. 계약을 구현하고 나면, 그 인터페이스를 구현한 다른 클래스들과 똑같이 취급할 수 있어요. 그런데 인터페이스에서 상속받았는데 정작 그 계약을 구현하지 않으면, PowerShell은 그 클래스에 대해 구문 분석(parsing) 오류를 띄워요.

일부 PowerShell 연산자는 클래스가 특정 인터페이스를 구현하고 있느냐에 따라 동작이 달라져요. 예를 들어 -eq 연산자는 클래스가 System.IEquatable 인터페이스를 구현하지 않으면 참조 동일성만 검사해요. 또 -le, -lt, -ge, -gt 연산자는 System.IComparable 인터페이스를 구현한 클래스에서만 동작해요.

파생 클래스는 : 문법을 써서 기반 클래스를 확장하거나 인터페이스를 구현해요. 이때 파생 클래스 이름은 클래스 선언에서 항상 맨 왼쪽에 와야 해요.

아래는 기본적인 PowerShell 클래스 상속 문법이에요.

class Derived : Base {...}

이번엔 기반 클래스 뒤에 인터페이스 선언이 오는 상속 예시예요.

class Derived : Base, Interface {...}

Syntax

클래스 상속은 다음 문법을 사용해요.

한 줄 문법 (One line syntax)

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 {...}

여러 줄 문법 (Multiline syntax)

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
}

Examples

예제 1 - 기반 클래스에서 상속받고 덮어쓰기

아래 예제는 상속받은 속성을 덮어쓸 때와 덮어쓰지 않을 때 각각 어떻게 동작하는지 보여줘요. 설명을 읽고 코드 블록을 순서대로 실행해 보세요.

기반 클래스 정의하기

첫 번째 코드 블록은 PublishedWork를 기반 클래스로 정의해요. 여기에는 정적 속성 ListArtists가 있어요. 그리고 정적 RegisterWork() 메서드를 정의해서 작업물을 정적 List 속성에, 아티스트를 Artists 속성에 추가하는데, 추가할 때마다 목록에 새 항목이 들어갔다고 메시지를 남겨요.

클래스는 출판된 작업물을 표현하는 인스턴스 속성 3개를 정의하고, 마지막으로 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)" }
}

덮어쓰지 않는 파생 클래스 정의하기

첫 번째 파생 클래스는 Album이에요. 이 클래스는 어떤 속성이나 메서드도 덮어쓰지 않고, 기반 클래스에 없던 인스턴스 속성 Genres만 새로 추가해요.

class Album : PublishedWork {
    [string[]] $Genres   = @()
}

다음 코드 블록은 파생 클래스 Album이 어떻게 동작하는지 보여줘요. 먼저 $VerbosePreference를 설정해서 클래스 메서드의 메시지가 콘솔에 출력되게 해요. 그다음 클래스 인스턴스 3개를 만들고 표로 보여준 다음, 상속받은 정적 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 클래스는 Category 값을 정하거나 생성자를 정의하지 않았는데도, 그 속성이 기반 클래스의 기본 생성자에 의해 정의되어 있어요.

자세한(verbose) 메시지를 보면 RegisterWork() 메서드를 두 번째로 호출했을 때 작업물과 아티스트가 이미 등록되어 있다고 나와요. 첫 번째 RegisterWork() 호출이 파생 클래스 Album을 대상으로 했지만, 실제로는 기반 클래스 PublishedWork에서 상속받은 정적 메서드가 실행된 거예요. 그 메서드는 기반 클래스의 정적 ListArtist 속성을 갱신했고, 파생 클래스는 이 속성들을 덮어쓰지 않았어요.

다음 코드 블록은 등록 목록을 비우고 Album 객체에 Register() 인스턴스 메서드를 호출해요.

[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에서 상속받는 Illustration 클래스를 정의해요. 이 새 클래스는 기본값이 UnknownMedium 인스턴스 속성을 정의해서 기반 클래스를 확장해요.

파생 클래스 Album과 달리 Illustration은 다음 속성과 메서드를 덮어써요.

  • 기반과 같은 정의지만, Illustration 클래스에서 직접 선언하는 정적 Artists 속성을 덮어써요.

  • 기본값을 Illustrations로 바꾼 Category 인스턴스 속성을 덮어써요.

  • 삽화가 만들어진 매체(medium)를 문자열 표현에 포함하도록 ToString() 인스턴스 메서드를 덮어써요.

클래스는 또 정적 RegisterIllustration() 메서드를 정의해요. 이 메서드는 먼저 기반 클래스의 RegisterWork() 메서드를 호출한 다음, 파생 클래스에서 덮어쓴 정적 Artists 속성에 아티스트를 추가해요.

마지막으로 클래스는 생성자 3개를 모두 덮어써요.

  1. 기본 생성자는 삽화를 만들었다는 자세한 메시지 외에는 비어 있어요.

  2. 다음 생성자는 삽화를 만든 이름과 아티스트, 두 문자열 값을 받아요. NameArtist 속성을 직접 설정하는 로직을 구현하는 대신, 기반 클래스에서 알맞은 생성자를 호출해요.

  3. 마지막 생성자는 삽화의 이름, 아티스트, 매체를 뜻하는 문자열 값 3개를 받아요. 두 생성자 모두 삽화를 만들었다는 자세한 메시지를 남겨요.

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"
    }
}

다음 코드 블록은 파생 클래스 Illustration이 어떻게 동작하는지 보여줘요. 인스턴스 3개를 만들고 표로 보여준 다음, 상속받은 정적 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 속성에는 삽화가(illustrator)만 들어 있고 앨범 아티스트는 없어요. 파생 클래스에서 Artist 속성을 다시 정의했기 때문에 기반 클래스의 정적 속성을 그대로 돌려주지 않는 거예요.

마지막 코드 블록은 기반 클래스의 정적 List 속성 항목에 ToString() 메서드를 호출해요.

[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)

Album 인스턴스는 문자열로 이름과 아티스트만 돌려줘요. 반면 Illustration 인스턴스는 그 클래스에서 ToString() 메서드를 덮어썼기 때문에 매체도 괄호 안에 함께 포함해요.

예제 2 - 인터페이스 구현하기

다음 예제는 클래스가 인터페이스를 하나 또는 여러 개 구현하는 방법을 보여줘요. 이 예제는 Temperature 클래스의 정의를 확장해서 더 많은 동작과 연산을 지원하게 만들어요.

초기 클래스 정의

인터페이스를 구현하기 전의 Temperature 클래스는 DegreesScale 두 속성으로 정의돼요. 생성자와, 인스턴스를 특정 눈금(scale)의 도(degree) 값으로 돌려주는 인스턴스 메서드 3개도 정의해요. 사용 가능한 눈금은 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.

출력에서 Temperature 인스턴스는 이런 문제가 있는 걸 볼 수 있어요.

  • 문자열로 제대로 표시되지 않아요.

  • 동등한지(equivalency) 제대로 검사할 수 없어요.

  • 비교할 수 없어요.

이 세 가지 문제는 클래스에 인터페이스를 구현하면 해결할 수 있어요.

IFormattable 구현하기

Temperature 클래스에 구현할 첫 번째 인터페이스는 System.IFormattable이에요. 이 인터페이스는 클래스 인스턴스를 다양한 문자열로 형식화(format)할 수 있게 해줘요. 인터페이스를 구현하려면 클래스가 System.IFormattable에서 상속받고 ToString() 인스턴스 메서드를 정의해야 해요.

ToString() 인스턴스 메서드는 다음 시그니처를 가져야 해요.

[string] ToString(
    [string]$Format,
    [System.IFormatProvider]$FormatProvider
) {
    # Implementation
}

인터페이스가 요구하는 시그니처는 참조 문서에 나와 있어요.

Temperature는 세 가지 형식을 지원하면 돼요. 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'"
    )
}

이 구현에서 메서드는 숫자 도 값 자체를 형식화할 때 인스턴스의 눈금을 기본 형식으로 쓰고 현재 문화권(culture)을 사용해요. To<Scale>() 인스턴스 메서드로 도 값을 변환하고, 소수점 두 자리로 형식화한 다음 적절한 도 기호를 문자열에 덧붙여요.

필수 시그니처를 구현하고 나면, 클래스는 형식화된 인스턴스를 더 쉽게 돌려주도록 오버로드를 추가로 정의할 수도 있어요.

[string] ToString([string]$Format) {
    return $this.ToString($Format, $null)
}

[string] ToString() {
    return $this.ToString($null, $null)
}

Temperature의 수정된 정의는 다음과 같아요.

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
}

인터페이스가 요구하는 시그니처는 참조 문서에 나와 있어요.

Temperature는 클래스 인스턴스 두 개만 비교하도록 하면 돼요. $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
}

인터페이스가 요구하는 시그니처는 참조 문서에 나와 있어요.

Temperature는 클래스 인스턴스 두 개만 비교하도록 하면 돼요. 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 - 제네릭 기반 클래스에서 상속받기

이 예제는 타입 매개변수가 분석(parse) 시점에 이미 정의되어 있다면 제네릭 타입에서 파생할 수 있다는 걸 보여줘요.

내장 클래스를 타입 매개변수로 사용하기

다음 코드 블록을 실행해 보세요. 타입 매개변수가 분석 시점에 이미 정의되어 있으면 새 클래스가 제네릭 타입에서 상속받을 수 있다는 걸 알려줘요.

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

사용자 정의 클래스를 타입 매개변수로 사용하기

다음 코드 블록은 먼저 인스턴스 속성 하나와 ToString() 메서드를 가진 새 클래스 ExampleItem을 정의해요. 그다음 ExampleItem을 타입 매개변수로 해서 System.Collections.Generic.List 기반 클래스에서 상속받는 ExampleItemList 클래스를 정의해요.

전체 코드 블록을 복사해서 한 번에 실행해 보세요.

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의 타입 가속기(type accelerator)에 추가해요. 이 패턴을 쓰면 모듈을 쓰는 사람이 먼저 using module 문을 쓰지 않고도 사용자 정의 타입에 대한 IntelliSense와 자동 완성을 바로 사용할 수 있어요.

이 패턴에 대한 자세한 내용은 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 클래스와 다른 모듈 파일에 정의되어 있기 때문에, 모듈은 오류 없이 로드돼요. 두 클래스 모두 모듈 사용자가 사용할 수 있어요.

기반 클래스에서 상속받기

클래스가 기반 클래스에서 상속받으면 기반 클래스의 속성과 메서드를 물려받아요. 기반 클래스의 생성자를 직접 상속받지는 않지만, 그 생성자를 호출할 수는 있어요.

기반 클래스가 PowerShell이 아니라 .NET으로 정의된 경우에는 다음을 기억해 두세요.

  • PowerShell 클래스는 봉인(sealed) 클래스에서 상속받을 수 없어요.

  • 제네릭 기반 클래스에서 상속받을 때는 그 제네릭의 타입 매개변수가 파생 클래스가 될 수 없어요. 파생 클래스를 타입 매개변수로 쓰면 구문 분석 오류가 나요.

파생 클래스에서 상속과 덮어쓰기가 어떻게 동작하는지 보려면 예제 1을 참고하세요.

파생 클래스 생성자

파생 클래스는 기반 클래스의 생성자를 직접 상속받지 않아요. 기반 클래스에 기본 생성자가 있고 파생 클래스에 생성자가 하나도 없다면, 파생 클래스의 새 인스턴스는 기반 클래스의 기본 생성자를 사용해요. 기반 클래스에 기본 생성자가 없다면 파생 클래스는 생성자를 최소한 하나는 반드시 명시적으로 정의해야 해요.

파생 클래스 생성자는 base 키워드로 기반 클래스의 생성자를 호출할 수 있어요. 파생 클래스가 기반 클래스의 생성자를 명시적으로 호출하지 않으면, 대신 기반 클래스의 기본 생성자를 호출해요.

기본이 아닌 기반 생성자를 호출하려면 생성자 매개변수 뒤, 본문 블록 앞에 : base(<parameters>)를 붙이면 돼요.

class <derived-class> : <base-class> {
    <derived-class>(<derived-parameters>) : <base-class>(<base-parameters>) {
        # initialization code
    }
}

기반 클래스 생성자를 호출하는 생성자를 정의할 때, 그 매개변수는 다음 중 하나가 될 수 있어요.

  • 파생 클래스 생성자에 있는 어떤 매개변수의 변수

  • 어떤 정적 값

  • 매개변수 타입의 값으로 계산되는 어떤 식(expression)

예제 1Illustration 클래스가 파생 클래스에서 기반 클래스 생성자를 활용하는 방법을 보여줘요.

파생 클래스 메서드

클래스가 기반 클래스에서 파생되면 기반 클래스의 메서드와 그 오버로드를 물려받아요. 기반 클래스에 정의된 모든 메서드 오버로드는 숨겨진 메서드를 포함해 파생 클래스에서도 쓸 수 있어요.

파생 클래스는 메서드를 클래스 정의에 다시 정의해서 상속받은 메서드 오버로드를 덮어쓸 수 있어요. 오버로드를 덮어쓰려면 매개변수 타입이 기반 클래스와 같아야 해요. 오버로드의 출력 타입은 달라도 돼요.

생성자와 달리 메서드는 : 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

파생 클래스가 상속받은 메서드를 덮어쓰는 더 자세한 예시를 보려면 예제 1Illustration 클래스를 참고하세요.

파생 클래스 속성

클래스가 기반 클래스에서 파생되면 기반 클래스의 속성을 물려받아요. 기반 클래스에 정의된 모든 속성은 숨겨진 속성을 포함해 파생 클래스에서도 쓸 수 있어요.

파생 클래스는 속성을 클래스 정의에 다시 정의해서 상속받은 속성을 덮어쓸 수 있어요. 파생 클래스의 속성은 다시 정의한 타입과 기본값(있다면)을 사용해요. 상속받은 속성에 기본값이 있었는데 다시 정의한 속성에 기본값이 없다면, 상속받은 속성은 기본값이 없는 상태가 돼요.

파생 클래스가 정적 속성을 덮어쓰지 않으면, 파생 클래스를 통해 그 정적 속성에 접근하면 기반 클래스의 정적 속성에 접근하는 거예요. 파생 클래스를 통해 속성 값을 바꾸면 기반 클래스의 값이 바뀌어요. 그 정적 속성을 덮어쓰지 않는 다른 파생 클래스도 기반 클래스의 속성 값을 사용하게 돼요. 같은 기반 클래스에서 파생된 클래스들에서, 덮어쓰지 않은 상태로 상속받은 정적 속성 값을 갱신하면 의도하지 않은 영향이 생길 수 있어요.

예제 1은 파생 클래스가 기반 클래스 속성을 상속받고, 확장하고, 덮어쓰는 방법을 보여줘요.

제네릭에서 파생하기

클래스가 제네릭에서 파생될 때는, PowerShell이 파생 클래스를 분석하기 전에 타입 매개변수가 이미 정의되어 있어야 해요. 제네릭의 타입 매개변수가 같은 파일이나 코드 블록에 정의된 PowerShell 클래스나 열거형이라면 PowerShell이 오류를 띄워요.

타입 매개변수에 사용자 정의 타입을 쓰는 제네릭 기반 클래스에서 파생하려면, 타입 매개변수로 쓸 클래스나 열거형을 다른 파일이나 모듈에 정의하고 using module 문으로 그 타입 정의를 로드해야 해요.

제네릭 기반 클래스에서 상속받는 예시를 보려면 예제 3을 참고하세요.

상속하면 유용한 클래스

PowerShell 모듈을 만들 때 상속받아 쓰면 유용한 클래스가 몇 가지 있어요. 아래에 기반 클래스 몇 개와, 그 클래스에서 파생된 클래스를 어떤 용도로 쓸 수 있는지 정리했어요.

  • System.Attribute — 변수, 매개변수, 클래스, 열거형 정의 등에 쓸 수 있는 특성(attribute)을 정의하려면 이 클래스에서 파생하면 돼요.

  • System.Management.Automation.ArgumentTransformationAttribute — 변수나 매개변수의 입력을 특정 데이터 타입으로 변환하는 처리를 하려면 이 클래스에서 파생하면 돼요.

  • System.Management.Automation.ValidateArgumentsAttribute — 변수, 매개변수, 클래스 속성에 사용자 정의 검증을 적용하려면 이 클래스에서 파생하면 돼요.

  • System.Collections.Generic.List — 특정 데이터 타입의 목록을 더 쉽게 만들고 관리하려면 이 클래스에서 파생하면 돼요.

  • System.Exception — 사용자 정의 오류를 정의하려면 이 클래스에서 파생하면 돼요.

인터페이스 구현하기

인터페이스를 구현하는 PowerShell 클래스는 그 인터페이스의 모든 멤버를 반드시 구현해야 해요. 인터페이스 멤버를 하나라도 빠뜨리면 스크립트에서 분석(parse) 시점에 오류가 나요.

참고 — PowerShell은 PowerShell 스크립트에서 새 인터페이스를 선언하는 걸 지원하지 않아요. 인터페이스는 .NET 코드로 선언한 다음 Add-Type cmdlet이나 using assembly 문으로 세션에 추가해야 해요.

클래스가 인터페이스를 구현하면, 그 인터페이스를 구현한 다른 클래스처럼 사용할 수 있어요. 일부 명령과 연산은 지원하는 타입을 특정 인터페이스를 구현한 클래스로 제한하기도 해요.

인터페이스 구현 예시를 살펴보려면 예제 2를 참고하세요.

구현하면 유용한 인터페이스

PowerShell 모듈을 만들 때 상속받아 쓰면 유용한 인터페이스가 몇 가지 있어요. 아래에 기반 클래스 몇 개와, 그 클래스에서 파생된 클래스를 어떤 용도로 쓸 수 있는지 정리했어요.

  • System.IEquatable — 이 인터페이스는 클래스의 인스턴스 두 개를 비교할 수 있게 해줘요. 클래스가 이 인터페이스를 구현하지 않으면, PowerShell은 두 인스턴스의 동등성을 참조 동일성으로 검사해요. 다시 말해 두 인스턴스의 속성 값이 같아도 인스턴스는 자기 자신과만 같다고 판단해요.

  • System.IComparable — 이 인터페이스는 사용자가 -le, -lt, -ge, -gt 비교 연산자로 클래스의 인스턴스를 비교할 수 있게 해줘요. 클래스가 이 인터페이스를 구현하지 않으면 그 연산자들은 오류를 띄워요.

  • System.IFormattable — 이 인터페이스는 사용자가 클래스의 인스턴스를 다양한 문자열로 형식화할 수 있게 해줘요. 예산 항목, 참고문헌, 온도처럼 표준 문자열 표현이 두 개 이상인 클래스에 유용해요.

  • System.IConvertible — 이 인터페이스는 사용자가 클래스의 인스턴스를 다른 런타임 타입으로 변환할 수 있게 해줘요. 기본이 되는 숫자 값이 있거나 그런 값으로 변환할 수 있는 클래스에 유용해요.

제한 사항 (Limitations)

  • PowerShell은 스크립트 코드에서 인터페이스를 정의하는 걸 지원하지 않아요. 해결 방법: C#으로 인터페이스를 정의하고, 그 인터페이스를 정의한 어셈블리를 참조하세요.

  • PowerShell 클래스는 기반 클래스 하나에서만 상속받을 수 있어요. 해결 방법: 클래스 상속은 전이적이에요. 파생 클래스가 또 다른 파생 클래스에서 상속받아 기반 클래스의 속성과 메서드를 얻으면 돼요.

  • 제네릭 클래스나 인터페이스에서 상속받을 때는 그 제네릭의 타입 매개변수가 이미 정의되어 있어야 해요. 클래스는 자기 자신을 클래스나 인터페이스의 타입 매개변수로 쓸 수 없어요. 해결 방법: 제네릭 기반 클래스나 인터페이스에서 파생하려면 사용자 정의 타입을 다른 .psm1 파일에 정의하고 using module 문으로 로드하세요. 제네릭에서 상속받을 때 사용자 정의 타입이 자기 자신을 타입 매개변수로 쓰는 건 해결할 방법이 없어요.

더 알아보기