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

前言

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

同样的,面向对象的基础概念不多说,就对比下语法,并简单说明下应该注意的地方。

继承

对象表达式(Object Expressions)

在介绍继承之前,先介绍一下F#面向对象中常用的一个表达式:对象表达式。它用于基于现有类型创建匿名对象类型,比如有时候只希望在一个或少数几个对象中修改成员函数的实现,这时候不一定要重新定义一个现有类型的派生类然后实例化。

可在创建对象时通过关键字with提供新的函数实现代码:

let oriPt = {
    new Point2D() with 
        member __.ToString() = "我是原点"
}

oriPt使用对象表达式实例化,并重写了基类的ToString方法。而如果在C#中我们就需要先定义一个派生类,然后重写ToString方法。其实这也是继承,只是这样会减少创建新的命名类型所需的代码和开销。

对象表达式需要放在一对大括号({})中,其中的new不可省略,且修改的成员函数必须是虚方法(包括抽象方法)

继承的实现

继承是面向对象的一大特性,下面分别是C#和F#中的语法对比。定义一个继承于Point2D的类:

public class Particle : Point2D
{
    public double Mass { get; set; }
}
type NamedPoint2D() =
    inherit Point2D()
    member val Name = "pt2d" with get, set

在F#中,在F#中,派生类中使用关键字inherit指定所继承的基类及其构造函数,若子类需要调用基类方法,同样使用base关键字。base无法像this一样自定义。😃

如果有多个构造函数,通常可以在不使用主构造函数的情况下使用对象表达式返回对象,或者使用使用其他构造函数实例化返回,并用then关键字指定额外的执行语句。

type NamedPoint2D =
    inherit Point2D
    val mutable Name:string
    new (x,y) ={
        inherit Point2D(x,y)
        Name = ""
    }
    new (name) = {
        inherit Point2D();
        Name = name
    }
    new (x,y,name) as this = 
        NamedPoint2D(x,y)
        then
            this.Name <- name

接口

接口的定义和使用

抽象类决定一个对象“是什么”,而接口决定了一个对象“具有什么功能”。我们先看F#中的定义:

type I2DLocation = interface		//完整定义方式
    abstract member X : float with get, set
    abstract member Y : float with get, set
end
type I2DLocation =					//简要定义方式
    abstract member X : float with get, set
    abstract member Y : float with get, set

在F#中,若不使用interface关键字显示定义,只要类的所有成员都为抽象(abstract)的,就会被类型推断系统推断为接口

接口在F#需要显示实现,所以在实现接口后,使用时也需要转换至接口类型,否则无法调用接口的属性或方法。

type Point2D(xValue:double, yValue:double) as this=
	……	//省略其他代码
	interface I2DLocation with
        member this.X with get() = this.X and set(v) = this.X <- v
        member this.Y with get() = this.Y and set(v) = this.Y <- v
let pt = Point2D()
let l = pt :> I2DLocation	//使用向上转换符,因为实现的接口已在编译期确定
printfn "x=%f,y=%f" l.X l.Y

使用对象表达式实现接口的匿名类型

[对象表达式](#对象表达式(Object Expressions))不仅可基于类创建匿名类型,同样可基于接口创建匿名类型。在C#中,我们在基于IComparer比较器进行比较时,都要定义一个实现IComparer的类。而在F#中,因为有对象表达式,我们可以省去很多代码。假设我们将一些Point2d基于X坐标排序:

open System.Collections.Generic
let pts = List<_>(
    [| Point2D();Point2D(4.,2.); Point2D(3.,4.);Point2D(6.,2.); |]
)
pts.Sort({new IComparer<Point2D> with 
    member __.Compare(l, r) =
        int(l.X - r.X)
})

代码使用List<T>定义了几个点,然后使用对象表达式定义了一个匿名类的实例传入Sort方法。而这样不需要像C#中重新定义一个类型。虽然这样的功能在C#中经常使用的是Linq中的Sort,但我们现在介绍面向对象就先以这为例了。

可以看到基于接口定义的对象表达式跟基于类的有所不同,在接口名后不能使用参数,因为接口无法实例化。

IDisposable接口

说到接口,就该说下.NET中比较特殊的IDisposable,实现了此接口的对象必须实现Dispose函数成员,用于对对象执行显式的销毁操作,通常执行一些释放资源的代码。

在C#中,实现IDisposable的对象可用using进行自动销毁。在F#中,则有对应的use关键字和using函数,而且在实例化实现了IDisposable接口的对象,必须使用new关键字。

open System; open System.Data.SqlClient
type Database(conStr) =
    let con = new SqlConnection(conStr)	//new不可省略
    member __.ConnectionString = conStr
    member __.Connect() = con.Open()
    member __.Close() = con.Close()
    interface IDisposable with
        member this.Dispose() = this.Close()

//使用use关键字,与C#类似
let testIDisposable() =
    use db = new Database("connection string ...")
    db.Connect()

//使用using函数,第一个参数是IDisposable接口的对象,第二个是要执行的操作
let testUsing(db:Database) = db.Connect()
using (new Database("connection string ...")) testUsing
  • 使用use时,会在use所在的代码块结束时调用Dispose方法,在示例中,是在testIDisposable执行完毕时。
  • using函数会在它的函数参数执行完毕时调用Dispose方法,示例中是在testUsing函数执行完毕时。

通常,应该选择使用use。但要注意的是,因为use需要等代码块结束时进行操作,所以无法在模块中使用,若在模块中使用,只会被当成let,而不会自动销毁对象。在模块中使用,可以用using函数。

类型转换与扩展

类型转换

数值运算和流程控制语法中我们介绍F#中数值转换需要使用对应的函数,如转成Int类型使用int函数等。

但基类和子类之间的转换,F#提供upcast(子类转为基类)和downcast(基类转为子类)函数进行转换,或者使用对应的符号函数::>:?>

type Base() = class end							//定义基类
type Derived() = inherit Base()					//定义子类
let myDerived = Derived()						
let upCaseResult = myDerived :> Base			//使用:>转换为基类
let upCaseResult2 : Base = upcast myDerived		//使用upcast转换为基类
let downCastResult = upCastResult :?> Derived	//使用:?>转换为子类
let downCastResult2 : Derived = downcast cast	//使用downcast转换为子类

需要注意的是,upcast操作总是安全的;但downcast并一定成功,可使用:?在转换前进行类型判断,否则转换失败会引发InvalidCastException异常。

if upCastResult :? Derived then upCastResult :?> Derived

:?还可以用在模式匹配(数值运算和流程控制语法有介绍过,类似于C#中的switch)里。

match shape with
| :? Circle    as c -> printfn "circle with radius %f" c.Radius
| :? Rectangle as r when r.Length = r.r.Height 
                    -> printfn "%f x %f square"    r.Length r.r.Height
| :? Rectangle as r -> printfn "%f x %f rectangle" r.Length r.r.Height
| _                 -> printfn "<unknown shape>"
| null              -> raise (ArgumentNullException("shape"))

此段代码从中 "What’s New in C# 7.0" (中文翻译[《C#7.0中有哪些新特性?》])C#7.0的模式匹配的示例代码转换而来的。C#原代码如下:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

C#的代码与F#一样,最后一项null其实是无法被匹配到的。F#中以_作为通配符

可以发现C#最近几个大版本中的函数式新功能是借鉴于F#的,C#在函数式的道路是越走越远了。👍

F#中在当前4.0版本中还没有nameof操作符,已经实现,估计会在新版本中释出。而C#7.0的功能也可以在Visual Studio “15” Preview 4中体验。

装箱(Boxing)和拆箱(Unboxing)

F#中使用boxunbox函数进行装箱和拆箱操作:

let i1 = 4
let o = box i	//此时o为obj类型
let i2 : int = unbox o

在F#中,objSystem.Object的别名。

扩展

可以使用with关键字对现有类型和接口增加属性及方法。

type System.Int32 with
    member i.IsPrime with get () =
        Array.forall (fun x-> i%x <> 0) [| 2..i/2 |]
(250).IsPrime        //250不是质数,将为false

示例中给int类型添加一个属性用于判断其是否为质数。

结构和枚举

结构(Struct)

在前面介绍的面向对象类以及类涉及的相关内容,但在示例的代码感觉使用类并没有感觉有什么优势。其实像上一篇中的Point2D类,使用结构(Struct)也许会更好一些。

结构是值类型,与类的引用类型不同,在内存分配上是被分配在栈(Stack)上,所以在使用中内存消耗更少,而且不需要垃圾回收(GC)。这方面知识熟悉.NET框架的大家都很熟悉了。

下面是结构的定义:

type Point2D(xValue:double, yValue:double) = struct
    member this.X = xValue
    member this.Y = yValue
end
[<Struct>]
type Point2D(xValue:double, yValue:double) =
    member this.X = xValue
    member this.Y = yValue

结构定义与类一样,在内容不为空时可省略struct end关键字。不过这样就和类定义一样了,所以需要加上[<Struct>]特性以示区别。这和抽象类、密封类的定义方法一致。

枚举(Enum)

枚举在F#中比较少用,替代的是使用可区分联合(Discriminated Unions,常被称作DU)。枚举可看作是可区分联合的简化,它们之间的区别等介绍可区分联合时再说明。下面是枚举的定义及与其基础类型的转换:

type Card =
    | Jack = 11
    | Queen = 12
    | King = 13
    | Ace = 14
let q = enum<Card>(12)	//int转为enum类型
let i = int Card.King	//enum转为int类型

泛型及约束

F#与C#同样基于.NET,所以泛型也并没有什么特殊的。在使用上,F#中的类型参数需要以“'”(单引号)开头

可以在类、结构、接口、集合、函数等中使用泛型。

let print<'a> (x:'a) = 
    printfn "%A" x
type MyClass<'T> (y:'T) =    
    member val Y = y with get, set

泛型在定义和使用上都与C#类似,F#中类型参数一般使用'a'b……

但泛型约束就与C#有较大的区别了,以下是C#与F#泛型约束的对比表。

C# 约束 F# 约束 描述
where T: struct when 'T : struct 值类型。
where T : class when 'T : not struct 引用类型。
where T : new() when 'T : ( new : unit -> 'a ) 构造函数约束。C#中此约束必须放在最后,但F#中需要。
where T : <基类> when 'T :> type T类型参数必须是从指定的基类型派生的,基类型可以是接口。
where T : U 不支持 T必须继承自U。
不支持 when 'T : null 提供的类型必须可以为null, 这包括所有 .NET 对象类型。
不支持 when 'T or 'U : (member 成员签名) 显式成员约束,所提供的类型参数T和U中至少有一个必须包含指定签名的成员。
不支持 when 'T : enum<基础类型> 枚举类型约束,提供的类型必须是基于指定基础类型的枚举。
不支持 when 'T : delegate<tuple参数,返回类型> 提供的类型必须是具有指定的参数和返回值的委托类型。其中参数是一个Tuple。
不支持 when 'T : comparison 提供的类型必须支持比较。
不支持 when 'T : equality 提供的类型必须支持相等性。
不支持 when 'T : unmanaged 提供的类型必须是非托管类型

非托管类型是某些基元类型(sbytebytecharnativeintunativeintfloat32floatint16uint16int32uint32int64uint64 或 decimal)、枚举类型、nativeptr<_> 或其所有字段均为非托管类型的非泛型结构。

在F#,泛型约束使用when关键字,写在<>里面或者外面均是可以的。虽然F#支持着很多C#不支持的约束,但其实这些约束很少用到。😏

其中显式成员约束可用于实现鸭子类型,有兴趣可通过文章《方法多态与Duck typing;C#之拙劣与F#之优雅》了解。


本文发表于博客园。 转载请注明源链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-7.html

posted @ 2016-09-26 09:02  忽见客来  阅读(1462)  评论(4编辑  收藏  举报