如果你也會C#,那不妨了解下F#(5):模塊、與C#互相調用


F# 項目

在之前的幾篇文章介紹的代碼都在交互窗口(fsi.exe)里運行,但平常開發的軟件程序可能含有大類類型和函數定義,代碼不可能都在一個文件里。下面我們來看VS里提供的F#項目模板。

F#項目模板有以下幾種類型(以VS2015為例): F#項目模板

  • Silverlight庫創建Silverlight的類庫
  • 教程模板是一個控制台應用程序,里面包含了F#的示例,可通過這個項目快速了解F#相關內容。
  • “可移植庫”則可創建用於多平台的庫,支持的平台在括號里說明。
  • ”用於創建類庫
  • 控制台應用程序”大家就熟悉了。
  • 安卓項目為安裝了Xamarin創建的,請忽略。

我們創建一個控制台應用程序來說明,下圖為程序的Program.fs文件及運行結果:F#控制台程序及運行結果

我們添加一行代碼(圖中藍框中)防止運行結束自動退出,這個應用程序默認是把參數打印出來,而運行時參數為空,所以結果為一空數組([||])。

其中ignore函數用於丟棄System.Console.ReadKey()結果

現在項目中除了AssemblyInfo.fs外,只有Program.fs一個文件,下面我們先了解模塊的相關信息再創建其他文件。

模塊

模塊簡介

模塊(Module)是F#程序代碼的基本組織單位。默認情況下,每個F#代碼文件(后綴為.fs)對應一個模塊,且必須在文件開頭指定模塊名稱。

創建模塊

我們創建File1.fs文件時,默認會在開頭添加module File1,當然也可自己改成其他名稱。

module File1
let x = 1

在其他模塊中使用File1.x進行訪問。

文件順序

F#項目中的文件是有順序要求的,在上面的文件無法訪問下面的模塊。我們可以使用Alt+上/下箭頭進行調整文件順序,或在文件上點擊右鍵進行操作: F#代碼順序

嵌套模塊

模塊中可嵌套模塊,但定義內層模塊需要在模塊名后使用等號(=),且內層模塊的內容必須比它的上層模塊縮進一級。

module TopLevelModel		
module NestedModule = 	//第一層嵌套模塊
    let i = 1
    module NestedModuleInNestedModule =  //第二層嵌套模塊
        let i = 2

使用模塊

若想不使用模塊名訪問模塊中的值時,則可使用open關鍵字進行打開。但有兩個需要注意的地方:

強制顯示訪問

在上一章介紹的集合模塊中,我們從未使用open List或者open Seq這樣的操作。

使用F12轉到Seq的代碼定義文件可以發現Seq模塊使用了
[<RequireQualifiedAccess>](強制顯示訪問)

附加了此特性的模塊在使用時必須使用模塊名訪問,因為幾個集合模塊中有大部分函數名稱是相同的,若設置此特性而可同時打開了多個模塊,則函數名稱將會沖突。

自動打開

而我們在使用printfnignore函數時,均不需要打開相關模塊,是因為在他們所屬模塊附加了[<AutoOpen>](自動打開)的特性。像Operators模塊里有我們常用的運算符,為了方便使用,添加了自動打開的特性。

我們在自定義模塊時可根據需要使用這兩個特性。

命名空間

