跳轉至內容

F# 程式設計/類

來自 Wikibooks,開放世界中的開放書籍
上一頁:運算子過載 索引 下一頁:繼承
F#:類和物件

在現實世界中,物件是“真實”的事物。貓、人、電腦和一卷膠帶都是以有形意義存在的“真實”事物。當我們思考這些事物時,我們可以從多個屬性的角度來廣泛地描述它們

  • 屬性:人有姓名,貓有四條腿,電腦有價格標籤,膠帶是粘性的。
  • 行為:人閱讀報紙,貓整天睡覺,電腦處理資料,膠帶將東西粘到其他東西上。
  • 型別/群體成員關係:員工是人的一種型別,貓是寵物,戴爾和 Mac 是電腦的型別,膠帶屬於更廣泛的粘合劑家族。

在程式設計世界中,“物件”簡單來說就是現實世界中某事物的模型。面向物件程式設計 (OOP) 存在是因為它允許程式設計師對現實世界的實體進行建模,並在程式碼中模擬它們的互動。就像它們在現實世界中的對應物一樣,計算機程式設計中的物件具有屬性和行為,並且可以根據其型別進行分類。

雖然我們當然可以建立代表貓、人、粘合劑的物件,但物件也可以代表不太具體的事物,例如銀行賬戶或業務規則。

儘管 OOP 的範圍已經擴充套件到包括一些高階概念,例如設計模式和應用程式的大規模體系結構,但此頁面將保持簡單,並將 OOP 的討論限制在資料建模。

定義物件

[編輯 | 編輯原始碼]

在建立物件之前,應定義其屬性和函式。您在中定義物件的屬性和方法。實際上,定義類的語法有兩種:隱式語法和顯式語法。

隱式類構造

[編輯 | 編輯原始碼]

隱式類語法定義如下

type TypeName optional-type-arguments arguments [ as ident ] =
    [ inherit type { as base } ]
    [ let-binding | let-rec bindings ] *
    [ do-statement ] *
    [ abstract-binding | member-binding | interface-implementation ] *
方括號中的元素是可選的,後跟*的元素可以出現零次或多次。

上面的語法並不像看起來那麼令人生畏。這是一個用隱式風格編寫的簡單類

type Account(number : int, holder : string) = class
    let mutable amount = 0m

    member x.Number = number
    member x.Holder = holder
    member x.Amount = amount
    
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end

上面的程式碼定義了一個名為Account的類,它具有三個屬性和兩個方法。讓我們仔細看看以下內容

type Account(number : int, holder : string) = class

下劃線程式碼稱為類建構函式。建構函式是一種特殊的函式,用於初始化物件中的欄位。在本例中,我們的建構函式定義了兩個值numberholder,可以在類的任何地方訪問它們。您可以使用new關鍵字並向建構函式傳遞適當的引數來建立Account的例項,如下所示

let bob = new Account(123456, "Bob’s Saving")

此外,讓我們看看成員的定義方式

member x.Deposit(value) = amount <- amount + value

上面的x是當前作用域中物件的別名。大多數面嚮物件語言都提供隱式thisself變數來訪問作用域中的物件,但 F# 要求程式設計師建立自己的別名。

建立了Account的例項後,可以使用.propertyName表示法訪問其屬性。以下是在 FSI 中的示例

> let printAccount (x : Account) =
    printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount;;

val printAccount : Account -> unit

> let bob = new Account(123456, "Bob’s Savings");;

val bob : Account

> printAccount bob;;
x.Number: 123456, x.Holder: Bobs Savings, x.Amount: 0
val it : unit = ()

> bob.Deposit(100M);;
val it : unit = ()

> printAccount bob;;
x.Number: 123456, x.Holder: Bobs Savings, x.Amount: 100
val it : unit = ()

> bob.Withdraw(29.95M);;
val it : unit = ()

> printAccount bob;;
x.Number: 123456, x.Holder: Bobs Savings, x.Amount: 70.05

讓我們在一個真實的程式中使用這個類

open System

type Account(number : int, holder : string) = class
    let mutable amount = 0m

    member x.Number = number
    member x.Holder = holder
    member x.Amount = amount
    
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end

let homer = new Account(12345, "Homer")
let marge = new Account(67890, "Marge")

let transfer amount (source : Account) (target : Account) =
    source.Withdraw amount
    target.Deposit amount
    
let printAccount (x : Account) =
    printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount
    
let main() =
    let printAccounts() =
        [homer; marge] |> Seq.iter printAccount
    
    printfn "\nInializing account"
    homer.Deposit 50M
    marge.Deposit 100M
    printAccounts()
    
    printfn "\nTransferring $30 from Marge to Homer"
    transfer 30M marge homer
    printAccounts()
    
    printfn "\nTransferring $75 from Homer to Marge"
    transfer 75M homer marge
    printAccounts()
        
main()

該程式具有以下型別

type Account =
  class
    new : number:int * holder:string -> Account
    member Deposit : value:decimal -> unit
    member Withdraw : value:decimal -> unit
    member Amount : decimal
    member Holder : string
    member Number : int
  end
val homer : Account
val marge : Account
val transfer : decimal -> Account -> Account -> unit
val printAccount : Account -> unit

該程式輸出以下內容

Initializing account
x.Number: 12345, x.Holder: Homer, x.Amount: 50
x.Number: 67890, x.Holder: Marge, x.Amount: 100

Transferring $30 from Marge to Homer
x.Number: 12345, x.Holder: Homer, x.Amount: 80
x.Number: 67890, x.Holder: Marge, x.Amount: 70

Transferring $75 from Homer to Marge
x.Number: 12345, x.Holder: Homer, x.Amount: 5
x.Number: 67890, x.Holder: Marge, x.Amount: 145

使用do關鍵字的示例

[編輯 | 編輯原始碼]

do關鍵字用於建構函式後初始化。例如,要建立一個表示股票的物件,需要傳入股票程式碼並在建構函式中初始化其餘屬性

open System
open System.Net

type Stock(symbol : string) = class
    let url =
        "http://download.finance.yahoo.com/d/quotes.csv?s=" + symbol + "&f=sl1d1t1c1ohgv&e=.csv"

    let mutable _symbol = String.Empty
    let mutable _current = 0.0
    let mutable _open = 0.0
    let mutable _high = 0.0
    let mutable _low = 0.0
    let mutable _volume = 0

    do
	(* We initialize our object in the do block *)

        let webClient = new WebClient()

       	(* Data comes back as a comma-separated list, so we split it
           on each comma *)
        let data = webClient.DownloadString(url).Split([|','|])

        _symbol <- data.[0]
        _current <- float data.[1]
        _open <- float data.[5]
        _high <- float data.[6]
        _low <- float data.[7]
        _volume <- int data.[8]
    
    member x.Symbol = _symbol
    member x.Current = _current
    member x.Open = _open
    member x.High = _high
    member x.Low = _low
    member x.Volume = _volume
end

let main() =
    let stocks = 
        ["msft"; "noc"; "yhoo"; "gm"]
        |> Seq.map (fun x -> new Stock(x))
        
    stocks |> Seq.iter (fun x -> printfn "Symbol: %s (%F)" x.Symbol x.Current)
    
main()

該程式具有以下型別

type Stock =
  class
    new : symbol:string -> Stock
    member Current : float
    member High : float
    member Low : float
    member Open : float
    member Symbol : string
    member Volume : int
  end

該程式輸出以下內容(您的輸出可能會有所不同)

Symbol: "MSFT" (19.130000)
Symbol: "NOC" (43.240000)
Symbol: "YHOO" (12.340000)
Symbol: "GM" (3.660000)
注意:在類定義中可以有任意數量的do語句,儘管通常不需要超過一個。

顯式類定義

[編輯 | 編輯原始碼]

以顯式風格編寫的類遵循以下格式

type TypeName =
    [ inherit type ]
    [ val-definitions ]
    [ new ( optional-type-arguments arguments ) [ as ident ] =
      { field-initialization }
      [ then constructor-statements ]
    ] *
    [ abstract-binding | member-binding | interface-implementation ] *

這是一個使用顯式語法定義的類

