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

C# 无需使用接口即可访问F#记录库属性,c#,.net,f#,functional-programming,record,C#,.net,F#,Functional Programming,Record,F#记录不能被继承,但它们可以实现接口。例如,我想创建不同的控制器: type ControllerType = | Basic | Advanced1 | Advanced1RAM | Advanced1RAMBattery | Advanced2 // base abstract class type IController = abstract member rom : byte[] abstract member ``type``

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
我有两个问题:

  • 当我创建一个控制器记录时,我需要将它向上转换到一个接口。有没有更好的方法来编写上面的
    init
    函数而不使用
    :>IController
    每条记录
  • 我尝试过歧视性的工会,但不知怎的,最终写下了这个例子。但是接口是一个.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 */ }
    
    顺便说一句,这也消除了向上投射,因为现在所有的东西都是相同的类型。顺便提一下,您的代码现在要小得多,因为您不必显式地将所有不同的控制器定义为它们自己的类型

    Q:等等,现在,我如何从外部访问
    ram
    rom
    rom模式

    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 }
    

    也就是说,只需指定一次返回类型,然后让编译器为
    \uu
    或使用
    upcast

    将函数用作greate。我不需要访问
    ram
    rom
    romMode
    ,但我可以在哪里存储这些数据以使用后者?您可以将它们存储在函数可以访问的闭包中。看看我的例子:看看我如何在
    controlSomething
    的实现中使用
    ram
    rom
    ?你甚至可以使它们可变!(尽管我强烈反对)