繁体   English   中英

无需使用接口即可访问F#记录库属性

[英]Access to F# record base properties without using interface

F#记录不能继承,但可以实现接口。 例如,我想创建不同的控制器:

type ControllerType =
    | Basic
    | Advanced1
    | Advanced1RAM
    | Advanced1RAMBattery
    | Advanced2

// base abstract class
type IController =
    abstract member rom : byte[]
    abstract member ``type`` : ControllerType

type BasicController =
    { rom : byte[]
      ``type`` : ControllerType }
    interface IController with
        member this.rom = this.rom
        member this.``type`` = this.``type``

type AdvancedController1 =
    { ram : byte[]
      rom : byte[]
      ``type`` : ControllerType }
    interface IController with
        member this.rom = this.rom
        member this.``type`` = this.``type``

type AdvancedController2 =
    { romMode : byte
      rom : byte[]
      ``type`` : ControllerType }
    interface IController with
        member this.rom = this.rom
        member this.``type`` = this.``type``

let init ``type`` =
    match ``type`` with
    | Basic ->
        { rom = Array.zeroCreate 0
          ``type`` = Basic } :> IController
    | Advanced1 | Advanced1RAM | Advanced1RAMBattery ->
        { ram = Array.zeroCreate 0
          rom = Array.zeroCreate 0
          ``type`` = ``type`` } :> IController
    | Advanced2 ->
        { romMode = 0xFFuy
          rom = Array.zeroCreate 0
          ``type`` = ``type`` } :> IController

我有两个问题:

  1. 当我创建一个控制器记录时,我需要将它转发到一个接口。 有没有更好的方法来编写上面的init函数:> IController每条记录?
  2. 我尝试过有区别的工会但不知何故最终会像这个例子一样写出对话。 但是接口是一个.NET的东西,我如何以功能的方式重写示例,使用组合而不是继承?

回答第一个问题:不,你不能每次都摆脱向上倾斜。 F#不执行自动类型强制(这是一件好事),并且所有match分支必须具有相同的类型。 所以唯一要做的就是手动强制。

对第二个问题的回答:受歧视的工会代表“封闭的世界假设” - 也就是说,当你事先知道不同案件的数量时,它们是好的,而你以后不想扩展它们(你的世界是“封闭的”) 。 在这种情况下,您可以让编译器帮助您确保每个使用您的东西的人处理所有情况。 这对某些应用来说非常强大。

另一方面,有时你需要设计你的东西,以便以后可以通过外部插件进行扩展。 这种情况通常被称为“开放世界假设”。 在这种情况下,接口工作。 但它们不是唯一的方法。

接口只不过是函数的记录, 方法通用性除外。 如果你对泛型方法不感兴趣, 并且你不打算稍后向特定的实现进行向下转换(无论如何都要做坏事),你可以将你的“开放世界”事物表示为函数记录:

type Controller = { 
   ``type``: ControllerType
   controlSomething: ControllableThing -> ControlResult
}

现在,您可以通过提供不同的controlSomething实现来创建不同类型的控制器:

let init ``type`` =
    match ``type`` with
    | Basic ->
        let rom = Array.zeroCreate 0
        { ``type`` = Basic
          controlSomething = fun c -> makeControlResult c rom }

    | Advanced1 | Advanced1RAM | Advanced1RAMBattery ->
        let ram = Array.zeroCreate 0
        let rom = Array.zeroCreate 0
        { ``type`` = ``type`` 
          controlSomething = fun c -> makeControlResultWithRam c rom ram }

    | Advanced2 ->
        let romMode = 0xFFuy
        let rom = Array.zeroCreate 0
        { ``type`` = ``type`` 
          controlSomething = fun c -> /* whatever */ }

顺便说一句,这也消除了向上转换,因为现在一切都属于同一类型。 顺便提一下,您的代码现在要小得多,因为您不必将所有不同的控制器明确定义为自己的类型。

问:等等,但现在,如何从外部访问ramromromMode

A:嗯,你打算怎么用界面做的? 您是否要将接口转发为特定的实现类型,然后访问其字段? 如果你打算这样做,那么你就回到了“封闭的世界”,因为现在每个处理你的IController人都需要知道所有的实现类型以及如何使用它们。 如果是这种情况,那么你最好先选择一个有区别的联盟。 (就像我上面说的那样,向下倾斜并不是一个好主意)

另一方面,如果您对向下转换为特定类型不感兴趣,则意味着您只对使用所有控制器实现的功能感兴趣(这是接口的全部概念)。 如果是这种情况,那么功能记录就足够了。

最后 ,如果你兴趣在通用的方法,你必须使用接口,但仍然没有宣布一切,类型,F#有内嵌的接口实现:

type Controller =  
   abstract member ``type``: ControllerType
   abstract member genericMethod: 'a -> unit

let init ``type`` =
    match ``type`` with
    | Basic ->
        let rom = Array.zeroCreate 0
        { new Controller with 
             member this.``type`` = Basic
             member this.genericMethod x = /* whatever */ }

    // similar for other cases

这比记录更冗长,你不能轻易修改它们(即没有{ ... with ... }语法接口),但如果你绝对需要通用方法,那就有可能。

回答第一个问题 :你可以让编译器完成大部分工作:

let init = function
| Basic ->
    { rom = [||]; ``type`` = Basic } :> IController
| Advanced1 | Advanced1RAM | Advanced1RAMBattery as t ->
    { ram = [||]; rom = [||]; ``type`` = t } :> _
| Advanced2 ->
    upcast { romMode = 0xFFuy; rom = [||]; ``type`` = Advanced2 }

也就是说,指定一次返回类型,然后让编译器填入_或使用upcast

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM