F# 程式設計/類
| 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
下劃線程式碼稱為類建構函式。建構函式是一種特殊的函式,用於初始化物件中的欄位。在本例中,我們的建構函式定義了兩個值number和holder,可以在類的任何地方訪問它們。您可以使用new關鍵字並向建構函式傳遞適當的引數來建立Account的例項,如下所示
let bob = new Account(123456, "Bob’s Saving")
此外,讓我們看看成員的定義方式
member x.Deposit(value) = amount <- amount + value
上面的x是當前作用域中物件的別名。大多數面嚮物件語言都提供隱式this或self變數來訪問作用域中的物件,但 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: Bob’s Savings, x.Amount: 0
val it : unit = ()
> bob.Deposit(100M);;
val it : unit = ()
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s Savings, x.Amount: 100
val it : unit = ()
> bob.Withdraw(29.95M);;
val it : unit = ()
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s 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關鍵字用於建構函式後初始化。例如,要建立一個表示股票的物件,需要傳入股票程式碼並在建構函式中初始化其餘屬性
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
您可能已經猜到,兩種語法之間的主要區別與建構函式有關:顯式語法強制程式設計師提供顯式建構函式,而隱式語法將主建構函式與類體融合在一起。但是,還有一些其他的細微差別
- 顯式語法不允許程式設計師宣告
let和do繫結。 - 即使
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語法允許程式設計師省略類定義中的class和end關鍵字,此功能通常稱為類推斷或型別種類推斷。例如,以下類定義之間沒有區別
| 類推斷 | 顯式類 |
|---|---|
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.ReferenceEquals是System.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>