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

时间:2011-05-20 17:04:54

标签: f# enums 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

4 个答案:

答案 0 :(得分:12)

我认为一般来说这是一个很高的命令,因为枚举是“弱”的。 ConsoleSpecialKey是“完整”枚举的一个很好的示例,其中ControlCControlBreak分别由0和1表示,是它可以承担的唯一有意义的值。但是我们遇到了问题,你可以将任何整数强制转换为ConsoleSpecialKey!:

let x = ConsoleSpecialKey.Parse(typeof<ConsoleSpecialKey>, "32") :?> ConsoleSpecialKey

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

更不用说更复杂的枚举,如System.Reflection.BindingFlags,它们用于比特掩码,但通过简单枚举的类型信息无法区分,使图片更加复杂 编辑:实际上, @ildjarn指出按惯例,Flags属性用于区分完整和位掩码枚举,尽管编译器不会阻止您在未标记此属性的枚举上使用按位操作,再次显示枚举的弱点)。

但是,如果您正在使用特定的“完整”枚举,例如ConsoleSpecialKey,并且编写最后一个不完整的模式匹配案例,那么您总是会惹恼您,您可以随时提供完整的活动模式:

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

//complete
match value with
| ControlC -> ()
| ControlBreak -> ()

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

答案 1 :(得分: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

实施例

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

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

答案 2 :(得分:4)

我认为它是F#的一个特性,它会强制你处理枚举的意外值(因为可以通过显式转换创建它们,并且因为更高版本的程序集可能会添加其他命名值)。你的方法看起来很好。另一种选择是创建一个活动模式:

let (|UnhandledEnum|) (e:'a when 'a : enum<'b>) = 
    failwithf "Unexpected enum member %A:%A" typeof<'a> e

function
| System.ConsoleSpecialKey.ControlC -> ()
| System.ConsoleSpecialKey.ControlBreak -> ()
| UnhandledEnum r -> r

这里匹配UnhandledEnum模式的过程将引发异常,但返回类型是可变的,因此无论从匹配返回什么类型,它都可以在模式的右侧使用。

答案 3 :(得分:0)

这是F#语言的一个小烦恼,而不是一个功能。可以创建无效的枚举,但这并不意味着F#模式匹配代码必须处理它们。如果模式匹配失败,因为枚举采用了超出定义范围的值,则错误不在模式匹配代码中,而是在生成无意义值的代码中。因此,枚举上没有考虑无效值的模式匹配没有任何问题。

想象一下,如果按照相同的逻辑,F#用户每次遇到.Net引用类型时都被迫进行空检查(可以为null,就像枚举可以存储无效的整数一样)。语言将变得无法使用。幸运的是,枚举不会那么多,我们可以替换DUs。