Обход неполного сопоставления с образцом в перечислениях

Есть ли какие-нибудь творческие способы обойти "слабые" перечисления.NET при сопоставлении с образцом? Я бы хотел, чтобы они работали аналогично ОУ. Вот как я в настоящее время справляюсь с этим. Есть идеи получше?

[<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 ответа

Я думаю, что в целом это высокий заказ, потому что перечисления "слабые". ConsoleSpecialKey хороший пример "полного" перечисления, где ControlC а также ControlBreak, которые представлены 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 -> ()

Однако это похоже на простое обращение с неполным регистром и подавление предупреждения. Я думаю, что ваше текущее решение хорошо, и вам было бы хорошо просто придерживаться его.

Следуя предложению, которое Стивен сделал в комментариях к своему ответу, я получил следующее решение. 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

Очевидно, что он предупреждает о необработанных случаях во время выполнения, а не во время компиляции, но, похоже, это лучшее, что мы можем сделать.

Я бы сказал, что это особенность 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 вызовет исключение, но возвращаемый тип является переменным, поэтому его можно использовать в правой части шаблона независимо от того, какой тип возвращается из совпадения.

Это небольшое раздражение языка F#, а не функция. Возможно создание недопустимых перечислений, но это не значит, что код соответствия шаблону F# должен иметь с ними дело. Если при сопоставлении с образцом происходит сбой, поскольку перечисление получило значение за пределами определенного диапазона, ошибка не в коде совпадения с образцом, а в коде, который сгенерировал бессмысленное значение. Поэтому нет ничего плохого в сопоставлении с образцом в перечислении, которое не учитывает недопустимые значения.

Представьте себе, если по той же логике пользователи F# были вынуждены делать нулевую проверку каждый раз, когда сталкивались с ссылочным типом.Net (который может быть нулевым, как перечисление может хранить недопустимое целое число). Язык станет непригодным для использования. К счастью, перечисления не подходят так часто, и мы можем заменить DU.

Другие вопросы по тегам