type Line = class
    val X1 : float
    val Y1 : float
    val X2 : float
    val Y2 : float
    
    new (x1, y1, x2, y2) =
        { X1 = x1; Y1 = y1;
            X2 = x2; Y2 = y2}
            
    member x.Length =
        let sqr x = x * x
        sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end

每個val在我們的物件中定義一個欄位。與其他面嚮物件語言不同,F# 不會隱式地將類中的欄位初始化為任何值。相反,F# 要求程式設計師定義一個建構函式,並使用值顯式初始化物件中的每個欄位。

我們可以使用then塊執行一些建構函式後處理,如下所示

type Line = class
    val X1 : float
    val Y1 : float
    val X2 : float
    val Y2 : float
    
    new (x1, y1, x2, y2) as this =
        { X1 = x1; Y1 = y1;
            X2 = x2; Y2 = y2;}
        then
            printfn "Line constructor: {(%F, %F), (%F, %F)}, Line.Length: %F"
                this.X1 this.Y1 this.X2 this.Y2 this.Length
            
    member x.Length =
        let sqr x = x * x
        sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end

請注意,我們必須在建構函式後新增一個別名(new (x1, y1, x2, y2) as this),以訪問正在構造的物件的欄位。每次建立Line物件時,建構函式都會列印到控制檯。我們可以使用 fsi 演示這一點

> let line = new Line(1.0, 1.0, 4.0, 2.5);;

val line : Line

Line constructor: {(1.000000, 1.000000), (4.000000, 2.500000)}, Line.Length: 3.354102

使用兩個建構函式的示例

[編輯 | 編輯原始碼]

由於建構函式是顯式定義的,因此我們可以選擇提供多個建構函式。

open System
open System.Net

type Car = class
    val used : bool
    val owner : string
    val mutable mileage : int
    
    (* first constructor *)
    new (owner) =
        { used = false;
            owner = owner;
            mileage = 0 }
    
    (* another constructor *)
    new (owner, mileage) =
        { used = true;
            owner = owner;
            mileage = mileage }
end

let main() =
    let printCar (c : Car) =
        printfn "c.used: %b, c.owner: %s, c.mileage: %i" c.used c.owner c.mileage
    
    let stevesNewCar = new Car("Steve")
    let bobsUsedCar = new Car("Bob", 83000)
    let printCars() =
        [stevesNewCar; bobsUsedCar] |> Seq.iter printCar
    
    printfn "\nCars created"
    printCars()
    
    printfn "\nSteve drives all over the state"   
    stevesNewCar.mileage <- stevesNewCar.mileage + 780
    printCars()
    
    printfn "\nBob commits odometer fraud"
    bobsUsedCar.mileage <- 0
    printCars()
    
main()

該程式具有以下型別

type Car =
  class
    val used: bool
    val owner: string
    val mutable mileage: int
    new : owner:string * mileage:int -> Car
    new : owner:string -> Car
  end

請注意,我們的val欄位包含在類定義的公共介面中。

該程式輸出以下內容

Cars created
c.used: false, c.owner: Steve, c.mileage: 0
c.used: true, c.owner: Bob, c.mileage: 83000

Steve drives all over the state
c.used: false, c.owner: Steve, c.mileage: 780
c.used: true, c.owner: Bob, c.mileage: 83000

Bob commits odometer fraud
c.used: false, c.owner: Steve, c.mileage: 780
c.used: true, c.owner: Bob, c.mileage: 0

隱式和顯式語法之間的差異

[編輯 | 編輯原始碼]

您可能已經猜到,兩種語法之間的主要區別與建構函式有關:顯式語法強制程式設計師提供顯式建構函式,而隱式語法將主建構函式與類體融合在一起。但是,還有一些其他的細微差別

  • 顯式語法不允許程式設計師宣告letdo繫結。
  • 即使val欄位可以在隱式語法中使用,它們也必須具有屬性[<DefaultValue>]並且是可變的。在這種情況下,使用let繫結更方便。當需要公開時,可以新增公共member訪問器。
  • 在隱式語法中,主建構函式引數在整個類體中可見。透過使用此功能,無需編寫將建構函式引數複製到例項成員的程式碼。
  • 雖然兩種語法都支援多個建構函式,但當使用隱式語法宣告其他建構函式時,它們必須呼叫主建構函式。在顯式語法中,所有建構函式都使用 new() 宣告,並且沒有需要從其他建構函式中引用的主建構函式。
