F#第二章试译
第二章
基本概念
在第一章中,你已经可以编写你的第一个F#程序。在这里,我会慢慢让你加深对你所学的东西体会,但是,至今为止,绝大多数的代码依旧是很神秘的。在这章,我将会讲解读懂这些代码应有的基础知识,但是,更重要的是,我将会在这章里面提出几个或更多典型的例子来让你在接触更加复杂的代码之前掌握F#的根基。
本章的第一个部分将涉及到F#中的基础类型,比如int、string 等在F#程序中的基本内建类型。然后,我会简单的介绍函数,因此你将会知道如何操纵和改变数据。
第三部分将详细介绍一些内建类型(functional types),例如list、option、unit。当你掌握如何使用他们以后,你能够在接下来的章节中,更好的理解F#的面向对象和函数式编程风格。
在本章的最后,你将能够独立的编写简单的处理数据的F#程序。当然,在以后的章节中,你能学习到更加强大和富有表现力的方式来编写你的代码,但是在此之前,让我们脚踏实地得学会这些基础。
基础类型
类型是一种抽象的强制类型安全的实例。类型代表了一系列的可以真实存在的实例。有些只是简单的代表了一个整数,但是其他的一些可能会更加抽象得多,比如一个函数。F#是静态类型,这意味着在编译期间,就完成了所有的类型检测。例如,你会在给一个接纳一个整数作为参数的函数传入一个字符串的时候得到一个编译错误。
同C#和VB.NET一样,F#支持.NET中的所有基本类型以及.NET中所允许的类型转换(这点和绝大部分的语言是保持一致的)。他们是F#的内在组成部分,并且和用户自定义的类型相互区别。
我们一般用let来绑定一个变量。
当然,你也可以让let绑定许多其他的东西,但是我们讲在第三章才讲述那些东西。现在,我们只需知道,你可以用let来绑定(通过关键字)声明一个新的标识符。例如,接下来的代码中,我们在FSI中定义了一个叫做x的变量:
>let x = 1;;
valx : int = 1
基本数值类型
基本数值类型有2种:整数和浮点数。整型的表示范围随着他长度而改变,因此,一些类型只能用较少的内存表示一个较小范围内的输。整数通过他们是否能够表示一个负数又分成了2类:有符号数和无符号数。
浮点数的表示范围也和长度有关,我们通过他们的字母后缀来确定let绑定一个整数还是浮点数。Table 2-1 给出了所有的整型和浮点型以及他们的字母后缀。
>let answerToEverything = 32UL;;
valeverything: uint64 = 42UL
>let pi = 3.1415926M;;
valpi : decimal = 3.1415926M
>let avogadro = 6.022e-23;;
valavogadro : float = 6.022e-23
Table 2-1. Numerical primitives in F#
Type Suffix .NET type Range
byte uy System.Byte 0 to 255
sbyte y System.SByte -128 to 127
int16 s System.Int16 -32768 to 32767
uint16 us System.UInt16 0 to 65535
int, int32 System.Int32 -231 to231-1
uint, uint32 u System.UInt32 0 to 232-1
int64 L System.Int64 -263 to 263-1
uint64 UL System.UInt64 0 to 264-1
float System.Double A double-precision floating point based onthe IEEE64 standard. Rep-
resents values with approximately 15 significant digits.
float32 f System.Float A single-precision floating pointbased on the IEEE32 standard. Rep-
resents values with approximately 7 significant digits.
decima M System.Decimal A fixed-precision floating-point typewith precisely 28 digits of
precision.
F#同样允许你通过加上0x, 0O, 0b后缀来设置16进制,8进制和二进制的值:
>let hex = 0xFCAF;;
valhex : int = 64687
>let oct = 0o7771L;;
valoct : int64 = 4089L
>let bin = 0b00101010y;;
valbin : sbyte = 42y
> (hex, oct, bin);;
val it : int * int64 * sbyte = (64687, 4089, 42)
如果对IEEE32或者是IEEE64标准很熟悉的话,你同样可以用16进制,8进制或者是2进制的方式来指定浮点数的值。F#能把二进制的值转化为他表示的对应的浮点数的值。我们通过使用不同的后缀来区分不同的精度的浮点数类型,LF后缀用来指定float类型,而lf用来指定float32类型。
> 0x401E000000000000LF;;
val it : float = 7.5
> 0x00000000lf;;
val it : float32 = 0.0f
运算
你可以标准的基本运算符。Table2-3列出了所支持的所有的运算符。和所有基于CLR的语言一样,整数做除法运算的时候结果会舍弃余数。
Table 2-2. Arithmetic operators
Operator Description Example Result
+ Addition 1 + 2 3
− Subtraction 1− 2 −1
* Multiplication 2 *3 6
/ Division 8L / 3L 2L
** Powera 2.0 ** 8.0 256.0
% Modulus 7 % 3 1
Power,**运算符,只能在float或者是float32类型下工作。要得到一个整数的乘方,你必须选择把他转换沉一个浮点数或者是有pown函数
默认情况下,运算符并不会检测运算结果是否越界,因此当你超过一个整数的表示范围的时候,他会溢出成一个负数。(同理,减法会获得一个正数当一个无法被存储在整型中的数存放在整型中的时候。)
> 32767s + 1s;;
val it : int16 = −32768s
> −32768s + −1s;;
val it : int16 = 32767s
当一个整数涉及到溢出的时候,你应该考虑一个表示范围更大的类型或者是使用带有检测功能的运算符,这部分内容,参考第七章
F#毫无疑问提供了所有你期待有的数学函数,所有的这些,都在Table2-3中列出了。
Table2-3.Commonmath functions
Routine Description Example Result
abs Absolute value of a number abs −1.0 1.0
ceil Roundup to the nearest integer ceil9.1 10
exp Raisea value to a power of e exp1.0 2.718
floor Rounddown to the nearest integer floor9.9 9.0
sign Sign of the value sign −5 −1
log Natural logarithm log 2.71828 1.0
log10 Logarithm in base 10 log10 1000.0 3.0
sqrt Square root sqrt 4.0 2.0
cos Cosine cos 0.0 1.0
sin Sine sin 0.0 0.0
tan Tangent tan1.0 1.557
pown Compute the power of an integer pown 2L 10
类型转换
F#的一个原则就是不允许任何的隐式转换。这就意味着,编译器不会自动把基本数据类型转换成你所想要的,例如,把一个int16转换为int64。这将避免很多难以察觉的bug。为了打到类型转换的效果,你必须使用Table2-4中的类型转换函数。所有的类型转换函数都能接纳任意基本类型,包括字符串和字符。
Table2-4.Numericprimitive conversion routines
Routine Description Example Result
sbyte Converts data to an sybte sbyte −5 −5y
byte Converts data to a byte byte "42" 42uy
int16 Converts data to an int16 int16 'a' 97s
uint16 Converts data to auint16 uint16 5 5us
int32, int Convertsdata to an int int 2.5 2
uint32, uint Converts data to a uint uint32 0xFF 255
int64 Converts data to an int64 int64−8 −8L
uint64 Converts data to a uint64 uint64 "0xFF" 255UL
float Converts data to a float float3.1415M 3.1415
float32 Converts data to a float32 float32 8y 8.0f
decimal Converts data to a decimal decimal 1.23 1.23M
当上述的类型转换接受字符串为参数的时候,他们使用底层的System.Convert系列函数分析字符串的内容,这意味着,非法的输入将导致他们抛出System.FormatException异常。
大数类型
F#提供了BigInt类型来代表你要处理大于264的大整数。其实,BigInt只是System.Numer.ics.BigInteger类型的一个别名而已,他同C#和VB.NET中支持大数的语法是一模一样的。
BigInt使用I作为字母后缀标志。Example2-1定义了一些使用BigInt的数据。
>open System.Numerics
//Data storage units
letmegabyte = 1024I * 1024I
letgigabyte = megabyte * 1024I
letterabyte = gigabyte * 1024I
letpetabyte = terabyte * 1024I
letexabyte = petabyte * 1024I
letzettabyte = exabyte * 1024I;;
valmegabyte : BigInteger = 1048576
valgigabyte : BigInteger = 1073741824
valterabyte : BigInteger = 1099511627776
valpetabyte : BigInteger = 1125899906842624
valexabyte : BigInteger = 1152921504606846976
valzettabyte : BigInteger = 1180591620717411303424
尽管BigInt在性能上进行了很大程度上的优化,但是他任然比使用普通的基本整型的速度慢得多。
比特运算符
基本整数类型支持比特运算符以用来操纵二进制级别的数据运算。他们经常在读写二进制数据的时候用到。参考Table2-5
Table2-5. Bitwise operators
Operator Description Example Result
&&& Bitwise ‘And’ 0b1111 &&& 0b0011 0b0011
||| Bitwise ‘Or’ 0xFF00 |||0x00FF 0xFFFF
^^^ Bitwise ‘Exclusive Or’ 0b0011 ^^^ 0b0101 0b0110
<<< Left Shift 0b0001 <<< 3 0b1000
>>> Right Shift 0b1000 >>> 3 0b0001s
字符型
由于.NET平台是基于Unicode的,因此字符都是使用2个字节的UTF-16字符表示法。你可以通过给任意一个Unicode字符加上单引号来定义一个字符类型变量。当然了,字符型变量也可以用16进制字符的Unicode码来指定。
接下来的这段代码定义了一个字符列表,然后输出一个字符对应的16进制的值:
> let vowels = ['a'; 'e'; 'i'; 'o';'u'];;
val vowels : char list = ['a'; 'e'; 'i';'o'; 'u']
> printfn "Hex u0061 = '%c'"'\u0061';;
Hex u0061 = 'a'
val it : unit = ()
为了表示那些特殊的转义字符,你必须如同Table2-6那样使用转义字符。一个转义字符是一个反斜杠加上一个要表示的特定的字符。
Table2-6.Character escape sequences
Character Meaning
\’ Single quote 单引号
\” Double quote 双引号
\\ Backslash 反斜杠
\b Backspace 退格符
\n Newline 换行
\r Carriage return 回车
\t Horizontal tab tab
如果你想要知道一个.NET Unicode字符对应的数值,你可以通过我们在前面的Table2-3中所列出的转换方法来获得。当然,您也可以选择通过在一个字符后面加上一个后缀B来获得:
>// Convert value of 'C' to an integer
int'C';;
val it : int = 67
> // Convert value of 'C' to a byte
'C'B;;
val it : byte = 67uy
字符串
字符串是通过一串在双引号的的字符来定义的,可以是一行,也可以是多行。可以通过向字符串标识符后面加上.[]并向[]里传入一个非零的有符号整数来获得指定的字符:
> let password ="abracadabra";;
valpassword : string = "abracadabra"
> let multiline = "This string
takes up
multiple lines";;
val multiline : string = "Thisstring
takes up
multiple lines";;
> multiline.[0];;
val it : char = 'T'
> multiline.[1];;
val it : char = 'h'
> multiline.[2];;
val it : char = 'i'
> multiline.[3];;
valit : char = 's'
你可以通过在每行的最后一个字符后面添加反斜杠\ 来将一个字符串分割到好几行。当一行的最后一个字符后面是反斜杠的时候,字符串会自动忽略这一行后所有的空白符并且自动添加下一行的字符到当前字符串中去:
let longString = "abc-\
def-\
ghi";;
> longString;;
val it : string = "abc-def-ghi"
你可以通过在一个字符串中自由的使用类似与\t或者是\\这样的转义字符,但是,转义字符会在定义一些路径和注册表键值的时候带来很大的麻烦。你可以选择在定义一个字符串的时候加上@这个符号,这表示在这个字符串里面,不对任何字符进行转义:
> let normalString ="Normal.\n.\n.\t.\t.String";;
val normalString : string = "Normal.
.
. . .String
> let verbatimString =@"Verbatim.\n.\n.\t.\t.String";;
val verbatimString : string ="Verbatim.\n.\n.\t.\t.String"
和在一个字符后面加上后缀B一样,在一个字符串后面加上后缀B会通过一个字符数组返回这个字符串中所有字符。(数组将在第四张被介绍。)
> let hello = "Hello"B;;
val hello : byte array = [|72uy; 101uy;108uy; 108uy; 111uy|]
布尔值
F#提供了bool类型(System.Boolean)来处理只有true和false的值的情况。标准的布尔值操作符在Table2-7中被列出
Table2-7. Boolean operators
Operator Description Example Result
&& Boolean ‘And’ true && false false
|| Boolean ‘Or’ true || false true
not Boolean ‘Not’ not false true
Example2-2提供了一张打印布尔值函数运算结果的真值表。他通过定义一个接纳一个函数名为f作为参数的printTruthTable函数来实现这一切。这个函数在真值表中的每个表格中都被调用一次,并且打印他自己的结果。接下来,我们会看到运算符&&和||被当作参数传到printTruthTable函数中去。
Example2-2. Printing truth tables
>// Print the truth table for the given function
letprintTruthTable f =
printfn " |true | false |"
printfn " +-------+-------+"
printfn " true | %5b | %5b |" (f truetrue)(f true false)
printfn " false | %5b | %5b |" (f false true) (f false false)
printfn " +-------+-------+"
printfn ""
();;
valprintTruthTable : (bool -> bool -> bool) -> unit
>printTruthTable (&&);;
| true | false |
+-------+-------+
true | true| false |
false | false | false |
+-------+-------+
valit : unit = ()
>printTruthTable (||);;
| true | false |
+-------+-------+
true | true | true |
false | true | false |
+-------+-------+
valit : unit = ()
F#在求布尔值表达式的时候采用了短路法则,这意味着整个表达式的值可能在第一个表达式被求出来以后就被确定了,第二个表达式不会被计算。例如:
true || f()
结果将会是true,不会执行函数f。下面一个例子:
false && g()
结果将会是false,不会执行函数g。
比较和相等
你可以通过使用Table2-8中列出的大于,小于,等于操作符来比较2个数值的大小。所有的比较运算符的执行结果都是布尔值;比较函数返回的是-1,0或者是1的依据是第一个参数是否小于,等于或者是大于第二个参数。
Table2-8. Comparison operators
Operator Description Example Result
< Less than 1 < 2 true
<= Less than or equal 4.0 <= 4.0 true
> Greater than 1.4e3 > 1.0e2 true
>= Greater than or equal 0I >=2I false
= Equal to "abc" = "abc" true
<> Not equal to 'a' <> 'b' true
compare Compare two values compare31 31 0
在.NET中,相等是一个很难纠缠的主题。既有值相等,也有引用对象相等。对于数值类型,比较意味着变量的值是一样的,例如1=1,。但是对于引用类型,是否相等是通过重载Sytem.Object的Equals方法来判定的。更多的关于相等的细节,将会在119页的”相等对象”中介绍。
函数
至今为止,我们已经掌握在所有的F#中的基本类型,接下来,让我们定义函数来使用他们。
你可以使用同定义变量一样的方式定义函数,在函数名以后的所有东西都被认为是这个函数的参数。下面定义了一个接纳一个整型参数x的square函数,他将返回参数x的平方:
> let square x = x * x;;
val square : int -> int
> square 4;;
val it : int = 16
和C#不同的是,F#中没有return这个关键字。因此当你定义一个函数的时候,最后一个表达式就等于是函数执行结果的返回。
让我们来试着写一个让函数的输入加1的函数:
> let addOne x = x + 1;;
valaddOne : int -> int
在FSI上的输出显示了这个函数的类型是int -> int。我们这样读:一个函数传入一个整数并且返回一个整数。这个规则在一个函数拥有多个参数的时候会显得稍显复杂:
> let add x y = x + x;;
val add : int –> int -> int
一个很有挑战性的读法, 类型 int -> int ->int 被读作:一个函数传入一个整数,该函数返回一个传入一个整数并且返回一个整数的函数。不要所谓的一个函数返回一个函数所迷惑和害怕。你需要知道的仅仅是如何去调用一个函数,只需要简单的把参数用空格隔开:
> add 1 2;;
valint : 3
类型引用
因为F#是静态类型,因此当你在给add方法传递浮点数作为参数的时候会得到一个编译错误:
> add 1.0 2.0;;
add 1.0 2.0;;
----^^^^
stdin(3,5): error FS0001: This expression has type
float
but is here used withtype
int.
你可能会对此感到很迷惑,为什么编译器就认为函数只能接纳整数作为参数呢?但是运算符+却同样可以工作在浮点数下。
这是因为类型引用,和C#不同,F#编译器不需要你明确的告诉编译器一个函数的所有参数类型。编译器会指定他们为通常的选择。
小心,不要混淆了类型引用和动态类型。尽管F#允许你在编写代码的时候省略类型,但是这不意味着编译器在编译时不会强制检查类型。
因为运算符+为很多的类型服务,例如:byte,int,decimal,编译器会在没有附加条件的情况下简单的认为类型就是int。