前言

面向对象的思想已经非常成熟,而使用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
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

如果你也会C#,那不妨了解下F#(6):面向对象编程之“类”的更多相关文章

  1. 如果你也会C#,那不妨了解下F#(7):面向对象编程之继承、接口和泛型

    前言 面向对象三大基本特性:封装.继承.多态.上一篇中介绍了类的定义,下面就了解下F#中继承和多态的使用吧.

  2. 如果你也会C#,那不妨了解下F#(5):模块、与C#互相调用

    F# 项目 在之前的几篇文章介绍的代码都在交互窗口(fsi.exe)里运行,但平常开发的软件程序可能含有大类类型和函数定义,代码不可能都在一个文件里.下面我们来看VS里提供的F#项目模板. F#项目模 ...

  3. 如果你也会C#,那不妨了解下F#(4):了解函数及常用函数

    函数式编程其实就是按照数学上的函数运算思想来实现计算机上的运算.虽然我们不需要深入了解数学函数的知识,但应该清楚函数式编程的基础是来自于数学. 例如数学函数\(f(x) = x^2+x\),并没有指定 ...

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

    本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-3.html 在第一篇中,我们介绍了一些基础数据类型,其实那篇标题中不应该含有"F#&q ...

  5. 如果你也会C#,那不妨了解下F#(1):F# 数据类型

    本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-1.html 简单介绍 F#(与C#一样,念作"F Sharp")是一种基于. ...

  6. 如果你也会C#,那不妨了解下F#(2):数值运算和流程控制语法

    本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-2.html 一些废话 一门语言火不火,与语言本身并没太大关系,主要看语言的推广. 推广得好,用的 ...

  7. Cocos2d-x下Lua调用自定义C++类和函数的最佳实践[转]

    Cocos2d-x下Lua调用C++这事之所以看起来这么复杂.网上所有的文档都没讲清楚,是因为存在5个层面的知识点: 1.在纯C环境下,把C函数注册进Lua环境,理解Lua和C之间可以互相调用的本质 ...

  8. linux下启动dbca或netmgr类的图形界面报错解决

    linux下启动dbca或netmgr类的图形界面报错解决    Xlib: connection to ":0.0" refused by server Xlib: No pro ...

  9. 【转】Cocos2d-x下Lua调用自定义C++类和函数的最佳实践

    转自:http://segmentfault.com/blog/hongliang/1190000000631630 关于cocos2d-x下Lua调用C++的文档看了不少,但没有一篇真正把这事给讲明 ...

随机推荐

  1. Js 原型和原型链

    Js中通过原型和原型链实现了继承 Js对象属性的访问,首先会查找自身是否拥有这个属性 如果查到,则返回属性值,如果找不到,就会遍历原型链,一层一层的查找,如果找到就会返回属性值 直到遍历完Object ...

  2. SQL:指定名称查不到数据的衍伸~空格 换行符 回车符的批量处理

    异常处理汇总-数据库系列  http://www.cnblogs.com/dunitian/p/4522990.html 先看看啥情况 复制查询到的数据,粘贴一下看看啥情况 那就批量处理一下~ 就这样 ...

  3. requests的content与text导致lxml的解析问题

    title: requests的content与text导致lxml的解析问题 date: 2015-04-29 22:49:31 categories: 经验 tags: [Python,lxml, ...

  4. 开始学nodejs——net模块

    net模块的组成部分 详见 http://nodejs.cn/api/net.html 下面整理出了整个net模块的知识结构,和各个事件.方法.属性的用法 net.Server类 net.Socket ...

  5. ES6的一些常用特性

    由于公司的前端业务全部基于ES6开发,于是给自己开个小灶补补ES6的一些常用特性.原来打算花两天学习ES6的,结果花了3天才勉强过了一遍阮老师的ES6标准入门(水好深,ES6没学好ES7又来了...) ...

  6. [转载]一个标准java程序员的进阶过程

    第一阶段:Java程序员 技术名称 内                 容 说明 Java语法基础 基本语法.数组.类.继承.多态.抽象类.接口.object对象.常用类(Math\Arrarys\S ...

  7. 【开发软件】 在Mac下配置php开发环境:Apache+php+MySql

    本文地址 原文地址   本文提纲: 1. 启动Apache 2. 运行PHP 3. 配置Mysql 4. 使用PHPMyAdmin 5. 附录   有问题请先 看最后的附录   摘要: 系统OS X ...

  8. ResponsibleChain(责任链模式)

    /** * 责任链模式 * @author TMAC-J * 老板讲任务交给CTO,CTO自然不会亲自去做,又把人物分配给项目经理,项目经理再把任务分配给组长,组长再分配给个人 * 如果中途哪个环节出 ...

  9. Flexible 弹性盒子模型之CSS flex-basis 属性

    实例 设置第二个弹性盒元素的初始长度为 80 像素: div:nth-of-type(2){flex-basis:80px;}   效果预览 浏览器支持 表格中的数字表示支持该属性的第一个浏览器的版本 ...

  10. Android—Volley:接收服务端发送的json数据乱码问题解决

    new JsonObjectRequest中重写方法parseNetworkResponse,内容如下: /** * 重写此方法不会导致乱码 */ @Override protected Respon ...