Обход неполного сопоставления с образцом в перечислениях - PullRequest
11 голосов
/ 20 мая 2011

Есть ли какие-нибудь творческие способы обойти "слабые" перечисления .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 ]

12 голосов
/ 20 мая 2011

Я думаю, что в целом это высокий порядок, потому что перечисления "слабые". ConsoleSpecialKey является хорошим примером "полного" перечисления, где ControlC и ControlBreak, которые представлены соответственно 0 и 1, являются единственными значимыми значениями, которые он может принимать. Но у нас есть проблема, вы можете привести любое целое число к ConsoleSpecialKey!:

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

Так что шаблон, который вы дали, на самом деле неполон и действительно требует обработки.

( не говоря уже о более сложных перечислениях, таких как System.Reflection.BindingFlags, которые используются для маскировки битов и все же неразличимы в информации о типах из простых перечислений, что еще больше усложняет картину edit: фактически, @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 -> ()

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

9 голосов
/ 25 мая 2011

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

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

3 голосов
/ 20 мая 2011

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

0 голосов
/ 06 марта 2018

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

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

Редактировать: эта проблема теперь решается с помощью https://github.com/dotnet/fsharp/pull/4522, при условии, что пользователи добавляют #nowarn "104" вручную.Вы получите предупреждения по несопоставленным определенным случаям DU, но не получите предупреждения, если вы охватили их все.

...