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

发布于 2024-11-08 13:31:19 字数 495 浏览 3 评论 0原文

在模式匹配时,是否有任何创造性的方法来解决 .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

Are there any creative ways to work around .NET's "weak" enums when pattern matching? I'd like them to function similarly to DUs. Here's how I currently handle it. Any better ideas?

[<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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

峩卟喜欢 2024-11-15 13:31:19

我认为总的来说这是一个艰巨的任务,正是因为枚举是“弱”的。 ConsoleSpecialKey 是“完整”枚举的一个很好的示例,其中分别由 0 和 1 表示的 ControlCControlBreak 是唯一有意义的它可以承担的价值观。但我们有一个问题,您可以将任何整数强制转换为 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 -> ()

然而,这类似于简单地不处理不完整的模式匹配情况并抑制警告。我认为你当前的解决方案很好,你只要坚持下去就好了。

I think in general this is a tall order, exactly because enums are "weak". ConsoleSpecialKey is a good example of a "complete" enum where ControlC and ControlBreak, which are represented by 0 and 1 respectively, are the only meaningful values it can take on. But we have a problem, you can coerce any integer into a ConsoleSpecialKey!:

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

So the pattern you gave really is incomplete and really does needs to be handled.

(not to mention more complex enums like System.Reflection.BindingFlags, which are used for bitmasking and yet indistinguishable through type information from simple enums, further complicating the picture edit: actually, @ildjarn pointed out that the Flags attribute is used, by convention, to distinguish between complete and bitmask enums, though the compiler won't stop you from using bitwise ops on an enum not marked with this attribute, again revealing the weakens of enums).

But if you are working with a specific "complete" enum like ConsoleSpecialKey and writing that last incomplete pattern match case all the time is really bugging you, you can always whip up a complete active pattern:

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

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

However that's akin to simply leaving the incomplete pattern match case unhandled and suppressing the warning. I think your current solution is nice and you would be good just to stick with it.

浅忆 2024-11-15 13:31:19

根据斯蒂芬在评论中对他的回答提出的建议,我最终得到了以下解决方案。 Enum.unexpected 通过在前一种情况下抛出 FailureExceptionEnum.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

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

Following the suggestion Stephen made in the comments to his answer, I ended up with the following solution. Enum.unexpected distinguishes between invalid enum values and unhandled cases (possibly due to enum members being added later) by throwing a FailureException in the former case and Enum.Unhandled in the latter.

[<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

Example

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

Obviously, it warns about unhandled cases at run-time instead of compile-time, but it seems to be the best we can do.

你不是我要的菜∠ 2024-11-15 13:31:19

我认为这是 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 模式匹配的过程将抛出异常,但返回类型是可变的,因此无论是什么类型,它都可以在模式的右侧使用比赛归来。

I'd argue that it's a feature of F# that it forces you to handle unexpected values of an enum (since it is possible to create them via explicit conversions, and since additional named values may be added by later versions of an assembly). Your approach looks fine. Another alternative would be to create an active pattern:

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

Here the process of matching against the UnhandledEnum pattern will throw an exception, but the return type is variable so that it can be used on the right hand side of the pattern no matter what type is being returned from the match.

迎风吟唱 2024-11-15 13:31:19

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

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

编辑:此问题现已通过 https://github.com/dotnet/fsharp/pull/ 解决4522,取决于用户手动添加#nowarn“104”。您将收到有关未匹配的定义 DU 案例的警告,但如果您已涵盖所有案例,则不会收到警告。

This is a minor annoyance of the F# language, not a feature. Invalid enums are possible to create, but that doesn't mean that F# pattern matching code should have to deal with them. If a pattern match fails because the enum took a value outside of the defined range, the error is not in the pattern match code but in the code that generated the meaningless value. Therefore there is nothing wrong with a pattern match on an enum that does not account for invalid values.

Imagine if, by the same logic, F# users were forced to do a null check every time they came across a .Net reference type (which can be null, just like an enum can store an invalid integer). The language would become unusable. Fortunately enums don't come up as much and we can substitute DUs.

Edit: this issue is now solved by https://github.com/dotnet/fsharp/pull/4522, subject to users adding #nowarn "104" manually. You will get warnings on unmached defined DU cases, but no warning if you have covered them all.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文