추상 클래스

추상 클래스

F#에서 추상 클래스(abstract class)는 일부 또는 모든 멤버를 구현하지 않고 남겨두는 클래스예요. 이렇게 비워 둔 멤버들은 파생 클래스가 직접 구현하도록 하죠. 상속 계층의 공통 기능을 표현하는 기반 클래스로서, 객체 지향 프로그래밍에서 아주 유용하게 쓰인답니다.

출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/abstract-classes

본문

추상 클래스는 일부 또는 모든 멤버가 구현되지 않은 채로 남겨져 있어서, 그 구현을 파생 클래스가 제공하도록 하는 클래스예요.

구문 (Syntax)

// Abstract class syntax.
[<AbstractClass>]
type [ accessibility-modifier ] abstract-class-name =
    [ inherit base-class-or-interface-name ]
    [ abstract-member-declarations-and-member-definitions ]

// Abstract member syntax.
abstract member member-name : type-signature

설명 (Remarks)

객체 지향 프로그래밍에서 추상 클래스는 계층 구조의 기반 클래스로 쓰이며, 서로 다른 여러 객체 타입이 공통으로 가지는 기능을 나타내요. "추상(abstract)"이라는 이름이 뜻하듯이, 추상 클래스는 문제 영역의 구체적인 개체에 직접 대응하지 않는 경우가 많아요. 하지만 여러 구체적인 개체들이 공통으로 갖는 성질을 잘 드러내 주죠.

추상 클래스는 반드시 AbstractClass 특성(attribute)을 가져야 해요. 그리고 이미 구현된 멤버와 구현되지 않은 멤버를 함께 가질 수 있어요. 클래스에 붙는 "추상"이라는 용어의 의미는 다른 .NET 언어와 동일하지만, 메서드(와 속성)에 붙는 "추상"의 의미는 F#에서 다른 .NET 언어와 조금 달라요.

F#에서 메서드에 abstract 키워드를 붙이면, 그 멤버가 해당 타입의 가상 함수 내부 테이블에 가상 디스패치 슬롯(virtual dispatch slot) 이라는 항목을 가진다는 뜻이에요. 다시 말해 그 메서드는 가상(virtual)이라는 거죠. F#에는 virtual 키워드가 없기 때문이에요. 메서드가 구현되었는지와 무관하게, 가상 메서드에는 항상 abstract 키워드를 사용해요. 가상 디스패치 슬롯의 선언은 그 슬롯에 대한 메서드의 정의와는 별개로 이루어져요. 따라서 다른 .NET 언어에서의 "가상 메서드 선언+정의"에 해당하는 F# 표현은, abstract 메서드 선언에 별도의 정의(이때 default 키워드나 override 키워드를 사용)를 더한 조합이에요. 자세한 내용과 예제는 Methods 문서를 참고하세요.

클래스가 "추상"으로 간주되는 경우는, 선언되었지만 정의되지 않은 추상 메서드가 있을 때뿐이에요. 따라서 추상 메서드를 가진 클래스라고 해서 반드시 추상 클래스인 것은 아니에요. 정의되지 않은 추상 메서드가 없는 경우에는 AbstractClass 특성을 쓰지 마세요.

앞선 구문에서 accessibility-modifierpublic, private, internal 중 하나가 될 수 있어요. 자세한 내용은 Access Control 문서를 참고하세요.

다른 타입과 마찬가지로 추상 클래스도 기반 클래스 하나와 기반 인터페이스 하나 이상을 가질 수 있어요. 각 기반 클래스나 인터페이스는 inherit 키워드와 함께 각각 별도의 줄에 나타나요.

추상 클래스의 타입 정의에는 완전히 구현된 멤버를 넣을 수도 있고, 추상 멤버를 넣을 수도 있어요. 추상 멤버의 구문은 앞선 구문에서 따로 보여드렸어요. 이 구문에서 멤버의 타입 시그니처는 매개변수 타입을 순서대로 나열하고 반환 타입을 붙인 목록이에요. 커리(curried) 및 튜플(tupled) 매개변수에 맞게 -> 토큰과/또는 * 토큰으로 구분해요. 추상 멤버 타입 시그니처의 구문은 시그니처 파일에서 쓰는 구문이나 Visual Studio 코드 편집기의 IntelliSense가 보여주는 구문과 동일해요.

