如果你也會C#,那不妨了解下F#(7):面向對象編程之繼承、接口和泛型


前言

面向對象三大基本特性:封裝、繼承、多態。上一篇中介紹了類的定義,下面就了解下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#中使用boxunbox函數進行裝箱和拆箱操作:

let i1 = 4
let o = box i	//此時o為obj類型
let i2 : int = unbox o

在F#中,objSystem.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 提供的類型必須是非托管類型

非托管類型是某些基元類型(sbytebytecharnativeintunativeintfloat32floatint16uint16int32uint32int64uint64 或 decimal)、枚舉類型、nativeptr<_> 或其所有字段均為非托管類型的非泛型結構。

在F#,泛型約束使用when關鍵字,寫在<>里面或者外面均是可以的。雖然F#支持着很多C#不支持的約束,但其實這些約束很少用到。😏

其中顯式成員約束可用於實現鴨子類型,有興趣可通過文章《方法多態與Duck typing;C#之拙劣與F#之優雅》了解。


本文發表於博客園。 轉載請注明源鏈接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-7.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM