如果你也会C#,那不妨了解下F#(6):面向对象编程之“类”
前言
面向对象的思想已经非常成熟,而使用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); // 完整属性Xprivate double x; public double X{ get { return this.x; } set { this.x = value; }} // 自动属性Ypublic 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 //定义一个空类endtype internal Point2D() = class //定义一个空的internal类endtype 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
。当然get
和set
都可以省略使之成为只写或只读属性。
遗憾的是自动属性中不支持分别定义访问修饰符(如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 <- xpoint.Y <- ypoint
重载方法
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 = 0docount <- count + 1
注意let
代码和do
代码必须在member
之前。而do
语句中必须返回unit
,可使用ignore
函数丢弃返回值。
非静态的do
语句会在实例化时执行,而静态的do
语句会在第一次使用该类型时执行。而在没有主构造函数的类中,无法使用let
和do
语句。
如果需要在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
也更改为完整属性,并且x
和y
字段的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 -> doubledefault 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.Itemwith get idx = m_letters.[idx]and set idx c = m_letters.[idx] <- cmember 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 doself.OriginalPoint2 <- new Point2D()count <- count + 1member __.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
异常。
最后再简单说下类中let
和val
的区别:
let
始终是private
的,且需要有主构造函数才能定义,因为let语句在主构造函数中运行(同do
语句)。val
默认为public
的,并且可添加修饰符使之成为internal
或private
的。若有主构造函数,需要为val
字段添加DefaultValue
特性。val
与member
关键字结合使用,可声明自动属性。在类中访问
val
字段,需要使用对象标识符,而let
字段不需要。
注意,此DefaultValue
位于Microsoft.FSharp.Core
命名空间,不要和C#中常用的位于System.ComponentModel
的DefaultValue
混淆。
相关文章:
如果你也会C#,那不妨了解下F#(1):F# 数据类型
如果你也会C#,那不妨了解下F#(2):数值运算和流程控制语法
如果你也会C#,那不妨了解下F#(3):F#集合类型和其他核心类型
如果你也会C#,那不妨了解下F#(4):了解函数及常用函数
如果你也会C#,那不妨了解下F#(5):模块、与C#互相调用
【送书活动】机器学习项目开发实战
《机器学习项目开发实战》送书活动结果公布
F#年度调查结果概述
原文地址:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-6.html
.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注
如果你也会C#,那不妨了解下F#(6):面向对象编程之“类”相关推荐
- 如果你也会C#,那不妨了解下F#(5):模块、与C#互相调用
F# 项目 在之前的几篇文章介绍的代码都在交互窗口(fsi.exe)里运行,但平常开发的软件程序可能含有大类类型和函数定义,代码不可能都在一个文件里.下面我们来看VS里提供的F#项目模板. F#项目模 ...
- 如果你也会C#,那不妨了解下F#(4):了解函数及常用函数
函数式编程其实就是按照数学上的函数运算思想来实现计算机上的运算.虽然我们不需要深入了解数学函数的知识,但应该清楚函数式编程的基础是来自于数学. 例如数学函数f(x) = x^2+x,并没有指定返回值的 ...
- 如果你也会C#,那不妨了解下F#(1):F# 数据类型
简单介绍 F#(与C#一样,念作"F Sharp")是一种基于.Net框架的强类型.静态类型的函数式编程语言. 可以说C#是一门包含函数式编程的面向对象编程语言,而F#是一门包含面 ...
- Jove:@houyr 那你也可以用totalcmd发。下个curl.然后配有个tc的命令。。。。。
Jove:@ houyr 那你也可以用totalcmd发.下个curl.然后配有个tc的命令.....
- 线程间定制化调用通信—— 1 高内聚低耦合的前提下,线程操作资源类 2 判断/干活/通知 3 多线程交互中,必须要防止多线程的虚假唤醒,也即(判断只用while,不能用if)
生产者与消费者模式 一个生产者与一个消费者 题目:现在有两个线程,可以操作初始值为0的一个变量,实现一个线程对该变量加1,另一个线程对该变量减1,这两个线程的操作加一.减一交替,进行10轮,变量的初始 ...
- java扫描指定package注解_java获取包下被指定注解的类
方案一: 采用reflections 框架(此框架依赖com.google.guava) 2.项目依赖 org.reflections reflections 0.9.11 com.google.gu ...
- win10开启oracle服务器配置,Windows环境(Win10)下安装、配置服务器类Oracle Database 11g Release 2...
该篇为服务器类Oracle Database 11gRelease 2的安装.配置,若需安装.配置桌面类(通常是选择桌面类,如果是将本机作为服务器来使用,则选择服务器类),可参考"Windo ...
- Cocos2d-x下Lua调用自定义C++类和函数的最佳实践
关于cocos2d-x下Lua调用C++的文档看了不少,但没有一篇真正把这事给讲明白了,我自己也是个初学者,摸索了半天,总结如下: cocos2d-x下Lua调用C++这事之所以看起来这么复杂.网上所 ...
- 笔录软件在linux系统,linux下f但tp服务器架设笔录.doc
linux下f但tp服务器架设笔录 Linux下FTP服务器架设笔录 本文环境:CentOS5.4+Vsftpd2.05 安装Vsftpd服务器软件 检查安装情况 [dx@localhost ~]$ ...
最新文章
- GVINS:基于GNSS视觉惯性紧耦合的平滑状态估计方法
- leetcode day4
- SpringMVC教程--图片上传
- 1050 循环数组最大子段和
- html5的文档申明为什么是!DOCTYPE html?
- python赋值语句格式_Python赋值语句后逗号的作用分析
- 运维工程师如果将web服务http专变为https
- 部署到gcp_GCP 网络系统Andromeda --- 概述篇
- 九、模型文档编辑器(生成项目文档)
- 开课吧9.9元学python靠谱吗-quot;我,90 后,月薪 5k,副业 2w ”年轻人搞副业到底有多野?...
- 如何删除拒绝访问的文件
- SQL SERVER 2005 使用订阅发布同步数据库
- 【渝粤教育】电大中专计算机使用基础_1作业 题库
- 如何在matlab中建立水箱模型_水箱
- 在你可以执行与打印机有关的任务(例如页面设置或打印一个文档)之前,你必须已经安装打印机。你想现在安装打印机吗?
- 负载均衡篇(二)实现Web负载均衡的几种方式
- 5类网线,超5类网线,6类网线,超6类网线的区别
- Apache POI读合并单元格
- 太极安装的应用打开闪退_BUG:通过太极阴创建应用什么值得买的过程中太极闪退,应用创建失败...
- 初学风水-某商务中心店铺
热门文章
- 认清几种视频接口标准---无私奉献版
- Eclipse 安装配置总结(WST WTP)(转)
- About the windchill Command -
- 推荐曹济的FPA培训课程
- 如何让组织的KPI成为敏捷转型的推手而不是杀手 | IDCF
- 通过 GitExtensions 来使用 Git 子模块功能
- 牛!又一顶级大厂开招.NET,5年35k!
- Apache Member、ALC Beijing 发起人姜宁:一个人走的很快,但是一群人能走得更远
- ML.NET Cookbook:(3)如何从CSV加载包含多个列的数据?
- ML.NET 示例:图像分类模型训练-首选API(基于原生TensorFlow迁移学习)