命名空間(Namespace)和模塊類似,不同的是命名空間里不能直接定義值,只能定義類型。(與C#中的命名空間一樣,可以想象我們無法在C#的命名空間中直接定義一個方法,而需要首先定義一個類。)

但F#中的命名空間不能像模塊那樣嵌套,但可以在同一文件中定義多個命名空間。

namespace PlayingCards
type Suit = Spade | Club | Diamond | Heart

namespace PlayingCards.Poker
type PokerPlayer = {Name:string; Money:int; Position:int}

上面的代碼在一個文件中使用兩個命名空間分別定義了一個類型。

其中Suit可區分聯合(Discriminated Union)類型;PokerPlayer記錄(Record)類型。將在下一篇介紹。

應用程序入口

在F#中,程序從程序集的最后一個文件開始執行,而且必須是一個模塊。但最后一個模塊的名稱可省略

也可以使用[<EntryPoint>]特性應用於最后一個代碼文件的最后一個函數,使其成為程序入口點而無需顯示調用。

可查看控制台應用程序項目的模板:

[<EntryPoint>]
let main argv =     
    printfn "%A" argv
    0

main函數的參數是一個數組(通常可自定義為字符串數組),是應用程序的運行參數,返回的整數則為程序的退出代碼(exit code)。

若不使用[<EntryPoint>],則需要在最后調用該函數,否則並不會自動調用該函數。

let main (argv:string[]) = 
    printfn "%A" argv
    System.Console.ReadKey(true) |> ignore
    0
main [||]

控制台應用程序通常在結束之前使用System.Console.ReadKey()方法來防止運行完成自動退出。

擴展模塊

可以通過創建一個同名模塊,在其中添加值來對原有模塊進行擴展。

在介紹常用函數時,我們提到Seq模塊沒有提供rev函數,現在自己實現以Seq模塊進行擴展

open System.Collections.Generic
module Seq =
    /// 反轉Seq中的元素
    let rec rev (s : seq<'a>) =
        let stack = new Stack<'a>()
        s |> Seq.iter stack.Push
        seq {
            while stack.Count > 0 do
                yield stack.Pop()
        }

其中使用了.NET框架中的泛型集合類型(System.Collections.Generic.Stack<T>)。

與C#互相調用

F#代碼和C#代碼(包括VB.NET)一樣,都編譯成MSIL,在CLR運行。(可參考文章《.NET框架》)所以,兩種語言之間可以方便地互相調用。

程序集的引用大家都熟悉,但C#和F#中又有一些獨立的東西不能互相使用,下面簡單介紹一下在互相調用中常見的問題。

F#調用C#代碼

本節涉及操作需要創建兩個項目,一個C#的類庫項目,一個F#的控制台項目。然后F#項目引用C#項目。

dynamic:在F#中訪問C#的動態類型

在.NET4.0,C#引入了dynamic關鍵字使得可以像使用動態語言一樣來使用C#。但在F#中並不支持dynamic關鍵字和動態類型,在引用C#編譯的程序集時,則變成了Object類型。

我們知道dynamicMicrosoft.CSharp.dll程序集中實現,在F#中可以通過引用此程序集,通過反射等操作自己實現對動態類型及屬性的訪問。

而我在平常一般使用第三方庫FSharp.Interop.Dynamic(Nuget)。代碼示例:

//C#代碼,命名空間CSharpForFSharp
public class CSharpClass
{
  public dynamic TestDynamic()
  {
    return "5566";
  }
}

在F#中調用:

//F#代碼,位於F#項目的Program.fs
open FSharp.Interop.Dynamic
open CSharpForFSharp			//C#項目中的命名空間
[<EntryPoint>]
let main argv =     
    let cc = CSharpClass()
    let str = cc.TestDynamic()    
    printfn "%A" (str?Length)	//使用?替代.
    System.Console.Read()|>ignore
    0

打開FSharp.Interop.Dynamic命名空間,F#中可使用?來訪問動態類型的屬性和方法。

調用帶有 refout 參數的函數

在C#中,有refout兩個關鍵字來修飾函數的參數,使函數可以進行引用傳遞和返回多個值。若要在F#中調用,則有一些不同。

帶有ref參數或者out參數的函數,因為參數值可能在函數中發生改變,需要在F#先定義一個可變值類型,並使用尋址操作符(&進行傳入。

// C#代碼,位於命名空間CSharpForFSharp
public class CSharpClass
{
    public static bool OutRefParams(out int x, ref int y)
    {
        x = 100;
        y = y * y;
        return true;
    }
}

在F#中調用:

// F#代碼,位於F#項目的Program.fs
open CSharpForFSharp
let mutable x,y = 0,0
CSharpClass.OutRefParams(&x,&y)	

返回true並對xy進行了改變。

帶有out的參數在C#中可以使用未賦值的變量傳入,所以在F#中除了尋址傳入的方法,還可以直接忽略該參數,則該函數在F#中成為了多返回值(即返回tuple)的形式:

let successful, result = Int32.TryParse(str)

Int32.TryParse返回了兩個值,第一個總是函數返回值,而后是out參數。

柯里化C#的方法

因為C#中的函數無論有多少個參數,在F#中調用時都視為一個tuple參數,所以無法柯里化和使用函數管道符(|>)操作。

在F#中可以使用FuncConvert類將.NET中的函數轉換成F#中的函數。

let join : string*string list -> string = System.String.Join
let curryJoin = FuncConvert.FuncFromTupled join
[ 1..10 ]
|> List.map string
|> curryJoin "*"				// "1*2*3*4*5*6*7*8*9*10"
let joinStar = curryJoin "*"	// joinStar類型為:string list -> string

以上代碼將System.String.Join轉化為F#中的函數,因為該方法具有多個重載,所以第一行代碼用來指定一個要轉換的重載。

其實FuncConvert類也可以在C#中使用,需要添加FSharp.Core程序集,有興趣的可以自己嘗試。

C#調用F#代碼

本節涉及操作需要創建兩個項目,一個F#的類庫項目,一個C#的控制台項目。然后C#項目引用F#項目,因為涉及到F#中獨有類型,還需要引用FSharp.Core程序集。

若要在UWP項目中引用F#項目,需要通過“可移植庫”模板創建項目。

因為C#中的類型比F#少了很多,所以很多C#不支持的類型均使用來代替,使用時只需像使用類一樣使用它就行了。而模塊,在C#中則為靜態類

F#中的函數

需要注意的是,若在F#將函數作為參數或返回值,則F#中的函數在C#中將會變成

FSharpFunc<_,_>對象(位於FSharp.Core程序集的Microsoft.FSharp.Core命名空間)。

//F# 代碼,位於TestModule模塊
open System
type MathUtilities =
	static member GetAdder() =
		(fun x y z -> Int32.Parse(x) + Int32.Parse(y) + Int32.Parse(z))

GetAdder函數返回一個將三個字符串轉成int再相加的函數,在C#中調用此函數:

FSharpFunc<string, FSharpFunc<string, FSharpFunc<string, int>>> ss = MathUtilities.GetAdder();
var ret = ss.Invoke("123").Invoke("45").Invoke("67");

F#中的string -> string -> string -> int類型函數在C#中變成了FSharpFunc <string, FSharpFunc <string, FSharpFunc <string, int>>>

這是因為C#中的不支持函數柯里化,如果F#中的函數需要更多的參數,在C#中調用就很麻煩了。雖然在F#使用很方便,但若需要編寫供C#使用的程序集,盡量不要使用這些功能。

命名規范

通過上面的了解,至少可以簡單地使用F#和C#互相調用。但有個地方可能使有強迫症的程序員很難受:F#模塊中的函數命名使用的是駝峰式(camelCase),在C#中類的方法則使用PascalCase命名規范。

F#模塊在編譯成靜態類后,在C#中使用變得不一致。在F#中提供了CompiledName特性用來指定編譯后的名稱

在第一篇中提到的F#中可用“`` ``”來使任何字符串作為變量(值)的名稱,若想在C#中調用這類值(不符合變量命名規則),也需要用CompiledName指定編譯后的名稱,否則無法調用。

module TestModule
[<CompiledName("Add")>]
let add = fun a b -> a+b
[<CompiledName("IsSeven")>]
let ``7?`` i = i % 7 = 0

在C#中調用:

int i = TestModule.Add(3,4);
var b = TestModule.IsSeven(7);

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


免責聲明!

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



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