如果你也會C#,那不妨瞭解下F#(6):面向對象編程之“類”

来源:http://www.cnblogs.com/hjklin/archive/2016/09/13/fs-for-cs-dev-6.html
-Advertisement-
Play Games

前言 面向對象的思想已經非常成熟,而使用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。當然getset都可以省略使之成為只寫或只讀屬性。

遺憾的是自動屬性中不支持分別定義訪問修飾符(如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語句會在第一次使用該類型時執行。而在沒有主構造函數的類中,無法使用letdo語句

如果需要在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也更改為完整屬性,並且xy欄位的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異常。

最後再簡單說下類中letval的區別:

  • let始終是private的,且需要有主構造函數才能定義,因為let語句在主構造函數中運行(同do語句)。
  • val預設為public的,並且可添加修飾符使之成為internalprivate的。若有主構造函數,需要為val欄位添加DefaultValue特性。valmember關鍵字結合使用,可聲明自動屬性。
  • 在類中訪問val欄位,需要使用對象標識符,而let欄位不需要。

註意,此DefaultValue位於Microsoft.FSharp.Core命名空間,不要和C#中常用的位於System.ComponentModelDefaultValue混淆。


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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • String和string的區別 從位置講: 1.String是.NET Framework裡面的String,小寫的string是C#語言中的string 2.如果把using System;刪掉,沒有大寫的String了,System是.NET Framework類庫中的一個函數名. 從性質講: ...
  • 後臺代碼: ...
  • Web.config配置: 在<system.web>節下: 登錄代碼: /// <summary> /// 登錄 /// </summary> public static bool Login(string userName, string userPwd) { MySqlHelper dbHel ...
  • 一、環境及工具 1、伺服器 VirtualBox5.1.4 安裝 Ubuntu Server 16.04 amd64 MySql Ver 14.14 Distrib 5.6.21 Jexus 5.8.1 nginx 1.10.0 dotnet core 1.0.0-preview2-003121 s ...
  • 隨著大規模的項目越來越多,許多項目都引入了依賴註入框架,其中最流行的有Castle Windsor, Autofac和Unity Container。 微軟在最新版的Asp.Net Core中自帶了依賴註入的功能,有興趣可以 "查看這裡" 。 關於什麼是依賴註入容器網上已經有很多的文章介紹,這裡我將 ...
  • 從微軟推出第一個版本的.NET Framework的時候,就在“System.Diagnostics”命名空間中提供了Debug和Trace兩個類幫助我們完成針對調試和跟蹤信息的日誌記錄。在.NET Framework 2.0中,微軟引入了TraceSource並對跟蹤日誌系統進行了優化,優化後的跟... ...
  • 將特定用戶代理的別名添加到用戶代理別名的內部集合中。 來自 <https://msdn.microsoft.com/zh-cn/library/6379d90d(v=vs.110).aspx> 用戶代理別名的集合指示 ASP.NET 伺服器控制項應為其呈現內容的目標用戶代理。其信息可以在Page.Cl ...
  • Google的ProtoBuf序列化器性能的牛逼已經有目共睹了,可以把它應用到Socket通訊,隊列,Wcf中,身為dotnet程式員一邊期待著不久後Grpc對dotnet core的支持更期待著Wcf有一天能在Linux平臺上閃瞎所有人。現在簡單表述下Wcf中應用ProtoBuf替代預設的序列化器 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...