跳轉至內容

F# 程式設計/繼承

來自華夏公益教科書,自由的教科書
前一頁:類 索引 下一頁:介面
F#:繼承

許多面向物件的語言在 .NET BCL 中廣泛使用 **繼承** 來構建類層次結構。

簡單來說,子類是從已經定義的類派生的類。子類除了新增自己的成員外,還從基類繼承其成員。子類使用 `inherit` 關鍵字定義,如下所示

type Person(name) =
    member x.Name = name        
    member x.Greet() = printfn "Hi, I'm %s" x.Name
    
type Student(name, studentID : int) =
    inherit Person(name)
    
    let mutable _GPA = 0.0
    
    member x.StudentID = studentID
    member x.GPA
        with get() = _GPA
        and set value = _GPA <- value
    
type Worker(name, employer : string) = 
    inherit Person(name)
    
    let mutable _salary = 0.0
    
    member x.Salary
        with get() = _salary
        and set value = _salary <- value
    
    member x.Employer = employer

我們的簡單類層次結構如下所示

System.Object (* All classes descend from  *)
 - Person
   - Student
   - Worker

`Student` 和 `Worker` 子類都從 `Person` 基類繼承了 `Name` 和 `Greet` 方法。這可以在 fsi 中演示

> let somePerson, someStudent, someWorker =
    new Person("Juliet"), new Student("Monique", 123456), new Worker("Carla", "Awesome Hair Salon");;

val someWorker : Worker
val someStudent : Student
val somePerson : Person

> somePerson.Name, someStudent.Name, someWorker.Name;;
val it : string * string * string = ("Juliet", "Monique", "Carla")

> someStudent.StudentID;;
val it : int = 123456

> someWorker.Employer;;
val it : string = "Awesome Hair Salon"

> someWorker.ToString();; (* ToString method inherited from System.Object *)
val it : string = "FSI_0002+Worker"

.NET 的物件模型支援 *單類繼承*,這意味著子類只能有一個基類。換句話說,無法建立同時從 `Student` 和 `Employee` 派生的類。

覆蓋方法

[編輯 | 編輯原始碼]

有時,您可能希望派生類更改從基類繼承的方法的預設行為。例如,上面 `ToString()` 方法的輸出不是很有用。我們可以使用 `override` 關鍵字透過不同的實現來覆蓋這種行為

type Person(name) =
    member x.Name = name        
    member x.Greet() = printfn "Hi, I'm %s" x.Name
    
    override x.ToString() = x.Name    (* The ToString() method is inherited from System.Object *)

我們覆蓋了 `ToString()` 方法的預設實現,使其打印出人的姓名。

F# 中的方法預設不可覆蓋。如果您希望使用者能夠在派生類中覆蓋方法,則必須使用 `abstract` 和 `default` 關鍵字將您的方法宣告為可覆蓋,如下所示

type Person(name) =
    member x.Name = name        
    
    abstract Greet : unit -> unit
    default x.Greet() = printfn "Hi, I'm %s" x.Name
    
type Quebecois(name) =
    inherit Person(name)
    
    override x.Greet() = printfn "Bonjour, je m'appelle %s, eh." x.Name

我們的類 `Person` 提供了一個 `Greet` 方法,可以在派生類中覆蓋。以下是在 fsi 中使用這兩個類的示例

> let terrance, phillip = new Person("Terrance"), new Quebecois("Phillip");;

val terrance : Person
val phillip : Quebecois

> terrance.Greet();;
Hi, I'm Terrance
val it : unit = ()

> phillip.Greet();;
Bonjour, je m'appelle Phillip, eh.

抽象類

[編輯 | 編輯原始碼]

抽象類是提供物件不完整實現的類,要求程式設計師建立抽象類的子類來完成其餘的實現。例如,考慮以下情況

[<AbstractClass>]
type Shape(position : Point) =
    member x.Position = position
    override x.ToString() =
        sprintf "position = {%i, %i}, area = %f" position.X position.Y (x.Area())
    
    abstract member Draw : unit -> unit 
    abstract member Area : unit -> float

