解决枚举上不完整的模式匹配问题

Dan*_*iel 11 enums f# pattern-matching

在模式匹配时,是否有任何创造性的方法可以解决.NET的"弱"枚举问题?我希望它们的功能与DU类似.这是我目前处理它的方式.有更好的想法吗?

[<RequireQualifiedAccess>]
module Enum =
  let unexpected<'a, 'b, 'c when 'a : enum<'b>> (value:'a) : 'c = //'
    failwithf "Unexpected enum member: %A: %A" typeof<'a> value //'

match value with
| ConsoleSpecialKey.ControlC -> ()
| ConsoleSpecialKey.ControlBreak -> ()
| _ -> Enum.unexpected value //without this, gives "incomplete pattern matches" warning
Run Code Online (Sandbox Code Playgroud)

Ste*_*sen 12

我认为总的来说这是一个很高的命令,因为枚举是"弱"的.ConsoleSpecialKey是一个"完整的",其中枚举的一个很好的例子ControlCControlBreak,这是由0和1分别表示的,是它可以采取上的唯一有意义的值.但是我们有一个问题,你可以将任何整数强制转换为ConsoleSpecialKey!:

let x = ConsoleSpecialKey.Parse(typeof<ConsoleSpecialKey>, "32") :?> ConsoleSpecialKey
Run Code Online (Sandbox Code Playgroud)

所以你给出的模式确实不完整,确实需要处理.

(更不用说更复杂的枚举,如System.Reflection.BindingFlags用于比特掩码,但通过简单枚举的类型信息无法区分,使图片 编辑更加复杂化:实际上,@ ijjarn指出Flags属性按惯例用于区分完整和位掩码枚举,虽然编译器不会阻止你在未标记此属性的枚举上使用按位运算,再次揭示枚举的弱点).

但是,如果你正在处理一个特定的"完整"枚举,ConsoleSpecialKey并且写下最后一个不完整的模式匹配案例,那么你总是会惹恼你,你可以随时提出一个完整的活动模式:

let (|ControlC|ControlBreak|) value =
    match value with
    | ConsoleSpecialKey.ControlC -> ControlC
    | ConsoleSpecialKey.ControlBreak -> ControlBreak
    | _ -> Enum.unexpected value

//complete
match value with
| ControlC -> ()
| ControlBreak -> ()
Run Code Online (Sandbox Code Playgroud)

然而,这类似于简单地保留未完成的模式匹配案例并且禁止警告.我认为你目前的解决方案很好,只要坚持下去就会很好.

  • 当成员被添加到枚举中时,使用通配符模式的最大优点是不会被警告. (2认同)
  • @Daniel - 整洁!增加你的`Enum.unexpected`实现,以指示失败是由于成员被添加到枚举还是坏值,肯定会让你更接近你所追求的. (2认同)

Dan*_*iel 9

根据斯蒂芬在回答评论中提出的建议,我最终得到了以下解决方案.Enum.unexpected区分无效的枚举值和未处理的案例(可能是由于枚举成员后来被添加),通过抛出FailureException前一种情况和Enum.Unhandled后一种情况.

[<RequireQualifiedAccess>]
module Enum =

  open System

  exception Unhandled of string

  let isDefined<'a, 'b when 'a : enum<'b>> (value:'a) =
    let (!<) = box >> unbox >> uint64
    let typ = typeof<'a>
    if typ.IsDefined(typeof<FlagsAttribute>, false) then
      ((!< value, System.Enum.GetValues(typ) |> unbox)
      ||> Array.fold (fun n v -> n &&& ~~~(!< v)) = 0UL)
    else Enum.IsDefined(typ, value)

  let unexpected<'a, 'b, 'c when 'a : enum<'b>> (value:'a) : 'c =
    let typ = typeof<'a>
    if isDefined value then raise <| Unhandled(sprintf "Unhandled enum member: %A: %A" typ value)
    else failwithf "Undefined enum member: %A: %A" typ value
Run Code Online (Sandbox Code Playgroud)

type MyEnum =
  | Case1 = 1
  | Case2 = 2

let evalEnum = function
  | MyEnum.Case1 -> printfn "OK"
  | e -> Enum.unexpected e

let test enumValue =
  try 
    evalEnum enumValue
  with
    | Failure _ -> printfn "Not an enum member"
    | Enum.Unhandled _ ->  printfn "Unhandled enum"

test MyEnum.Case1 //OK
test MyEnum.Case2 //Unhandled enum
test (enum 42)    //Not an enum member
Run Code Online (Sandbox Code Playgroud)

显然,它在运行时而不是编译时警告未处理的情况,但它似乎是我们能做的最好的.