介紹創建F#項目,F#中的模塊以及與C#項目互相引用需要註意的問題。 ...
F# 項目
在之前的幾篇文章介紹的代碼都在交互視窗(fsi.exe)里運行,但平常開發的軟體程式可能含有大類類型和函數定義,代碼不可能都在一個文件里。下麵我們來看VS里提供的F#項目模板。
F#項目模板有以下幾種類型(以VS2015為例):
- Silverlight庫創建Silverlight的類庫
- 教程模板是一個控制台應用程式,裡面包含了F#的示例,可通過這個項目快速瞭解F#相關內容。
- “可移植庫”則可創建用於多平臺的庫,支持的平臺在括弧里說明。
- “庫”用於創建類庫
- “控制台應用程式”大家就熟悉了。
- 安卓項目為安裝了Xamarin創建的,請忽略。
我們創建一個控制台應用程式來說明,下圖為程式的Program.fs
文件及運行結果:
我們添加一行代碼(圖中藍框中)防止運行結束自動退出,這個應用程式預設是把參數列印出來,而運行時參數為空,所以結果為一空數組([||]
)。
其中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+上/下箭頭進行調整文件順序,或在文件上點擊右鍵進行操作:
嵌套模塊
模塊中可嵌套模塊,但定義內層模塊需要在模塊名後使用等號(=
),且內層模塊的內容必須比它的上層模塊縮進一級。
module TopLevelModel
module NestedModule = //第一層嵌套模塊
let i = 1
module NestedModuleInNestedModule = //第二層嵌套模塊
let i = 2
使用模塊
若想不使用模塊名訪問模塊中的值時,則可使用open
關鍵字進行打開。但有兩個需要註意的地方:
強制顯示訪問
在上一章介紹的集合模塊中,我們從未使用open List
或者open Seq
這樣的操作。
使用F12轉到Seq
的代碼定義文件可以發現Seq
模塊使用了
[<RequireQualifiedAccess>]
(強制顯示訪問)。
附加了此特性的模塊在使用時必須使用模塊名訪問,因為幾個集合模塊中有大部分函數名稱是相同的,若設置此特性而可同時打開了多個模塊,則函數名稱將會衝突。
自動打開
而我們在使用printfn
和ignore
函數時,均不需要打開相關模塊,是因為在他們所屬模塊附加了[<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
類型。
我們知道dynamic
在Microsoft.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#中可使用?
來訪問動態類型的屬性和方法。
調用帶有 ref
和 out
參數的函數
在C#中,有ref
和out
兩個關鍵字來修飾函數的參數,使函數可以進行引用傳遞和返回多個值。若要在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
並對x
和y
進行了改變。
帶有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-4.html。