前言 面向對象的思想已經非常成熟,而使用C 的程式員對面向對象也是非常熟悉,所以我就不對面向對象進行介紹了,在這篇文章中將只會介紹面向對象在F 中的使用。 F 是支持面向對象的函數式編程語言,所以你用C 能做的,用F 也可以做,而且通常代碼還會更為 簡潔 。我們先看下麵這個用C 定義的類,然後用F ...
前言
面向對象的思想已經非常成熟,而使用C#的程式員對面向對象也是非常熟悉,所以我就不對面向對象進行介紹了,在這篇文章中將只會介紹面向對象在F#中的使用。
F#是支持面向對象的函數式編程語言,所以你用C#能做的,用F#也可以做,而且通常代碼還會更為簡潔。我們先看下麵這個用C#定義的類,然後用F#來定義。
//定義一個二維點
[DebuggerDisplay("({X}, {Y})")]
public class Point2D
{
// 用於統計已實例化的數量
private static int count = 0;
// 原點
public readonly Point2D OriginalPoint = new Point2D(0, 0);
// 完整屬性X
private double x;
public double X
{
get { return this.x; }
set { this.x = value; }
}
// 自動屬性Y
public double Y { get; set; }
// 到原點的距離
public double LengthToOriginal
{
get { return this.GetDistance(this.OriginalPoint); }
}
// 預設構造函數
public Point2D() : this(0,0) {}
// 包含兩個參數的構造函數
public Point2D(double x, double y)
{
this.X = x;
this.Y = y;
count++; // 創建新點時,數量遞增1
}
// 將(x,y)數值轉為Point2D對象的靜態方法
public static Point2D FromXY(double x, double y)
{
return new Point2D(x, y);
}
// 計算到指定點的距離
public virtual double GetDistance(Point2D point)
{
var xDif = this.X - point.X;
var yDif = this.Y - point.Y;
var distance = Math.Sqrt(xDif * xDif + yDif * yDif);
return distance;
}
//重寫ToString方法
public override string ToString()
{
return String.Format("Point2D({0}, {1})", this.X, this.Y);
}
}
定義類
在F#中定義類不需要class
關鍵字,除非定義一個空的類。
type Point2D() = class //定義一個空類
end
type internal Point2D() = class //定義一個空的internal類
end
type Point2D() = //定義一個只包含欄位x的類
let mutable x = 0.0
不像C#,在F#中,類的訪問修飾符預設是public
的,所以internal
就需要手動指定。並且,在F#,不支持protected
訪問修飾符,在F#中應該使用介面,對象表達式和高階函數來實現類似功能。
欄位(Field)
在上一例代碼中我們已經定義了一個欄位x
,但類中用let
定義的欄位(包括後面介紹的函數)均為private
的。若需要定義其他訪問修飾符的欄位,需要使用val
定義。
type Point2D() =
[<DefaultValue>] val mutable public x : float
其中DefaultValue
特性用於將支持零值初始化的類型(具有零值的基元類型、可為null的類等)欄位初始化為零值。
屬性(Property)和索引器
使用member
關鍵字定義屬性,註意F#中的屬性需要有一個對象標識符(類似C#中的this
)作為首碼。
type Point2D() =
let mutable x = 0.0
// 定義屬性X,其中set為private的。
member this.X with get() = x
and private set(v) = x <- v
member val Y = 0.0 with get, set //自動屬性
在F#中,this
也可以使用其他名稱來代替。除了this
(C++和C#的習慣),有的人還使用self
(Python等的習慣)、me
(VB.NET的習慣)等。若不需要在屬性或方法內部使用到,一般還習慣使用__
(兩個下劃線)來作為對象標識符。
而像C#set
方法中的value
,F#中也需要自己指定,如例子中使用v
。當然get
和set
都可以省略使之成為只寫或只讀屬性。
遺憾的是自動屬性中不支持分別定義訪問修飾符(如C#中的{get; private set}
),只能使用完整屬性來定義。
方法(Method)
方法同樣使用member
定義,而靜態方法只需在member
前添加static
關鍵字。
type Point2D() =
…… //因為篇幅省略屬性X,Y的代碼
// 定義一個函數來獲取指定點到當前點的距離
member this.GetDistance (point:Point2D) =
let xDif = this.X - point.X
let yDif = this.Y - point.Y
let distance = sqrt (xDif**2. + yDif**2.)
distance
static member FromXY (x:double, y:double) =
let point = Point2D()
point.X <- x
point.Y <- y
point
重載方法
F#類中的方法重載與C#一樣,只需重新定義一個同名成員函數,且簽名不同即可。下麵實現一個接受int
類型的FromXY
方法:
static member FromXY (x:int, y:int) =
Point2D.FromXY(float x,float y)
構造函數與實例化
F#中有兩種構造函數,一為主構造函數(也稱隱式構造函數),上面例子中在類型定義後面的()
即表示一個無參構造函數。
另一種構造函數(顯示定義)是可選的,在類里定義一個new
函數即可。但new
函數必須滿足以下條件:
- 返回類型必須為該類
- 不使用
member
和對象標識符。 - 使用一個元組或
unit
作為參數。
我們改造上面的代碼:
type Point2D (xValue:double, yValue:double) =
let mutable x = xValue
member val Y = yValue with get, set
new() = Point2D(0.0,0.0)
其中,調用無參構造函數時,則使用主構造函數實例化,且兩個參數均為0.0
。
若要為構造函數添加訪問修飾符,寫在其之前即可。
type internal Point2D internal (xValue:double, yValue:double) =
private new() = Point2D(0.0,0.0)
需要註意的是,在類型定義之後若不提供主構造函數,F#並不像C#那樣有一個無參的構造函數。
下麵的代碼能通過編譯,但你無法進行實例化:
type Point2D = class end
在類中的let
綁定會在調用主構造函數時運行,但如果需要在主構造函數中執行其他操作,需要使用do
綁定。
type Point2D(xValue:double, yValue:double) =
let mutable x = xValue
static let mutable count = 0
do
count <- count + 1
註意let
代碼和do
代碼必須在member
之前。而do
語句中必須返回unit
,可使用ignore
函數丟棄返回值。
非靜態的do
語句會在實例化時執行,而靜態的do
語句會在第一次使用該類型時執行。而在沒有主構造函數的類中,無法使用let
和do
語句。
如果需要在do
語句中訪問其他方法,則需要類級別的對象標識符,在類型定義後使用as
關鍵字指定;若想在非主構造函數中執行額外的代碼,使用then
關鍵字:
type Point2D (xValue:double, yValue:double) as self =
do //省略其他代碼
self.Print "在主構造函數中。"
new() as this= //主構造函數中使用self,此處用this。定義為任何名稱都可以
Point2D(0.0,0.0)
then this.Print "在無參構造函數中。"
member this.Print str = printfn "%s" str
但這樣有一個問題:在執行do
代碼訪問Print
函數時,需要self
已經實例化好,但因為Y
自動屬性,編譯時會在do
後面插入一個Y@
欄位(即Y的back-end欄位),此時並未初始化。即違反了“先定義後引用”的原則。
所以,若在構造函數中需要訪問類中的方法,只能將Y
也更改為完整屬性,並且x
和y
欄位的let
綁定必須在do
之前。
實例化
在上面代碼中的new()
構造函數中,實例化了一個Point2D(0.0,0.0)
對象。在F#中,實例化時可以不使用new
關鍵字,但有特例,在後面會介紹。
在C#中,我們可以使用對象初始化器(Object Initializers)在初始化時對屬性進行賦值,在F#中,也有類似功能:
var pt = new Point2D() {X = 3.0}; //C#中使用對象初始化器
let pt = Point2D(X=3.0) //F#中使用對象初始化器,註意此處調用的是無參構造函數
到此,可將上面的FromXY
方法進行簡化:
static member FromXY (x:double, y:double) =
Point2D(x,y)
抽象類(Abstract Class)和密封(Sealed)
要將類定義為抽象類或密封類,只需要在類定義前加上[<AbstractClass>]
或[<Sealed>]
特性。
抽象方法和虛方法
在F#中,沒有提供virtual
關鍵字來定義虛方法。而使用定義一個抽象方法,並提供預設實現來代替。
type Point2D(xValue:double, yValue:double) as this=
……
// 實現開頭C#代碼中的GetDistance虛方法,此處省略其他代碼
abstract GetDistance : point:Point2D -> double
default this.GetDistance(point) =
let xDif = this.X - point.X
let yDif = this.Y - point.Y
let distance = sqrt (xDif**2. + yDif**2.)
distance
關鍵字abstract
用來定義抽象方法,只給出函數簽名,並且函數名前不需使用對象標識符;而default
預設實現語句中也不需使用member
關鍵字。
與C#一樣,在派生類中進行override關鍵字重寫基類中的虛方法:
type Point2D(xValue:double, yValue:double) =
……
//重寫Object類中的ToString虛方法,此處省略其他代碼
override this.ToString() = sprintf "Point2D(%f, %f)" this.X this.Y
在C#中,若要重寫非虛方法,可使用new
關鍵字。F#中沒有此關鍵字,但仍然可以重寫非虛方法,只是編譯器會給出警告。類的繼承與介面的實現在下一篇中介紹。
索引器(Indexer)及切片(Slice)
索引器
索引器,顧名思義就是可以使用索引來操作對象。在F#中,只需定義一個名為Item
的屬性或方法即可讓該類的對象使用索引器。當然,若Item
定義為方法,則索引器為只讀的。
我們用下來的代碼定義一個可變的字元串類。
open System.Collections.Generic
type WordBuilder(startingLetters : string) =
let m_letters = new List<char>(startingLetters)
member this.Item
with get idx = m_letters.[idx]
and set idx c = m_letters.[idx] <- c
member this.Word = new string (m_letters.ToArray())
let wb = WordBuilder("I Love C#")
wb.[7] <- 'F'
printfn "%s" wb.Word //輸出為:"I Love F#"
註意在使用索引訪問時,需要使用點訪問(.[]
)。
切片
切片和索引器類似,不過索引器訪問的是一個元素,而切片訪問的是數據的集合。實現切片訪問需要添加GetSlice
方法,切片是只讀的。
open System.Collections.Generic
type WordBuilder(startingLetters : string) =
…… //其他代碼省略
member this.GetSlice(lower:int option,upper:int option) =
letters
|> Seq.skip (lower.Value)
|> Seq.take (upper.Value - lower.Value + 1)
|> Seq.toArray
let wb = WordBuilder("I Love C#")
printfn "%A" wb.[2..5] //輸出為:[|'L'; 'o'; 'v'; 'e'|]
此切片方法並未實現完整,但已可瞭解實現方法。註意GetSlice
的參數必須是'a option
泛型類型, 其中option
類型將在後面再介紹,可理解為類似於Nullable<int>
。
結語
通過以上的介紹,熟悉C#面向對象的朋友內容應該能瞭解F#與C#間語法的差別了。至少也可以將C#代碼按面向對象的風格翻譯成F#代碼了,下麵是文章開頭C#代碼的F#版本:
open System.Diagnostics
[<DebuggerDisplay("({X}, {Y})")>]
type Point2D (xValue:double, yValue:double) as self =
let mutable x = xValue
static let mutable count = 0
do
self.OriginalPoint2 <- new Point2D()
count <- count + 1
member __.OriginalPoint with get () = Point2D()
member this.LengthToOriginal
with get () = this.GetDistance(this.OriginalPoint)
new() = Point2D(0.0,0.0)
member __.X with get() = x
and private set(v) = x <- v
member val Y = yValue with get, set
// 一個虛函數:獲取指定點到當前點的距離
abstract GetDistance : point:Point2D -> double
default this.GetDistance(point) =
let xDif = this.X - point.X
let yDif = this.Y - point.Y
let distance = sqrt (xDif**2. + yDif**2.)
distance
static member FromXY (x:double, y:double) =
Point2D(x,y)
override this.ToString() = sprintf "Point2D(%f, %f)" this.X this.Y
需要註意的是C#代碼中的OriginalPoint
公開欄位在此處以只讀屬性實現。主要是因為:
- F#中的
let
綁定的欄位只能為private
,無法設置為public。 val
綁定的顯示欄位(包括自動屬性)需要在主構造函數中初始化,而OriginalPoint
的類型也為Point2D
,在此會形成遞歸調用而引發StackOverflowException
異常。
最後再簡單說下類中let
和val
的區別:
let
始終是private
的,且需要有主構造函數才能定義,因為let語句在主構造函數中運行(同do
語句)。val
預設為public
的,並且可添加修飾符使之成為internal
或private
的。若有主構造函數,需要為val
欄位添加DefaultValue
特性。val
與member
關鍵字結合使用,可聲明自動屬性。- 在類中訪問
val
欄位,需要使用對象標識符,而let
欄位不需要。
註意,此DefaultValue
位於Microsoft.FSharp.Core
命名空間,不要和C#中常用的位於System.ComponentModel
的DefaultValue
混淆。
本文發表於博客園。 轉載請註明源鏈接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-6.html。