다음 코드는 두 개의 비추상 파생 클래스인 SquareCircle을 가지는 추상 클래스 Shape를 보여줘요. 이 예제는 추상 클래스, 메서드, 속성을 어떻게 쓰는지를 알려줘요. 예제에서 추상 클래스 Shape는 원(circle)과 정사각형(square)이라는 구체적인 개체들의 공통 요소를 나타내요. 2차원 좌표계에서 모든 도형이 공통으로 가지는 특징—격자 위의 위치, 회전 각도, 넓이(area), 둘레(perimeter) 속성—이 Shape 클래스로 추상화되어 있어요. 이 값들은 오버라이드할 수 있는데, 개별 도형이 바꿀 수 없는 위치(position)는 예외예요.

Rotate 메서드는 오버라이드할 수 있어요. Circle 클래스처럼 대칭성 덕분에 회전 불변인 도형이 그 예시죠. 그래서 Circle 클래스에서는 Rotate 메서드가 아무 일도 하지 않는 메서드로 대체됩니다.

// An abstract class that has some methods and properties defined
// and some left abstract.
[<AbstractClass>]
type Shape2D(x0: float, y0: float) =
    let mutable x, y = x0, y0
    let mutable rotAngle = 0.0

    // These properties are not declared abstract. They
    // cannot be overriden.
    member this.CenterX
        with get () = x
        and set xval = x <- xval

    member this.CenterY
        with get () = y
        and set yval = y <- yval

    // These properties are abstract, and no default implementation
    // is provided. Non-abstract derived classes must implement these.
    abstract Area: float with get
    abstract Perimeter: float with get
    abstract Name: string with get

    // This method is not declared abstract. It cannot be
    // overridden.
    member this.Move dx dy =
        x <- x + dx
        y <- y + dy

    // An abstract method that is given a default implementation
    // is equivalent to a virtual method in other .NET languages.
    // Rotate changes the internal angle of rotation of the square.
    // Angle is assumed to be in degrees.
    abstract member Rotate: float -> unit
    default this.Rotate(angle) = rotAngle <- rotAngle + angle

type Square(x, y, sideLengthIn) =
    inherit Shape2D(x, y)
    member this.SideLength = sideLengthIn
    override this.Area = this.SideLength * this.SideLength
    override this.Perimeter = this.SideLength * 4.
    override this.Name = "Square"

type Circle(x, y, radius) =
    inherit Shape2D(x, y)
    let PI = 3.141592654
    member this.Radius = radius
    override this.Area = PI * this.Radius * this.Radius
    override this.Perimeter = 2. * PI * this.Radius
    // Rotating a circle does nothing, so use the wildcard
    // character to discard the unused argument and
    // evaluate to unit.
    override this.Rotate(_) = ()
    override this.Name = "Circle"

let square1 = new Square(0.0, 0.0, 10.0)
let circle1 = new Circle(0.0, 0.0, 5.0)
circle1.CenterX <- 1.0
circle1.CenterY <- -2.0
square1.Move -1.0 2.0
square1.Rotate 45.0
circle1.Rotate 45.0
printfn "Perimeter of square with side length %f is %f, %f" (square1.SideLength) (square1.Area) (square1.Perimeter)
printfn "Circumference of circle with radius %f is %f, %f" (circle1.Radius) (circle1.Area) (circle1.Perimeter)

let shapeList: list<Shape2D> = [ (square1 :> Shape2D); (circle1 :> Shape2D) ]
List.iter (fun (elem: Shape2D) -> printfn "Area of %s: %f" (elem.Name) (elem.Area)) shapeList

출력 (Output):

Perimeter of square with side length 10.000000 is 40.000000
Circumference of circle with radius 5.000000 is 31.415927
Area of Square: 100.000000
Area of Circle: 78.539816

더 알아보기 (Learn more)