具有主(隱式)建構函式的類 僅具有顯式建構函式的類
// The class body acts as a constructor
type Car1(make : string, model : string) = class
    // x.Make and x.Model are property getters
    // (explained later in this chapter)
    // Notice how they can access the
    // constructor parameters directly
    member x.Make = make
    member x.Model = model

    // This is an extra constructor.
    // It calls the primary constructor
    new () = Car1("default make", "default model")
end
type Car2 = class
    // In this case, we need to declare
    // all fields and their types explicitly 
    val private make : string
    val private model : string

    // Notice how field access differs
    // from parameter access
    member x.Make = x.make
    member x.Model = x.model

    // Two constructors
    new (make : string, model : string) = {
        make = make
        model = model
    }
    new () = {
        make = "default make"
        model = "default model"
    }
end

通常,程式設計師可以使用隱式或顯式語法來定義類。但是,在實踐中隱式語法使用得更多,因為它往往會導致更短、更易讀的類定義。

類推斷

[編輯 | 編輯原始碼]

F# 的#light語法允許程式設計師省略類定義中的classend關鍵字,此功能通常稱為類推斷型別種類推斷。例如,以下類定義之間沒有區別

類推斷 顯式類
type Product(make : string, model : string) =
    member x.Make = make
    member x.Model = model
type Car(make : string, model : string) = class    
    member x.Make = make
    member x.Model = model
end

這兩個類都編譯成相同的位元組碼,但是使用類推斷的程式碼允許我們省略一些不必要的關鍵字。

類推斷和顯式類樣式都被認為是可以接受的。至少,在編寫 F# 庫時,不要使用類推斷定義一半的類,而使用顯式類樣式定義另一半的類——選擇一種樣式,並在整個專案中所有類中一致地使用它。

類成員

[編輯 | 編輯原始碼]

例項成員和靜態成員

[編輯 | 編輯原始碼]

您可以向物件新增兩種型別的成員

  • 例項成員,只能從使用new關鍵字建立的物件例項中呼叫。

  • 靜態成員,不與任何物件例項關聯。

以下類包含一個靜態方法和一個例項方法

type SomeClass(prop : int) = class
    member x.Prop = prop
    static member SomeStaticMethod = "This is a static method"
end

我們使用className.methodName的形式呼叫靜態方法。我們透過建立類的例項並使用classInstance.methodName呼叫方法來呼叫例項方法。以下是在fsi中的演示

> SomeClass.SomeStaticMethod;; (* invoking static method *)
val it : string = "This is a static method"

> SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)

  SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)
  ^^^^^^^^^^^^^^^

stdin(78,1): error FS0191: property 'Prop' is not static.

> let instance = new SomeClass(5);;

val instance : SomeClass

> instance.Prop;; (* now we have an instance, we can call our instance method *)
val it : int = 5

