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