如果你也会C#,那不妨了解下F#(3):F#集合类型和其他核心类型

*本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-3.html *

在第一篇中,我们介绍了一些基础数据类型,其实那篇标题中不应该含有“F#”字眼,因为并不是特有的。
在本篇中,我们介绍如数组这些集合类型以及部分F#特有的类型。

在第一篇里我们列了一个从0加到100的代码段,了解函数式编程的同学会说那个F#代码不正宗。
而现在的C#开发一般也会使用Linq的方式来代替循环,其实F#天生就是使用这种方式的,下面我们先介绍F#的集合类型,在之后会介绍相关集合的函数操作。

集合类型

列表:List

声明List

注意,F#中的List不是C#中常用的System.Collections.Generic.List<T>,虽然后者你也可以在F#里使用。

F#中的List是有序,不可变的,且每一项的类型必须一致。我们先看看怎么定义列表。以下代码以>开头的为输入,之后的一行则为输出结果。

> let charList = ['a';'o';'e';'i';'u';'ü'];; 
val charList : char list = ['a'; 'o'; 'e'; 'i'; 'u'; 'ü']
> let emptyList = [];;
val emptyList : 'a list
> let emptyList2 = List.empty;;
val emptyList : 'a list

使用[]声明和List.empty均可声明空列表,空列表类型为'a list表示可接收任意类型,但当你添加一个元素后,列表类型但确定了,无法添加其它类型的元素。

和C#不一样,F#在集合中使用分号(;)分隔各个项。但当列表很大时,使用这样的声明方式就变得很麻烦了。

这时,可以使用下面的范围(Range)声明方式来声明:

> let intList = [1..10];;
val intList : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
> let tens = [0 .. 10 .. 100];;
val tens : int list = [0; 10; 20; 30; 40; 50; 60; 70; 80; 90; 100]
> let countDown = [5L .. -1L .. 0L];;
val countDown : int64 list = [5L; 4L; 3L; 2L; 1L; 0L]

范围声明方式中,中间项为间隔(若无则间隔为1),并包含起始项到结束项熟悉Python的小伙伴要注意。😁

除了范围式声明,还可以使用推导式声明:

> let intList2 = [for i=0 to 8 do yield i*i];;
val intList2 : int list = [0; 1; 4; 9; 16; 25; 36; 49; 64]

推导式声明代码使用[]包裹,通过执行里面的代码并在每次执行到yield语句时生成一个元素。

注意:F#的List中yield生成元素是一次性完全在内存中生成,若元素太多,应使用序列(seq)。 将在后面介绍。

当然,由[]包裹的声明式代码可以是复杂的语句,请看下面生成质数列表的语句:

> let primesUnder50 =
    [
        for n in 1 .. 50 do
            let factorsOfN =
                [
                    for i in 1 .. n do
                        if n % i = 0 then
                            yield i
                ]
            // 如果一个数的因数只有1和自己,便是质数
            if List.length factorsOfN = 2 then
                yield n
    ];;
val primesUnder50 : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47]

经过前两章介绍,应该可以发现,F#不使用大括号包裹代码块,而像Python使用缩进。这段代码我们就先不解释了。

List操作

  • 索引取值

    在F#中,使用索引进行取值和方法一样也需要用.标记。

    list0[0] //错误
    list0.[0] //正确
    
  • 比较

    如果两个列表类型相同,且类型可以进行比较(即实现IComparable接口),则可进行比较操作:

    let list1= [1;3;4;5]
    let list2= [1;4;3]
    list1 > list2;; //结果为false
    
  • ::(cons)和@操作符

    :: 将一个元素添加到列表的开头,而@则将两个列表相连。例如list1[2;3;4]

    let list2 = 100 :: list1 ;; // [100; 2; 3; 4]
    let list2 = list1 @ list2 ;; // [2; 3; 4; 100; 2; 3; 4]
    

    因为列表为不可变类型,所以操作后均返回了一个新的列表。但在内部实现上,::只需将新项连接到原列表(O(1)),而@操作需克隆第一个列表,所以::总是优于@操作。这方面有兴趣可查看F#的List实现。

List属性

Property Type Description
Head 'T 返回第一项。
Empty 'T list 静态属性,返回空列表。
IsEmpty bool 判断是否为空。
Item 'T 返回指定项。
Length int 列表包含元素个数。
Tail 'T list 返回除第一个元素外的其他元素。
> [2;3;4;5;6;6].Tail;;
val it : int list = [3; 4; 5; 6; 6]

F#的List在.Net框架里的类型为Microsoft.FSharp.Collections.FSharpList<T>,若想在C#中使用,需要添加FSharp.Core.dll引用。关于List的其他信息可查阅MSDN文章Lists (F#)。而List模块相关函数将在函数式编程时再细说。

序列:Seq

F#中的seq<_>其实就是.Net中的IEnumerable<T>,声明时只需将List声明中的[]换成{}即可。

> let intSeq = {1..10};;
val intSeq : seq<int>
> let intSeq2 = seq {for i=0 to 8 do yield i*i};;
val intSeq2 : seq<int>

在推导式声明中,需要在{}前加seq关键字。

IEnumerable是在需要时才对其中的元素进行计算(惰性计算),所以在输出结果时并不显示具体元素。适用于具有大量元素的集合。所以在声明时,无法使用seq { 1; 2; 3 }来声明,需要使用seq { yield 1; yield 2; yield 3 }。也不支持索引操作。

IEnumerable<T> 大家都很熟悉,在下就不班门弄斧了。🙁

数组:Array

数组是固定长度、可变、每项类型一致的集合。F#中的数组和C#的一样,但声明方式是类似于F#中的list,使用[| |]包裹。但数组的类型名称是[],例如:

> let squares1 = [| for i in 1 .. 7 -> i * i |];;		//推导式声明
val squares1 : int [] = [|1; 4; 9; 16; 25; 36; 49|]
> let squares2 = [| 1; 4; 9; 16; 25; 36; 49; 64; 81 |];;		//手动声明
val squares2 : int []
> squares1.[5];;
val it : int = 36

数组支持索引取值,也是使用.[index]的方式。数组间也支持比较。

数组切片:Slices

在F#中,数组可通过切片操作取得一部分连续的元素,语法是[start..end],可省略。接上一个例子:

> suares1.[1..4];;
val it : int [] = [|4; 9; 16; 25|]
> suares1.[1..];;	//省略结束项,则取到最后一项
val it : int [] = [|4; 9; 16; 25; 36; 49|]
> suares1.[..4];;	//省略起始项,则从第一项开始
val it : int [] = [|1; 4; 9; 16; 25|]
> suares1.[*];;					
val it : int [] = [|1; 4; 9; 16; 25; 36; 49|]

*则可用来复制数组。list可支持切片操作,有兴趣的同学可自行尝试。 不像Python,F#的切片中不支持间隔。

可变集合类型

在F#中,使用System.Collections.Generic下的其他集合类型也是很常见的。这些类型大家都很熟悉了,不再赘述。

列些代码供大家参考:

// List<'T>
let animails = System.Collections.Generic.List<string>()
animails.AddRange( [| "猪"; "羊"; "鸭"; "狗"; "猫" |] )
animails.Remove("鸭")
animails.Count;;
(*
    val animails : List<string>
    val it : int = 4
*)
open System.Collections.Generic
let dict = new Dictionary<string, string>()
dict.Add("A","Atom")
dict.Add("B","Bob")
let atom = dict.["A"]
let found, bob = periodicTable.TryGetValue("B");;
(*
    val dict : Dictionary<string,string> = dict [("A", "Atom"); ("B", "Bob")]
    val atom : string = "Atom"
    val found : bool = false
    val bob : string = null
*)

还有其他如Hash<T>等类型不再举例了。

从上面的代码可以看出,类在初始化时可以使用new;打开模块或命名空间使用open;方法参数中有out关键字的,F#中使用多返回值返回(即Tuple类型)。这些将在之后F#的面向对象编程中涉及。

其他F#类型

常量

C#中使用const定义的常量,在F#中需要使用Literal特性(Attribute)定义。

[<Literal>]
let MyConstant = 99

在F#中,定义数据类型时使用特性多于使用关键字。而F#中的特性需要放于[< >]

元组:Tuple

元组是一组有序的项,其中可以包含不同类型。

(1,5);;		// val it : int * int = (1, 5)
("xyz",'c',2.0+3.);;	// val it : string * char * float = ("xyz", 'c', 5.0)

注意元组的类型名称为'T * 'T ...,以*间隔其中的不同项。

函数

函数在F#中也可看作是种类型,定义函数也很简单,和C#类似。

但需要注意的是,F#中不使用return关键字,而使用最后一条语句作为返回值

let addOne x = x + 1;; // val addOne : int -> int
let sum x y = x + y;; // val sum : int -> int -> int

定义一个addOne函数,以输入参数和输出参数均为int

F#中函数参数不需要使用()包裹,而只需跟在函数名称之后。而使用(x,y)在F#中将被看作为一个元组参数。

函数先不介绍太多,将在下一篇中详细介绍。

Unit

在C#中,函数若没有返回值,使用void声明。在F#中,使用unit。若要定义一个Unit类型,使用()

let f (i:int) = ()

定义一个输入为int而无返回值的函数。

类型别名

在F#中可给类型设置一个别名,如给int一个其他的名称,类似于C++中的typedef

type I = int
let fn (a:I) = a + 1;;	// val fn : a:I -> I

介绍seq时,说seq<_>其实就是.Net中的IEnumerable<T>,查看源代码,我们可以发现:

type seq<'T> = System.Collections.Generic.IEnumerable<'T>

本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-3.html

posted @ 2016-08-17 20:43  忽见客来  阅读(1945)  评论(0编辑  收藏  举报