> instance.SomeStaticMethod;; (* can't invoke static method from instance *)

  instance.SomeStaticMethod;; (* can't invoke static method from instance *)
  ^^^^^^^^^^^^^^^^^^^^^^^^^^

stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.

當然,我們也可以從傳遞靜態方法中的物件呼叫例項方法,例如,假設我們在上面定義的物件中添加了一個Copy方法

type SomeClass(prop : int) = class
    member x.Prop = prop
    static member SomeStaticMethod = "This is a static method"
    static member Copy (source : SomeClass) = new SomeClass(source.Prop)
end

我們可以在fsi中試驗此方法

> let instance = new SomeClass(10);;

val instance : SomeClass

> let shallowCopy = instance;; (* copies pointer to another symbol *)

val shallowCopy : SomeClass

> let deepCopy = SomeClass.Copy instance;; (* copies values into a new object *)

val deepCopy : SomeClass

> open System;;

> Object.ReferenceEquals(instance, shallowCopy);;
val it : bool = true

> Object.ReferenceEquals(instance, deepCopy);;
val it : bool = false

Object.ReferenceEqualsSystem.Object類上的一個靜態方法,它確定兩個物件例項是否為同一個物件。如上所示,我們的Copy方法接受一個SomeClass的例項並訪問其Prop屬性。

何時應該使用靜態方法而不是例項方法?

當.NET框架的設計人員設計System.String類時,他們必須決定Length方法應該放在哪裡。他們可以選擇將屬性設為例項方法(s.Length)或設為靜態方法(String.GetLength(s))。.NET設計人員選擇將Length設為例項方法,因為它是一個字串的內在屬性。

另一方面,String類還有一些靜態方法,包括String.Concat,它接受一個字串列表並將它們連線在一起。連線字串與例項無關,它不依賴於任何特定字串的例項成員。

以下一般原則適用於所有面向物件語言

  • 例項成員應該用於訪問物件的內在屬性,例如stringInstance.Length
  • 當例項方法依賴於特定物件例項的狀態時,應該使用例項方法,例如stringInstance.Contains
  • 當預計程式設計師希望在派生類中重寫該方法時,應該使用例項方法。
  • 靜態方法不應依賴於物件的特定例項,例如Int32.TryParse
  • 只要輸入保持不變,靜態方法應該返回相同的值。
  • 常量,即對於任何類例項都不變的值,應該宣告為靜態成員,例如System.Boolean.TrueString

獲取器和設定器

[編輯 | 編輯原始碼]

獲取器和設定器是特殊的函式,允許程式設計師使用方便的語法讀取和寫入成員。獲取器和設定器使用以下格式編寫

    member alias.PropertyName
        with get() = some-value
        and set(value) = some-assignment

這是一個使用fsi的簡單示例

> type IntWrapper() = class
    let mutable num = 0
    
    member x.Num
        with get() = num
        and set(value) = num <- value
end;;

type IntWrapper =
  class
    new : unit -> IntWrapper
    member Num : int
    member Num : int with set
  end

> let wrapper = new IntWrapper();;

val wrapper : IntWrapper

> wrapper.Num;;
val it : int = 0

> wrapper.Num <- 20;;
val it : unit = ()

> wrapper.Num;;
val it : int = 20

獲取器和設定器用於將私有成員暴露給外部世界。例如,我們的Num屬性允許使用者讀取/寫入內部num變數。由於獲取器和設定器是美化的函式,因此我們可以在寫入值到內部變數之前使用它們來清理輸入。例如,我們可以透過修改我們的類如下,修改我們的IntWrapper類來將我們的值限制在0到10之間

type IntWrapper() = class
    let mutable num = 0
    
    member x.Num
        with get() = num
        and set(value) =
            if value > 10 || value < 0 then
                raise (new Exception("Values must be between 0 and 10"))
            else
                num <- value
end

我們可以在fsi中使用此類

> let wrapper = new IntWrapper();;

val wrapper : IntWrapper

> wrapper.Num <- 5;;
val it : unit = ()

> wrapper.Num;;
val it : int = 5

> wrapper.Num <- 20;;
System.Exception: Values must be between 0 and 10
   at FSI_0072.IntWrapper.set_Num(Int32 value)
   at <StartupCode$FSI_0076>.$FSI_0076._main()
stopped due to error

向記錄和聯合新增成員

[編輯 | 編輯原始碼]

向記錄和聯合型別新增成員也同樣容易。

記錄示例

> type Line =
    { X1 : float; Y1 : float;
        X2 : float; Y2 : float }
    with    
        member x.Length =
            let sqr x = x * x
            sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2))
        
        member x.ShiftH amount =
            { x with X1 = x.X1 + amount; X2 = x.X2 + amount }
            
        member x.ShiftV amount =
            { x with Y1 = x.Y1 + amount; Y2 = x.Y2 + amount };;

type Line =
  {X1: float;
   Y1: float;
   X2: float;
   Y2: float;}
  with
    member ShiftH : amount:float -> Line
    member ShiftV : amount:float -> Line
    member Length : float
  end

