前言
面向對象三大基本特性:封裝、繼承、多態。上一篇中介紹了類的定義,下面就了解下F#中繼承和多態的使用吧。😋
同樣的,面向對象的基礎概念不多說,就對比下語法,並簡單說明下應該注意的地方。
繼承
對象表達式(Object Expressions)
在介紹繼承之前,先介紹一下F#面向對象中常用的一個表達式:對象表達式。它用於基於現有類型創建匿名對象類型,比如有時候只希望在一個或少數幾個對象中修改成員函數的實現,這時候不一定要重新定義一個現有類型的派生類然后實例化。
可在創建對象時通過關鍵字with
提供新的函數實現代碼:
let oriPt = {
new Point2D() with
member __.ToString() = "我是原點"
}
oriPt
使用對象表達式實例化,並重寫了基類的ToString
方法。而如果在C#中我們就需要先定義一個派生類,然后重寫ToString
方法。其實這也是繼承,只是這樣會減少創建新的命名類型所需的代碼和開銷。
對象表達式需要放在一對大括號({}
)中,其中的new
不可省略,且修改的成員函數必須是虛方法(包括抽象方法)。
繼承的實現
繼承是面向對象的一大特性,下面分別是C#和F#中的語法對比。定義一個繼承於Point2D
的類:
public class Particle : Point2D
{
public double Mass { get; set; }
}
type NamedPoint2D() =
inherit Point2D()
member val Name = "pt2d" with get, set
在F#中,在F#中,派生類中使用關鍵字inherit
指定所繼承的基類及其構造函數,若子類需要調用基類方法,同樣使用base
關鍵字。base
無法像this
一樣自定義。😃
如果有多個構造函數,通常可以在不使用主構造函數的情況下使用對象表達式返回對象,或者使用使用其他構造函數實例化返回,並用then
關鍵字指定額外的執行語句。
type NamedPoint2D =
inherit Point2D
val mutable Name:string
new (x,y) ={
inherit Point2D(x,y)
Name = ""
}
new (name) = {
inherit Point2D();
Name = name
}
new (x,y,name) as this =
NamedPoint2D(x,y)
then
this.Name <- name
接口
接口的定義和使用
抽象類決定一個對象“是什么”,而接口決定了一個對象“具有什么功能”。我們先看F#中的定義:
type I2DLocation = interface //完整定義方式
abstract member X : float with get, set
abstract member Y : float with get, set
end
type I2DLocation = //簡要定義方式
abstract member X : float with get, set
abstract member Y : float with get, set
在F#中,若不使用interface
關鍵字顯示定義,只要類的所有成員都為抽象(abstract
)的,就會被類型推斷系統推斷為接口。
接口在F#需要顯示實現,所以在實現接口后,使用時也需要轉換至接口類型,否則無法調用接口的屬性或方法。
type Point2D(xValue:double, yValue:double) as this=
…… //省略其他代碼
interface I2DLocation with
member this.X with get() = this.X and set(v) = this.X <- v
member this.Y with get() = this.Y and set(v) = this.Y <- v
let pt = Point2D()
let l = pt :> I2DLocation //使用向上轉換符,因為實現的接口已在編譯期確定
printfn "x=%f,y=%f" l.X l.Y
使用對象表達式實現接口的匿名類型
[對象表達式](#對象表達式(Object Expressions))不僅可基於類創建匿名類型,同樣可基於接口創建匿名類型。在C#中,我們在基於IComparer
比較器進行比較時,都要定義一個實現IComparer
的類。而在F#中,因為有對象表達式,我們可以省去很多代碼。假設我們將一些Point2d
基於X坐標排序:
open System.Collections.Generic
let pts = List<_>(
[| Point2D();Point2D(4.,2.); Point2D(3.,4.);Point2D(6.,2.); |]
)
pts.Sort({new IComparer<Point2D> with
member __.Compare(l, r) =
int(l.X - r.X)
})
代碼使用List<T>
定義了幾個點,然后使用對象表達式定義了一個匿名類的實例傳入Sort方法。而這樣不需要像C#中重新定義一個類型。雖然這樣的功能在C#中經常使用的是Linq中的Sort,但我們現在介紹面向對象就先以這為例了。
可以看到基於接口定義的對象表達式跟基於類的有所不同,在接口名后不能使用參數,因為接口無法實例化。
IDisposable接口
說到接口,就該說下.NET中比較特殊的IDisposable
,實現了此接口的對象必須實現Dispose
函數成員,用於對對象執行顯式的銷毀操作,通常執行一些釋放資源的代碼。
在C#中,實現IDisposable
的對象可用using
進行自動銷毀。在F#中,則有對應的use
關鍵字和using
函數,而且在實例化實現了IDisposable
接口的對象,必須使用new
關鍵字。
open System; open System.Data.SqlClient
type Database(conStr) =
let con = new SqlConnection(conStr) //new不可省略
member __.ConnectionString = conStr
member __.Connect() = con.Open()
member __.Close() = con.Close()
interface IDisposable with
member this.Dispose() = this.Close()
//使用use關鍵字,與C#類似
let testIDisposable() =
use db = new Database("connection string ...")
db.Connect()
//使用using函數,第一個參數是IDisposable接口的對象,第二個是要執行的操作
let testUsing(db:Database) = db.Connect()
using (new Database("connection string ...")) testUsing
- 使用
use
時,會在use
所在的代碼塊結束時調用Dispose
方法,在示例中,是在testIDisposable
執行完畢時。 using
函數會在它的函數參數執行完畢時調用Dispose
方法,示例中是在testUsing
函數執行完畢時。
通常,應該選擇使用use
。但要注意的是,因為use
需要等代碼塊結束時進行操作,所以無法在模塊中使用,若在模塊中使用,只會被當成let
,而不會自動銷毀對象。在模塊中使用,可以用using
函數。
類型轉換與擴展
類型轉換
在數值運算和流程控制語法中我們介紹F#中數值轉換需要使用對應的函數,如轉成Int
類型使用int
函數等。
但基類和子類之間的轉換,F#提供upcast
(子類轉為基類)和downcast
(基類轉為子類)函數進行轉換,或者使用對應的符號函數::>
和:?>
。
type Base() = class end //定義基類
type Derived() = inherit Base() //定義子類
let myDerived = Derived()
let upCaseResult = myDerived :> Base //使用:>轉換為基類
let upCaseResult2 : Base = upcast myDerived //使用upcast轉換為基類
let downCastResult = upCastResult :?> Derived //使用:?>轉換為子類
let downCastResult2 : Derived = downcast cast //使用downcast轉換為子類
需要注意的是,upcast操作總是安全的;但downcast並一定成功,可使用:?
在轉換前進行類型判斷,否則轉換失敗會引發InvalidCastException
異常。
if upCastResult :? Derived then upCastResult :?> Derived
:?
還可以用在模式匹配(數值運算和流程控制語法有介紹過,類似於C#中的switch
)里。
match shape with
| :? Circle as c -> printfn "circle with radius %f" c.Radius
| :? Rectangle as r when r.Length = r.r.Height
-> printfn "%f x %f square" r.Length r.r.Height
| :? Rectangle as r -> printfn "%f x %f rectangle" r.Length r.r.Height
| _ -> printfn "<unknown shape>"
| null -> raise (ArgumentNullException("shape"))
此段代碼從中 "What’s New in C# 7.0" (中文翻譯[《C#7.0中有哪些新特性?》])C#7.0的模式匹配的示例代碼轉換而來的。C#原代碼如下:
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
C#的代碼與F#一樣,最后一項null
其實是無法被匹配到的。F#中以“_
”作為通配符。
可以發現C#最近幾個大版本中的函數式新功能是借鑒於F#的,C#在函數式的道路是越走越遠了。👍
F#中在當前4.0版本中還沒有nameof
操作符,已經實現,估計會在新版本中釋出。而C#7.0的功能也可以在Visual Studio “15” Preview 4中體驗。
裝箱(Boxing)和拆箱(Unboxing)
F#中使用box
和unbox
函數進行裝箱和拆箱操作:
let i1 = 4
let o = box i //此時o為obj類型
let i2 : int = unbox o
在F#中,obj
為System.Object
的別名。
擴展
可以使用with
關鍵字對現有類型和接口增加屬性及方法。
type System.Int32 with
member i.IsPrime with get () =
Array.forall (fun x-> i%x <> 0) [| 2..i/2 |]
(250).IsPrime //250不是質數,將為false
示例中給int
類型添加一個屬性用於判斷其是否為質數。
結構和枚舉
結構(Struct)
在前面介紹的面向對象類以及類涉及的相關內容,但在示例的代碼感覺使用類並沒有感覺有什么優勢。其實像上一篇中的Point2D
類,使用結構(Struct
)也許會更好一些。
結構是值類型,與類的引用類型不同,在內存分配上是被分配在棧(Stack)上,所以在使用中內存消耗更少,而且不需要垃圾回收(GC)。這方面知識熟悉.NET框架的大家都很熟悉了。
下面是結構的定義:
type Point2D(xValue:double, yValue:double) = struct
member this.X = xValue
member this.Y = yValue
end
[<Struct>]
type Point2D(xValue:double, yValue:double) =
member this.X = xValue
member this.Y = yValue
結構定義與類一樣,在內容不為空時可省略struct end
關鍵字。不過這樣就和類定義一樣了,所以需要加上[<Struct>]
特性以示區別。這和抽象類、密封類的定義方法一致。
枚舉(Enum)
枚舉在F#中比較少用,替代的是使用可區分聯合(Discriminated Unions,常被稱作DU)。枚舉可看作是可區分聯合的簡化,它們之間的區別等介紹可區分聯合時再說明。下面是枚舉的定義及與其基礎類型的轉換:
type Card =
| Jack = 11
| Queen = 12
| King = 13
| Ace = 14
let q = enum<Card>(12) //int轉為enum類型
let i = int Card.King //enum轉為int類型
泛型及約束
F#與C#同樣基於.NET,所以泛型也並沒有什么特殊的。在使用上,F#中的類型參數需要以“'
”(單引號)開頭。
可以在類、結構、接口、集合、函數等中使用泛型。
let print<'a> (x:'a) =
printfn "%A" x
type MyClass<'T> (y:'T) =
member val Y = y with get, set
泛型在定義和使用上都與C#類似,F#中類型參數一般使用'a
、'b
……
但泛型約束就與C#有較大的區別了,以下是C#與F#泛型約束的對比表。
C# 約束 | F# 約束 | 描述 |
---|---|---|
where T: struct | when 'T : struct | 值類型。 |
where T : class | when 'T : not struct | 引用類型。 |
where T : new() | when 'T : ( new : unit -> 'a ) | 構造函數約束。C#中此約束必須放在最后,但F#中不需要。 |
where T : <基類> | when 'T :> type | T類型參數必須是從指定的基類型派生的,基類型可以是接口。 |
where T : U | 不支持 | T必須繼承自U。 |
不支持 | when 'T : null | 提供的類型必須可以為null , 這包括所有 .NET 對象類型。 |
不支持 | when 'T or 'U : (member 成員簽名) | 顯式成員約束,所提供的類型參數T和U中至少有一個必須包含指定簽名的成員。 |
不支持 | when 'T : enum<基礎類型> | 枚舉類型約束,提供的類型必須是基於指定基礎類型的枚舉。 |
不支持 | when 'T : delegate<tuple參數,返回類型> | 提供的類型必須是具有指定的參數和返回值的委托類型。其中參數是一個Tuple。 |
不支持 | when 'T : comparison | 提供的類型必須支持比較。 |
不支持 | when 'T : equality | 提供的類型必須支持相等性。 |
不支持 | when 'T : unmanaged | 提供的類型必須是非托管類型。 |
非托管類型是某些基元類型(sbyte
、byte
、char
、nativeint
、unativeint
、float32
、float
、int16
、uint16
、int32
、uint32
、int64
、uint64 或 decimal
)、枚舉類型、nativeptr<_>
或其所有字段均為非托管類型的非泛型結構。
在F#,泛型約束使用when
關鍵字,寫在<>
里面或者外面均是可以的。雖然F#支持着很多C#不支持的約束,但其實這些約束很少用到。😏
其中顯式成員約束可用於實現鴨子類型,有興趣可通過文章《方法多態與Duck typing;C#之拙劣與F#之優雅》了解。
本文發表於博客園。 轉載請注明源鏈接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-7.html。