6. Enums, tipos suma y match
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Código: examples/l06_enums.v y examples/l06_versions.v — v run examples/l06_versions.v desde code/v-for-csharp-java. Los datos son los proyectos de ga de la lección 5.
| C# | Java | V | |
|---|---|---|---|
| Declaración | enum Sdk { Library, Web } |
enum Sdk { LIBRARY, WEB } |
enum Sdk { library web } |
| Valor subyacente | int, convertido con un cast |
un objeto, con ordinal() |
int por defecto, enum Sdk as u8 |
| Métodos | métodos de extensión | sí | sí |
<, + 1 |
permitidos | compareTo, sin + |
rechazados: primero int(sdk) |
| Desde una cadena | Enum.Parse<Sdk>("Web") |
Sdk.valueOf("WEB") |
Sdk.from('web')! |
| Conjunto de flags | [Flags] |
EnumSet |
@[flag] |
Fuente: Enums.
En ga, la columna sdk tiene dos valores. Sdk les da nombre, con un método, y una función que lee el nombre MSBuild:
enum Sdk { library web}
// Un enum puede tener métodosfn (s Sdk) msbuild_name() string { return match s { .library { 'Microsoft.NET.Sdk' } .web { 'Microsoft.NET.Sdk.Web' } }}
fn parse_sdk(name string) !Sdk { return match name { 'Microsoft.NET.Sdk' { .library } 'Microsoft.NET.Sdk.Web' { .web } else { error('unknown SDK: ${name}') } }}Donde el tipo es conocido, basta con .web: en otros sitios, Sdk.web completo. match es una expresión, como una expresión switch de C# o un switch de Java con ->.
sdk := parse_sdk('Microsoft.NET.Sdk.Web')!println('${sdk} ${int(sdk)} ${sdk.msbuild_name()}')println(Sdk.from('library')!)println(Sdk.from(1)!)parse_sdk('Microsoft.NET.Sdk.Razor') or { println(err.msg()) }web 1 Microsoft.NET.Sdk.Weblibrarywebunknown SDK: Microsoft.NET.Sdk.RazorUn enum imprime su nombre, e int(sdk) da su valor. Sin embargo, un enum no es un número:
fn main() { sdk := Sdk.library if sdk < Sdk.web { println('library') } println(sdk + 1)}e06_enum_operators.v:8:9: error: only `==` and `!=` are defined on `enum`, use an explicit cast to `int` if needed 6 | fn main() { 7 | sdk := Sdk.library 8 | if sdk < Sdk.web { | ^ 9 | println('library') 10 | }e06_enum_operators.v:11:10: error: infix expr: cannot use `int literal` (right expression) as `Sdk` 9 | println('library') 10 | } 11 | println(sdk + 1) | ~~~~~~~ 12 | }C# acepta ambas líneas. El segundo mensaje ayuda menos que el primero: dice que 1 no es un Sdk, no que + esté rechazado.
match debe cubrir todos los valores
Sección titulada «match debe cubrir todos los valores»msbuild_name no tiene else: nombra ambos valores. Añade un tercer valor al enum, y todos esos match dejan de compilar:
enum Sdk { library web worker}
fn default_port(sdk Sdk) int { return match sdk { .library { 0 } .web { 8080 } }}e06_match_enum.v:8:9: error: match must be exhaustive (add match branches for: `.worker` or `else {}` at the end) 6 | 7 | fn default_port(sdk Sdk) int { 8 | return match sdk { | ~~~~~~~~~~~ 9 | .library { 0 } 10 | .web { 8080 }C# solo emite una advertencia: con .NET 11, una expresión switch sobre un enum sin Worker da warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Sdk.Worker' is not covered., y lanza una SwitchExpressionException en tiempo de ejecución si llega ese valor. Un match sobre cadenas o números no puede enumerar todos los valores, así que siempre necesita else:
fn sdk_kind(name string) string { return match name { 'Microsoft.NET.Sdk' { 'library' } 'Microsoft.NET.Sdk.Web' { 'web' } }}e06_match_string.v:2:9: error: match must be exhaustive (add `else {}` at the end) 1 | fn sdk_kind(name string) string { 2 | return match name { | ~~~~~~~~~~~~ 3 | 'Microsoft.NET.Sdk' { 'library' } 4 | 'Microsoft.NET.Sdk.Web' { 'web' }Una rama puede enumerar varios valores, o un rango inclusivo con tres puntos (size); los slices de la lección 4 usan dos puntos, y excluyen el final:
fn size(references int) string { return match references { 0 { 'none' } 1...3 { 'few' } 4, 5, 6 { 'some' } else { 'many' } }}0: none2: few5: some23: manyUn enum @[flag] da un bit a cada valor, como [Flags] en C#. traits activa los flags de un proyecto a partir de sus campos CSV:
@[flag]enum Traits { web fsharp in_solution}
fn traits(fields []string) Traits { mut t := Traits.zero() if fields[3] == 'Microsoft.NET.Sdk.Web' { t.set(.web) } if fields[2] == 'F#' { t.set(.fsharp) } if fields[5] == 'true' { t.set(.in_solution) } return t}mut by_sdk := map[Sdk]int{}mut web_outside := []string{}for line in os.read_lines('data/ga/projects.csv')![1..] { f := line.split(',') by_sdk[parse_sdk(f[3])!]++ t := traits(f) if t.has(.web) && !t.has(.in_solution) { web_outside << f[1] }}println(by_sdk)println('web projects outside the solution: ${web_outside}')println(Traits.web | Traits.in_solution){library: 95, web: 16}web projects outside the solution: ['FloorManager', 'GA.DocumentProcessing.Service', 'ScenesService', 'GA.Business.AI', 'FretboardExplorer', 'GA.WebBlazorApp']Traits{.web | .in_solution}Seis de los 16 proyectos web de ga no figuran en AllProjects.slnx, el archivo de la solución.
Tipos suma
Sección titulada «Tipos suma»Una versión NuGet no siempre son tres números. El package_refs.csv de ga tiene versiones estables (9.5.1), preliminares (1.0.0-beta.24164.1) y versiones flotantes (8.*-*, la última 8.x, preliminares incluidas). Cada forma tiene sus propios datos. C# y Java lo modelan con una jerarquía de clases o records; V tiene un tipo para ello:
| C# | Java 21+ | V | |
|---|---|---|---|
| Declaración | un record Version abstracto, y record Release(…) : Version |
sealed interface Version permits … y records |
type Version = Floating | Prerelease | Release |
| Cerrado | no: cualquier ensamblado puede derivar | sí | sí |
| Probar y convertir | if (v is Release r) |
if (v instanceof Release r) |
if v is Release { } |
Un caso olvidado en un switch |
advertencia CS8509, y luego una excepción en tiempo de ejecución | un error de compilación | un error de compilación |
Fuente: Sum types.
Version es uno de tres structs. Una función que devuelve !Version devuelve cualquiera de ellos:
struct Release { parts []int}
struct Prerelease { release Release label string}
struct Floating { pattern string}
type Version = Floating | Prerelease | Release
fn parse_version(s string) !Version { if s.contains('*') { return Floating{s} } if dash := s.index('-') { return Prerelease{parse_release(s[..dash])!, s[dash + 1..]} } return parse_release(s)!}match sobre un tipo suma tiene una rama por variante; dentro de cada rama, v tiene el tipo de la variante, como con is:
fn (v Version) describe() string { return match v { Release { 'release ${v}' } Prerelease { 'prerelease ${v.label} of ${v.release}' } Floating { 'floating ${v.pattern}' } }}
fn (v Version) is_stable() bool { return v is Release}for s in ['9.5.1', '1.0.0-beta.24164.1', '8.*-*', '1.x'] { v := parse_version(s) or { println('${s}: ${err.msg()}') continue } println('${s}: ${v.describe()}, stable: ${v.is_stable()}, type: ${v.type_name()}')}9.5.1: release 9.5.1, stable: true, type: Release1.0.0-beta.24164.1: prerelease beta.24164.1 of 1.0.0, stable: false, type: Prerelease8.*-*: floating 8.*-*, stable: false, type: Floating1.x: strconv.atoi: parsing "x": invalid radix 10 character${v} en la rama Release usa Release.str(), que une las partes con puntos. type_name() da la variante en tiempo de ejecución.
Una variante olvidada es un error, como un valor de enum olvidado:
fn describe(v Version) string { return match v { Release { 'release' } Prerelease { 'prerelease ${v.label}' } }}e06_match_variant.v:16:9: error: match must be exhaustive (add match branches for: `Floating` or `else {}` at the end) 14 | 15 | fn describe(v Version) string { 16 | return match v { | ~~~~~~~~~ 17 | Release { 'release' } 18 | Prerelease { 'prerelease ${v.label}' }El mismo código en Java 25, con una sealed interface y tres records, da error: the switch expression does not cover all possible input values. Una rama else {} silencia la comprobación, y con ella el recordatorio cuando se añade una variante.
Las versiones de ga
Sección titulada «Las versiones de ga»mut kinds := map[string]int{}for line in os.read_lines('data/ga/package_refs.csv')![1..] { f := line.split(',') v := parse_version(f[2])! kinds[v.type_name()]++ match v { Floating { println('${f[1]} ${v.pattern} in ${short(f[0])}') } Release { if v.parts.len != 3 { println('${f[1]} ${v} has ${v.parts.len} parts, in ${short(f[0])}') } } else {} }}println(kinds)ModelContextProtocol 0.* in DemerzelBridgeMicrosoft.AspNetCore.SpaProxy 8.*-* in ReactApp1.ServerMicrosoft.KernelMemory.Core 0.91.241101.1 has 4 parts, in GaCLI{'Release': 443, 'Prerelease': 33, 'Floating': 2}Las 478 versiones se analizan sin error. Dos son flotantes: cada restauración de DemerzelBridge y ReactApp1.Server puede elegir un paquete más reciente, así que dos compilaciones del mismo commit pueden diferir, salvo que el proyecto use un archivo de bloqueo. 0.91.241101.1 tiene cuatro partes, lo que NuGet acepta (números de versión normalizados).
as, y un campo que solo tiene una variante
Sección titulada «as, y un campo que solo tiene una variante»as convierte a una variante, y hace panic con la equivocada:
fn main() { v := Version(Floating{'0.*'}) r := v as Release println(r.parts)}V panic: as cast: cannot cast `main.Floating` to `main.Release`Prefiere is o match, que no pueden fallar. Una trampa: V 0.5.2 también acepta v.parts directamente sobre un Version, sin as, porque solo Release tiene un campo llamado parts. Compila, y hace panic con el mismo mensaje:
fn main() { v := Version(Floating{'0.*'}) println(v.parts)}V panic: as cast: cannot cast `main.Floating` to `main.Release`El checker reescribe un campo que existe en exactamente una variante como un cast as (checker.v, líneas 3180-3201). La documentación no lo menciona. Un campo que tienen todas las variantes, con el mismo tipo, se puede leer sin cast (table.v, líneas 878-925).
Un tipo suma recursivo
Sección titulada «Un tipo suma recursivo»Una variante puede contener el propio tipo suma. Tree es un proyecto y los proyectos que referencia, hasta los proyectos que no referencian nada:
struct Leaf { name string}
struct Branch { name string children []Tree}
type Tree = Branch | Leaf
fn (t Tree) count() int { return match t { Leaf { 1 } Branch { mut n := 1 for c in t.children { n += c.count() } n } }}Una rama de una expresión match puede contener sentencias; su última expresión, n, es su valor. print recorre el árbol de la misma manera:
GaCli (1) GA.Business.DSL (3) GA.Core GA.Domain.Core (2) GA.Business.Config GA.Core GA.Domain.Services (5) GA.Business.Config GA.Business.Core (1) GA.Domain.Core (2) GA.Business.Config GA.Core GA.Core GA.Domain.Core (2) GA.Business.Config GA.Core GA.Domain.Repositories (1) GA.Domain.Core (2) GA.Business.Config GA.Core20 nodesEl árbol tiene 20 nodos para 8 proyectos: GA.Domain.Core aparece cuatro veces, una bajo cada uno de los cuatro proyectos que lo referencian. Las referencias forman un grafo, no un árbol; la lección 7 cuenta cada proyecto una sola vez.
Puntos clave
Sección titulada «Puntos clave»- Un enum tiene métodos e imprime su nombre; solo
==y!=funcionan con él,int(e)da el número,Sdk.fromlee un nombre o un valor,@[flag]crea un conjunto de bits. - Un tipo suma enumera sus variantes: es cerrado, como una sealed interface de Java, y no necesita clase base.
matchdebe cubrir todos los valores del enum o todas las variantes, o tenerelse; sobre cadenas y números,elsees obligatorio. Unelsetambién oculta las variantes añadidas más tarde.isymatchconvierten de forma segura;ashace panic con la variante equivocada, y lo mismo un campo que solo tiene una variante, que V 0.5.2 acepta sinas.
Ejercicios
Sección titulada «Ejercicios»- Añade
fn (v Version) major() ?int, que devuelve el primer número de una versión estable o preliminar, y de una versión flotante cuando empieza por un número (8.*-*da 8,*danone).
Solución
fn (v Version) major() ?int { return match v { Release { v.parts[0] } Prerelease { v.release.parts[0] } Floating { strconv.atoi(v.pattern.all_before('.')) or { return none } } }}Release: 9Prerelease: 1Floating: 8Floating: no major versionLa rama Floating sale de la función con none cuando el patrón no empieza por un número; las otras ramas dan un int, que se convierte en el option.
- ga añade un servicio worker, y
Sdkrecibe un tercer valor,worker, paraMicrosoft.NET.Sdk.Worker. ¿Qué funciones del06_enums.vte obliga el compilador a cambiar, y qué pasa con las demás?
Solución
msbuild_name deja de compilar, con el error de e06_match_enum.v: su match sobre el enum no tiene else. parse_sdk sigue compilando: hace match sobre cadenas, con un else que devuelve un error, así que un proyecto Microsoft.NET.Sdk.Worker fallaría en tiempo de ejecución, en by_sdk[parse_sdk(f[3])!]++, con unknown SDK: Microsoft.NET.Sdk.Worker. traits no cambia, y contaría un worker como ni web ni ninguna otra cosa.
- ¿Por qué
if v is Release { v.parts }es seguro, mientras quev.partsa secas puede hacer panic, aunque ambos compilen?
Solución
Dentro de if v is Release, el compilador conoce la variante y lee el campo directamente. v.parts a secas se reescribe como (v as Release).parts, un cast comprobado en tiempo de ejecución, que hace panic cuando v contiene otra variante. En términos de C#, lo primero es if (v is Release r) { r.Parts }, lo segundo es ((Release)v).Parts, que lanza una InvalidCastException, salvo que en V nada en el código fuente muestra el cast.