> let line = { X1 = 1.0; Y1 = 2.0; X2 = 5.0; Y2 = 4.5 };;

val line : Line

> line.Length;;
val it : float = 4.716990566

> line.ShiftH 10.0;;
val it : Line = {X1 = 11.0;
                 Y1 = 2.0;
                 X2 = 15.0;
                 Y2 = 4.5;}

> line.ShiftV -5.0;;
val it : Line = {X1 = 1.0;
                 Y1 = -3.0;
                 X2 = 5.0;
                 Y2 = -0.5;}

聯合示例

> type shape =
    | Circle of float
    | Rectangle of float * float
    | Triangle of float * float
    with
        member x.Area =
            match x with
            | Circle(r) -> Math.PI * r * r
            | Rectangle(b, h) -> b * h
            | Triangle(b, h) -> b * h / 2.0
            
        member x.Scale value =
            match x with
            | Circle(r) -> Circle(r + value)
            | Rectangle(b, h) -> Rectangle(b + value, h + value)
            | Triangle(b, h) -> Triangle(b + value, h + value);;

type shape =
  | Circle of float
  | Rectangle of float * float
  | Triangle of float * float
  with
    member Scale : value:float -> shape
    member Area : float
  end

> let mycircle = Circle(5.0);;

val mycircle : shape

> mycircle.Area;;
val it : float = 78.53981634

> mycircle.Scale(7.0);;
val it : shape = Circle 12.0

泛型類

[編輯 | 編輯原始碼]

可以建立接受泛型型別的類

type 'a GenericWrapper(initialVal : 'a) = class
    let mutable internalVal = initialVal
    
    member x.Value
        with get() = internalVal
        and set(value) = internalVal <- value
end

我們可以在FSI中使用此類,如下所示

> let intWrapper = new GenericWrapper<_>(5);;

val intWrapper : int GenericWrapper

> intWrapper.Value;;
val it : int = 5

> intWrapper.Value <- 20;;
val it : unit = ()

> intWrapper.Value;;
val it : int = 20

> intWrapper.Value <- 2.0;; (* throws an exception *)

  intWrapper.Value <- 2.0;; (* throws an exception *)
  --------------------^^^^

stdin(156,21): error FS0001: This expression has type
	float
but is here used with type
	int.

> let boolWrapper = new GenericWrapper<_>(true);;

val boolWrapper : bool GenericWrapper

> boolWrapper.Value;;
val it : bool = true

泛型類幫助程式設計師將類泛化以對多種不同型別進行操作。它們的使用方式與已經在F#中看到的其他所有泛型型別(如列表、集合、對映和聯合型別)基本相同。

模式匹配物件

[編輯 | 編輯原始碼]

雖然我們不能像對列表和聯合型別那樣完全以相同的方式根據物件的結構來匹配物件,但F#允許程式設計師使用以下語法根據型別進行匹配

match arg with
| :? type1 -> expr
| :? type2 -> expr

這是一個使用型別測試的示例

type Cat() = class
    member x.Meow() = printfn "Meow"
end

type Person(name : string) = class
    member x.Name = name
    member x.SayHello() = printfn "Hi, I'm %s" x.Name
end

type Monkey() = class
    member x.SwingFromTrees() = printfn "swinging from trees"
end

let handlesAnything (o : obj) =
    match o with
    | null -> printfn "<null>"
    | :? Cat as cat -> cat.Meow()
    | :? Person as person -> person.SayHello()
    | _ -> printfn "I don't recognize type '%s'" (o.GetType().Name)

let main() =
    let cat = new Cat()
    let bob = new Person("Bob")
    let bill = new Person("Bill")
    let phrase = "Hello world!"
    let monkey = new Monkey()
    
    handlesAnything cat
    handlesAnything bob
    handlesAnything bill
    handlesAnything phrase
    handlesAnything monkey
    handlesAnything null

main()

此程式輸出

Meow
Hi, I'm Bob
Hi, I'm Bill
I don't recognize type 'String'
I don't recognize type 'Monkey'
<null>
上一頁:運算子過載 索引 下一頁:繼承
華夏公益教科書