您首先會注意到 `AbstractClass` 屬性,它告訴編譯器我們的類包含一些抽象成員。此外,您會注意到兩個抽象成員,`Draw` 和 `Area` 沒有實現,只有型別簽名。

我們無法建立 `Shape` 的例項,因為該類尚未完全實現。相反,我們必須從 `Shape` 派生並用具體實現覆蓋 `Draw` 和 `Area` 方法

type Circle(position : Point, radius : float) =
    inherit Shape(position)
    
    member x.Radius = radius
    override x.Draw() = printfn "(Circle)"
    override x.Area() = Math.PI * radius * radius
    
type Rectangle(position : Point, width : float, height : float) =
    inherit Shape(position)
    
    member x.Width = width
    member x.Height = height
    override x.Draw() = printfn "(Rectangle)"
    override x.Area() = width * height
    
type Square(position : Point, width : float) =
    inherit Shape(position)
    
    member x.Width = width
    member x.ToRectangle() = new Rectangle(position, width, width)
    override x.Draw() = printfn "(Square)"
    override x.Area() = width * width
    
type Triangle(position : Point, sideA : float, sideB : float, sideC : float) =
    inherit Shape(position)
    
    member x.SideA = sideA
    member x.SideB = sideB
    member x.SideC = sideC
    
    override x.Draw() = printfn "(Triangle)"
    override x.Area() =
        (* Heron's formula *)
        let a, b, c = sideA, sideB, sideC
        let s = (a + b + c) / 2.0
        Math.Sqrt(s * (s - a) * (s - b) * (s - c) )

現在我們有了 `Shape` 類的幾個不同的實現。我們可以在 fsi 中嘗試使用這些實現

> let position = { X = 0; Y = 0 };;

val position : Point

> let circle, rectangle, square, triangle =
    new Circle(position, 5.0),
    new Rectangle(position, 2.0, 7.0),
    new Square(position, 10.0),
    new Triangle(position, 3.0, 4.0, 5.0);;

val triangle : Triangle
val square : Square
val rectangle : Rectangle
val circle : Circle

> circle.ToString();;
val it : string = "Circle, position = {0, 0}, area = 78.539816"

> triangle.ToString();;
val it : string = "Triangle, position = {0, 0}, area = 6.000000"

> square.Width;;
val it : float = 10.0

> square.ToRectangle().ToString();;
val it : string = "Rectangle, position = {0, 0}, area = 100.000000"

> rectangle.Height, rectangle.Width;;
val it : float * float = (7.0, 2.0)

使用子類

[編輯 | 編輯原始碼]

向上轉型和向下轉型

[編輯 | 編輯原始碼]

*型別轉換* 是將物件從一種型別更改為另一種型別的操作。這與對映函式不同,因為型別轉換不會返回新物件的例項,而是返回具有不同型別的同一物件的例項。

例如,假設 `B` 是 `A` 的子類。如果我們有一個 `B` 的例項,我們能夠將其轉換為 `A` 的例項。由於 `A` 在類層次結構中向上,我們將其稱為向上轉型。我們使用 `:>` 運算子來執行向上轉型

> let regularString = "Hello world";;

val regularString : string

> let upcastString = "Hello world" :> obj;;

val upcastString : obj

> regularString.ToString();;
val it : string = "Hello world"

> regularString.Length;;
val it : int = 11

> upcastString.ToString();; (* type obj has a .ToString method *)
val it : string = "Hello world"

> upcastString.Length;; (* however, obj does not have Length method *)

  upcastString.Length;; (* however, obj does not have Length method *)
  -------------^^^^^^^

stdin(24,14): error FS0039: The field, constructor or member 'Length' is not defined.

向上轉型被認為是“安全的”,因為派生類保證擁有與祖先類相同的成員。如果需要,我們可以反過來進行操作:我們可以使用 `:?>` 運算子從祖先類向下轉型為派生類

> let intAsObj = 20 :> obj;;

val intAsObj : obj

> intAsObj, intAsObj.ToString();;
val it : obj * string = (20, "20")

> let intDownCast = intAsObj :?> int;;

val intDownCast : int

> intDownCast, intDownCast.ToString();;
val it : int * string = (20, "20")

> let stringDownCast = intAsObj :?> string;; (* boom! *)

val stringDownCast : string

System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
   at <StartupCode$FSI_0067>.$FSI_0067._main()
stopped due to error

由於 `intAsObj` 儲存一個裝箱為 `obj` 的 `int`,因此我們可以根據需要將其向下轉型為 `int`。但是,我們不能向下轉型為 `string`,因為它是不相容的型別。向下轉型被認為是“不安全的”,因為型別檢查器無法檢測到錯誤,因此向下轉型錯誤總是會導致執行時異常。

向上轉型示例

[編輯 | 編輯原始碼]
open System

type Point = { X : int; Y : int }

[<AbstractClass>]
type Shape() =
    override x.ToString() =
        sprintf "%s, area = %f" (x.GetType().Name) (x.Area())
    
    abstract member Draw : unit -> unit 
    abstract member Area : unit -> float
    
type Circle(radius : float) =
    inherit Shape()
    
    member x.Radius = radius
    override x.Draw() = printfn "(Circle)"
    override x.Area() = Math.PI * radius * radius
    
type Rectangle(width : float, height : float) =
    inherit Shape()
    
    member x.Width = width
    member x.Height = height
    override x.Draw() = printfn "(Rectangle)"
    override x.Area() = width * height
    
type Square(width : float) =
    inherit Shape()
    
    member x.Width = width
    member x.ToRectangle() = new Rectangle(width, width)
    override x.Draw() = printfn "(Square)"
    override x.Area() = width * width
    
type Triangle(sideA : float, sideB : float, sideC : float) =
    inherit Shape()
    
    member x.SideA = sideA
    member x.SideB = sideB
    member x.SideC = sideC
    
    override x.Draw() = printfn "(Triangle)"
    override x.Area() =
        (* Heron's formula *)
        let a, b, c = sideA, sideB, sideC
        let s = (a + b + c) / 2.0
        Math.Sqrt(s * (s - a) * (s - b) * (s - c) )

let shapes =
        [(new Circle(5.0) :> Shape);
            (new Circle(12.0) :> Shape);
            (new Square(10.5) :> Shape);
            (new Triangle(3.0, 4.0, 5.0) :> Shape);
            (new Rectangle(5.0, 2.0) :> Shape)]
        (* Notice we have to cast each object as a Shape *)
            
let main() = 
    shapes
    |> Seq.iter (fun x -> printfn "x.ToString: %s" (x.ToString()) )

main()

該程式具有以下型別

type Point =
  {X: int;
   Y: int;}

type Shape =
  class
    abstract member Area : unit -> float
    abstract member Draw : unit -> unit
    new : unit -> Shape
    override ToString : unit -> string
  end

type Circle =
  class
    inherit Shape
    new : radius:float -> Circle
    override Area : unit -> float
    override Draw : unit -> unit
    member Radius : float
  end

type Rectangle =
  class
    inherit Shape
    new : width:float * height:float -> Rectangle
    override Area : unit -> float
    override Draw : unit -> unit
    member Height : float
    member Width : float
  end

type Square =
  class
    inherit Shape
    new : width:float -> Square
    override Area : unit -> float
    override Draw : unit -> unit
    member ToRectangle : unit -> Rectangle
    member Width : float
  end

type Triangle =
  class
    inherit Shape
    new : sideA:float * sideB:float * sideC:float -> Triangle
    override Area : unit -> float
    override Draw : unit -> unit
    member SideA : float
    member SideB : float
    member SideC : float
  end

val shapes : Shape list

該程式的輸出如下

x.ToString: Circle, area = 78.539816
x.ToString: Circle, area = 452.389342
x.ToString: Square, area = 110.250000
x.ToString: Triangle, area = 6.000000
x.ToString: Rectangle, area = 10.000000

公有、私有和受保護成員

[編輯 | 編輯原始碼]
前一頁:類 索引 下一頁:介面
華夏公益教科書