F# 用类型扩展实现传统的类和对象

又翻到过去从微软文档摘录下的一段F#代码(https://docs.microsoft.com/en-us/archive/blogs/chrsmith/f-snippets-01),它是演示F#“如何进行基本对象继承以及实现接口”的。

type TimelessPerson = class
  val m_name : string // private field

  new (name) = { m_name = name } // public constructor

  member x.SayHello() = // public member function
    Console.WriteLine(x.m_name ^ @" says, 'Hello'")
end

type IAge = interface

  abstract GrowOlder : TimeSpan -> unit // in C# void GrowOlder(TimeSpan elapsedTime)

  abstract Age : int // A property. In C# int Age { get; }
end

type AgingPerson = class
  // Inherit from TimelessPerson and define the 'baseclass' as 'base'
  inherit TimelessPerson

  val mutable m_age : TimeSpan

  interface IAge with
    member x.GrowOlder elapsed =
      x.m_age <- x.m_age.Add(elapsed)
      Console.WriteLine(base.m_name + " has aged " + elapsed.ToString()) // 此句编译报错:error FS0419: "base" 值只能用于直接调用重写成员的基实现
    member x.Age with get() =
      int (x.m_age.TotalDays / 365.0)

  // In order to call the base class's constructor you need the 'inherit BaseClassName()' structure.
  new(name, age) = { inherit TimelessPerson(name); m_age = age}
end

let santa = TimelessPerson("Chris Kringle")
santa.SayHello()

let me = AgingPerson("Chris Smith", (TimeSpan(365 * 25,0,0,0)))
me.SayHello()

(* Casting me to the IAge interface, and calling methods/properties in the latter *)
let myIAge = me :> IAge
myIAge.GrowOlder( TimeSpan(12,0,0))
Console.WriteLine("My age = " + myIAge.Age.ToString())
Console.ReadKey(false)

可见,F#语言作为.NET家族的重要成员, 为了积极维系家族“血统”, 同时具备面向对象的编程范式,其OOP方面的语法高度秉承了C#关于类和对象的语言特征与风格,掺杂了太多语法和语义上的噪音,还与F#的优雅风格格格不入,真是强行拉郎配。

F# 作为以函数式为主的语言,对待函数 跟对待普通值 是同等的 (函数是一等公民嘛), 那么record类型里的字段也就能申明成函数类型。没有函数体的函数类型 等同于抽象abstract函数,只有抽象函数的记录就可以充当接口interface。

F#几乎所有类型都可以进行扩展,亦即给类型添加成员member, 当函数附加到类型上时,等同于给类class添加方法method,起到了类的行为的作用。被扩展的类型(一般都是record)里的字段则保存着类的内含数据和状态。

let时 给那个模拟interface的记录类型里的字段 赋予具体的函数体,于是"实现"了这个“接口”。let 多个变量,可以赋予其字段以不同的函数体,从而做到了多态。

顺着这些思路,我将多个F#原本用“赘婿”式的C#风格的OOP代码 改造为类型扩展表示法。本文就以上面那段代码为例,转换如下:

  type TimelessPerson = {
    m_name : string
  } with
    static member new_ (name) = { m_name = name } // public constructor
    member x.SayHello() =
      printfn ("%s says, 'Hello'") (x.m_name)

  type IAge = { // interface
    GrowOlder : TimeSpan -> unit // abstract
    Age : int
  }

  type AgingPerson = {
    mutable m_age : TimeSpan
    super : TimelessPerson // inherit
  } with
    member x.GrowOlder elapsed =
      x.m_age <- x.m_age.Add elapsed
      printfn "%s has aged %A" (x.super.m_name) elapsed
    member x.Age =
      int (x.m_age.TotalDays / 365.0)

    member x.IAge = {GrowOlder = x.GrowOlder; Age = x.Age}

    member x.SayHello = x.super.SayHello

    static member new_ (name, age) =
      {super = TimelessPerson.new_ (name); m_age = age}

  let santa = TimelessPerson.new_ ("Chris Kringle")
  santa.SayHello()

  let me = AgingPerson.new_ ("Chris Smith", (TimeSpan(365 * 25,0,0,0)))
  me.SayHello()

  (* Casting me to the IAge interface, and calling methods/properties in the latter *)
  let myIAge = me.IAge
  myIAge.GrowOlder(TimeSpan(12,30,0) )
  printfn "My age = %d" (myIAge.Age)

转换的代码中有三处亮点:
1、member x.IAge = ...
明确了实现了哪个接口(此例是 IAge), 以免实例后对象 张冠李戴误用(有draw方法的不能既是figue类 又是gun类, 会fly的对象不允许从 bird “动态” 到 plane),同时与方法实现相分离, 可根据类的外部需要 将已有方法任意组合成新的接口 而不必修改实现本身。这就好比同一份文件(这里的方法)可以贴上不同的标签(这里的 x.SayHello)供不同需求而查询到。

2、member x.SayHello = x.super.SayHello
F# 不支持类似于 typeScript 的交叉类型, 或者 GO 的结构匿名嵌入————它们都可将其它记录/结构类型的字段平铺到自己类型里来。
本方法模拟了这些嵌入,继承者 (本例中的 AgingPerson ) “主动”将其它记录 (TimelessPerson) 的成员拉进来,从而“组合”成为自己的成员,既起到了继承作用(可以是多"继承"),又避免了传统多继承会遇到的 父类上溯的复杂性和重名冲突 问题。
这种靠使用者主动拉来“父类”中的成员 的方法还有一个好处,就是自己根据需要有选择性地去拉,而不会“本来只想要个香蕉,但拿到的却是拿着香蕉的猩猩,乃至最后你拥有了整片丛林。”

3、附着在 record 的 member 可不必紧邻 record 的定义之后, 可以隔开,甚至可以在不同模块里,还是按需附着(关于这点 以后会有心得分享),这就做到了 数据与方法相分离,易扩展,易修改,轻耦合。

显然,转换后并未增加多少代码,却去除了C#式的杂碎,统一了风格(统一的风格要比表面上少写几句代码 更能保证代码的质量,降低编程时的心智负担,减少出错几率和增加安全性)

本文未涉及封装和可见性控制。但不会受这样的转换所影响。

对此感兴趣的朋友可以参阅我的另一篇笔记 《F#语言的类型扩展实现面向对象编程 及与GO语言的比较》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值