4. Arrays, maps, slices y memoria
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/l04_arrays.v, examples/l04_maps.v, examples/l04_references.v y examples/l04_memory.v — v run examples/l04_maps.v desde code/v-for-csharp-java.
Un array de V, []int, crece como una List<int> o un ArrayList<Integer>; en el código del día a día no hay un tipo de array de tamaño fijo aparte ([3]int existe, para los buffers).
mut lines := [474, 341, 326]lines << 394lines << [120, 88]println('${lines} len=${lines.len}')
squares := []int{len: 5, init: index * index}println(squares)[474, 341, 326, 394, 120, 88] len=6[0, 1, 4, 9, 16]| C# | Java | V | |
|---|---|---|---|
| Tipo | List<int> |
ArrayList<Integer> |
[]int |
| Literal | [474, 341] (C# 12) |
List.of(474, 341) (inmutable) |
[474, 341] |
| Vacío, con capacidad | new List<int>(100) |
new ArrayList<>(100) |
[]int{cap: 100} |
| Añadir | Add(x), AddRange(xs) |
add(x), addAll(xs) |
a << x, a << xs |
| Número de elementos | Count |
size() |
len |
| Contiene | Contains(x) |
contains(x) |
x in a |
| Índice fuera de rango | ArgumentOutOfRangeException |
IndexOutOfBoundsException |
panic, o a[i] or { } |
Fuentes: V arrays, List<T>, ArrayList.
init se evalúa para cada elemento, con index como posición. Los elementos siempre se inicializan: []int{len: 5} son cinco ceros.
filter, map y ordenación
Sección titulada «filter, map y ordenación»long := lines.filter(it > 300)println(long)println(lines.map(it / 10))println(lines.filter(fn (n int) bool { return n < 100}))println('${lines.any(it > 400)} ${lines.all(it > 100)}')println('${326 in lines} ${lines.index(394)}')[474, 341, 326, 394][47, 34, 32, 39, 12, 8][88]true falsetrue 3filter, map, any y all reciben una expresión en la que it es el elemento, o una función. Devuelven un nuevo array de inmediato: no son perezosos (lazy) como LINQ o los streams.
| LINQ | Streams | V |
|---|---|---|
Where(n => n > 300) |
filter(n -> n > 300) |
filter(it > 300) |
Select(n => n / 10) |
map(n -> n / 10) |
map(it / 10) |
Any(…), All(…) |
anyMatch(…), allMatch(…) |
any(…), all(…) |
OrderBy(n => n) |
sorted() |
sorted() |
OrderByDescending(n => n) |
sorted(Comparator.reverseOrder()) |
sorted(a > b) |
mut sorted := lines.sorted()println(sorted)sorted.sort(a > b)println(sorted)titles := ['Mission', 'Journal', '1. Install']println(titles.sorted(a.len < b.len))[88, 120, 326, 341, 394, 474][474, 394, 341, 326, 120, 88]['Mission', 'Journal', '1. Install']sort ordena en su sitio, sorted devuelve una copia. La expresión de ordenación llama a los dos elementos a y b, y no puede usar nada más: una variable de la función que la contiene queda fuera de su alcance.
fn main() { lines := { 'duckdb': 3177 'github-actions': 2785 } mut courses := lines.keys() courses.sort(lines[a] > lines[b]) println(courses)}e04_sort_capture.v:7:15: error: can not access external variable `lines` 5 | } 6 | mut courses := lines.keys() 7 | courses.sort(lines[a] > lines[b]) | ~~~~~~~~~~~~~~~~ 8 | println(courses) 9 | }e04_sort_capture.v:7:10: error: `.sort()` can only use `a` or `b` as argument, e.g. `arr.sort(a < b)` 5 | } 6 | mut courses := lines.keys() 7 | courses.sort(lines[a] > lines[b]) | ~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | println(courses) 9 | }e04_sort_capture.v:7:20: error: invalid key: expected `string`, not `&string`; did you mean `lines[*a]`? 5 | } 6 | mut courses := lines.keys() 7 | courses.sort(lines[a] > lines[b]) | ~~~ 8 | println(courses) 9 | }e04_sort_capture.v:7:31: error: invalid key: expected `string`, not `&string`; did you mean `lines[*b]`? 5 | } 6 | mut courses := lines.keys() 7 | courses.sort(lines[a] > lines[b]) | ~~~ 8 | println(courses) 9 | }Los errores tercero y cuarto muestran cómo funciona sort: a y b son referencias (&string) a los elementos. sort_with_compare recibe en cambio una función de comparación; la sección sobre los maps más abajo ordena un array de structs por un campo.
counts := [474, 341, 326, 394, 120, 88]first := counts[..2]println('${first} ${counts[4..]} ${counts#[-2..]}')[474, 341] [120, 88] [120, 88]a[start..end] excluye end, como los rangos de C# a[..2]. Un índice negativo cuenta desde el final, pero solo con #[…]: counts#[-2..] es counts[^2..] en C#. En V, un slice es un array corriente ([]int), no un tipo aparte como Span<int> o una vista subList.
Que un slice comparta la memoria de su array depende de la mutabilidad (array slices):
- Si el array y el slice son ambos inmutables, el slice comparte la memoria del array: ninguno de los dos puede modificarla.
- Si alguno de los dos es
mut, V clona el slice, y lo indica con un aviso (notice):
w04_implicit_clone.v:3:16: notice: an implicit clone of the slice was done here 1 | fn main() { 2 | mut lines := [474, 341, 326] 3 | first := lines[..2] | ~~~~~ 4 | lines[0] = 0 5 | println('${lines} ${first}')Details: w04_implicit_clone.v:3:16: details: To silence this notice, use either an explicit `a[..].clone()`,or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice. 1 | fn main() { 2 | mut lines := [474, 341, 326] 3 | first := lines[..2] | ~~~~~ 4 | lines[0] = 0 5 | println('${lines} ${first}')[0, 341, 326] [474, 341]first conservó [474, 341] después de lines[0] = 0: es una copia.
Lo que comparte una asignación
Sección titulada «Lo que comparte una asignación»Un struct se copia en la asignación (lección 2). Un array no. El checker en assign.v, líneas 845-855 solo clona los slices (a[..]); un array entero asignado a otra variable comparte su memoria:
mut original := [1, 2, 3]alias := originaloriginal[0] = 100println('original ${original}, alias ${alias}')println('cap ${original.cap}')original << 4original[1] = 200println('original ${original}, alias ${alias}')
mut copy := original.clone()copy[0] = -1println('original ${original}, copy ${copy}')original [100, 2, 3], alias [100, 2, 3]cap 3original [100, 200, 3, 4], alias [100, 2, 3]original [100, 200, 3, 4], copy [-1, 200, 3, 4]alias := originalcomparte la memoria: elaliasinmutable veoriginal[0] = 100, sin ningún aviso.original << 4supera la capacidad de 3: V mueveoriginala un bloque más grande.aliassigue apuntando al antiguo, y ya no veoriginal[1] = 200.clone()hace una copia independiente, comonew List<int>(list)onew ArrayList<>(list).
Una List<int> de C# o un ArrayList de Java asignado a otra variable es el mismo objeto para siempre; un array de V se comparte hasta que crece, como un slice en Go. Aunque sea inmutable, alias no protege nada: el array que lee puede seguir cambiando a través de original.
V rechaza el sentido contrario, una variable mutable que compartiría un array inmutable:
fn main() { lines := [474, 341, 326] mut copy := lines copy[0] = 0 println(copy)}e04_mut_alias.v:3:14: notice: left-side of assignment expects a mutable reference, but variable `lines` is immutable, declare it with `mut` to make it mutable or clone it 1 | fn main() { 2 | lines := [474, 341, 326] 3 | mut copy := lines | ~~~~~ 4 | copy[0] = 0 5 | println(copy)e04_mut_alias.v:3:11: error: use `mut array2 := array1.clone()` instead of `mut array2 := array1` (or use `unsafe`) 1 | fn main() { 2 | lines := [474, 341, 326] 3 | mut copy := lines | ~~ 4 | copy[0] = 0 5 | println(copy)e04_mut_alias.v:4:2: error: `copy` aliases mutable data from an immutable value, clone it first (or use `unsafe`) 2 | lines := [474, 341, 326] 3 | mut copy := lines 4 | copy[0] = 0 | ~~~~ 5 | println(copy) 6 | }data/pages.csv tiene una línea por página de este sitio. encoding.csv lo lee (lección 3), y dos maps cuentan las páginas por idioma y las líneas en inglés por curso:
mut reader := csv.new_reader(os.read_file('data/pages.csv')!)reader.read()! // la cabecera: url,locale,course,title,linesmut pages_per_locale := map[string]int{}mut lines_per_course := map[string]int{}for { row := reader.read() or { break } pages_per_locale[row[1]]++ if row[1] == 'en' { lines_per_course[row[2]] += row[4].int() }}println(pages_per_locale){'en': 117, 'es': 101, 'fr': 101}pages_per_locale[row[1]]++ funciona la primera vez que aparece un idioma: una clave ausente se lee como el valor cero del tipo. Esa es la principal diferencia con C# y Java:
println(pages_per_locale['de'])println('de' in pages_per_locale)println(pages_per_locale['de'] or { -1 })if n := pages_per_locale['fr'] { println('fr: ${n} pages')}0false-1fr: 101 pagesC# Dictionary |
Java HashMap |
V map |
|
|---|---|---|---|
| Tipo | Dictionary<string, int> |
HashMap<String, Integer> |
map[string]int |
Clave ausente, m[k] |
KeyNotFoundException |
get devuelve null |
el valor cero, 0 |
| Clave ausente, explícito | TryGetValue, GetValueOrDefault |
getOrDefault |
m[k] or { … }, if v := m[k] { } |
| Contiene la clave | ContainsKey(k) |
containsKey(k) |
k in m |
| Eliminar | Remove(k) |
remove(k) |
m.delete(k) |
| Orden de iteración | indefinido | indefinido (LinkedHashMap: inserción) |
inserción |
Fuentes: V maps, Dictionary<TKey,TValue>, HashMap.
El valor cero oculta las erratas: m['fr '] devuelve 0 sin decir nada. Un módulo puede renunciar a él con @[strict_map_index], que convierte en error cada índice sin or { }:
@[strict_map_index]module main
fn main() { pages := { 'en': 117 } println(pages['de'])}e04_strict_map_index.v:8:15: error: `@[strict_map_index]` requires handling missing map keys with `or {}` or `if value := map[key] {}` 6 | 'en': 117 7 | } 8 | println(pages['de']) | ~~~~~~ 9 | }Para ordenar las entradas, el ejemplo las copia en un array de structs, ya que una expresión de ordenación solo ve a y b:
struct CourseLines { course string lines int}
mut totals := []CourseLines{}for course, lines in lines_per_course { totals << CourseLines{course, lines}}totals.sort(a.lines > b.lines)println('English lines per course:')for t in totals[..5] { println(' ${t.course:-22} ${t.lines:5}')}English lines per course: streeling 9225 java-for-csharp 6251 rust-for-csharp-java 5652 duckdb 3177 github-actions 2785${t.course:-22} rellena hasta 22 caracteres por la derecha, ${t.lines:5} hasta 5 por la izquierda, como {t.Course,-22} y {t.Lines,5} en C#.
A diferencia de un array, un map no se puede asignar a otra variable, sea mutable o no:
fn main() { mut status := { 'ladybugdb': 'lesson 8' } next := status status['ladybugdb'] = 'done' println(next)}e04_map_copy.v:5:10: error: cannot copy map: call `move` or `clone` method (or use a reference) 3 | 'ladybugdb': 'lesson 8' 4 | } 5 | next := status | ~~~~~~ 6 | status['ladybugdb'] = 'done' 7 | println(next)clone() hace explícita la copia:
mut status := { 'duckdb': 'done' 'ladybugdb': 'lesson 8' 'v-for-csharp-java': 'lesson 4'}status.delete('duckdb')mut next := status.clone()next['v-for-csharp-java'] = 'lesson 5'println(status)println(next){'ladybugdb': 'lesson 8', 'v-for-csharp-java': 'lesson 4'}{'ladybugdb': 'lesson 8', 'v-for-csharp-java': 'lesson 5'}Memoria
Sección titulada «Memoria»La pila, el montón (heap) y las referencias
Sección titulada «La pila, el montón (heap) y las referencias»V pone los structs en la pila cuando puede, y en el montón cuando su dirección escapa (stack and heap). Los arrays y los maps guardan sus elementos en el montón. No eliges con una palabra clave como class y struct en C#: el compilador decide según lo que hace el código con la dirección.
struct Page { title string}
fn newest() &Page { p := Page{'Journal'} return &p}&Page es una referencia a un Page, y &p toma la dirección de p. Devolverla está permitido: V ve que la dirección sale de newest y asigna p en el montón. &Page{…} asigna directamente en el montón, como new en C# y Java:
lesson := &Page{'4. Arrays, maps, slices and memory'}println(typeof(lesson).name)println(lesson.title)&Page4. Arrays, maps, slices and memoryDentro de una función que recibe una referencia, el compilador no puede saber dónde vive el struct. Se niega a almacenar la referencia, porque el struct puede estar en la pila del llamador:
struct Page { title string}
struct History {mut: last &Page = unsafe { nil }}
fn (mut h History) visit(p &Page) { h.last = p}e04_stack_reference.v:11:11: error: `p` cannot be assigned outside `unsafe` blocks as it might refer to an object stored on stack. Consider declaring `Page` as `@[heap]`. 9 | 10 | fn (mut h History) visit(p &Page) { 11 | h.last = p | ^ 12 | } 13 |Como dice el mensaje, @[heap] sobre el struct asigna cada instancia en el montón, y el mismo código compila (l04_references.v, líneas 13-26). Una referencia nula se escribe unsafe { nil }: fuera de unsafe, una referencia no puede ser nula.
Por defecto: un recolector de basura
Sección titulada «Por defecto: un recolector de basura»La documentación enumera cuatro formas de gestionar la memoria:
| Modo | Opción | Qué libera la memoria |
|---|---|---|
| Recolector de basura (GC) | el comportamiento por defecto | el recolector conservador Boehm-Demers-Weiser, incluido en thirdparty/libgc |
| Autofree | -autofree |
llamadas a free() insertadas por el compilador; la documentación lo describe como un trabajo en curso y lo desaconseja |
| Manual | -gc none |
nada: llamas a free() en código unsafe, o hay fugas |
| Arena | -prealloc |
un asignador de arena, para programas de corta duración, de un solo hilo, de tipo lote |
l04_memory.v construye 50 arrays de 100,000 cadenas, uno tras otro, y no conserva ninguno. runtime.used_memory da la memoria residente del proceso, en los tres SO:
for round in 1 .. 51 { lines := make_lines(100_000) total += lines.len if round % 25 == 0 { println('round ${round}: ${runtime.used_memory()! / 1024 / 1024} MB resident') }}Memoria residente después de la ronda 50, tal como la imprime la CI del curso y en mi máquina:
| Compilación | Windows (mi máquina) | Runner Windows | Runner Linux | Runner macOS |
|---|---|---|---|---|
v run (GC) |
20 MB | 20 MB | 18 MB | 36 MB |
v -gc none run |
540 MB | 540 MB | 383 MB | 384 MB |
v -autofree run |
10 MB | 9 MB | 6 MB | 6 MB |
v -prod run |
542 MB (MSVC) | |||
v -prod -gc boehm run |
30 MB (MSVC) |
- Con el GC, la memoria se mantiene estable, como en un programa .NET o Java: los arrays de las rondas anteriores se recolectan.
- Con
-gc none, no se libera nada: la memoria crece de 8 a 11 MB por ronda. -autofreelibera cada array al final de su ronda, y es el que menos memoria usa aquí. También desactiva el GC:-autofreefijagc_mode = .no_gcenpref.v, líneas 807-811, así que todo lo que autofree no libera se pierde en fugas, cuando la documentación dice que el GC lo libera.- En Windows, una compilación
-prodcompilada por MSVC desactiva el GC sin avisar:default.v, líneas 369-381 cambia ano_gccuando el compilador C es MSVC y no se da ninguna opción-gc. El mismo programa que se queda en 20 MB en desarrollo crece hasta 542 MB en producción.-gc boehmrecupera el GC.
gc_is_enabled() indica en qué caso estás: el ejemplo lo imprime en su primera línea, GC enabled: false con -gc none, -autofree, y -prod compilado por MSVC.
Puntos clave
Sección titulada «Puntos clave»[]Tes un array que crece con<<,filter,map,sort; los métodos funcionales no son perezosos, y una expresión de ordenación solo veayb.- Un slice de un array inmutable comparte su memoria; con
muten cualquiera de los dos lados, V lo clona e imprime un aviso. - Asignar un array entero lo comparte hasta que crece más allá de su capacidad;
clone()hace una copia real. Un map no se puede asignar sinclone(). - Una clave de map ausente devuelve el valor cero; usa
or { },if v := m[k],in, o@[strict_map_index]. - El compilador elige la pila o el montón;
&T{}y@[heap]fuerzan el montón. - El GC es el comportamiento por defecto,
-gc nonepierde en fugas lo que no liberas, y-autofreedesactiva el GC. En Windows,-prodcon MSVC también lo desactiva.
Ejercicios
Sección titulada «Ejercicios»- Cuenta las lecciones en francés de cada curso en
data/pages.csv(los títulos que empiezan por un dígito), e imprime los cursos ordenados por nombre.
Solución
examples/s04_lessons_per_course.v:
mut reader := csv.new_reader(os.read_file('data/pages.csv')!)reader.read()!mut lessons := map[string]int{}for { row := reader.read() or { break } if row[1] == 'fr' && row[3].len > 0 && row[3][0].is_digit() { lessons[row[2]]++ }}mut courses := lessons.keys()courses.sort()for course in courses { println('${course}: ${lessons[course]}')}duckdb: 8github-actions: 10java-for-csharp: 12rust-for-csharp-java: 15wsl-containers: 5El map conserva el orden en que los cursos aparecen por primera vez en el archivo; keys() y sort() dan el orden alfabético. Comprobé los recuentos con awk sobre el mismo archivo.
- ¿Qué imprime esto?
mut a := [1, 2, 3]b := aa << 4a[0] = 9println('a ${a}, b ${b}')Solución
a [9, 2, 3, 4], b [1, 2, 3]b := a comparte el array, pero a << 4 supera su capacidad de 3 y mueve a a un nuevo bloque antes de a[0] = 9. Intercambia las dos líneas, a[0] = 9 y luego a << 4, y b imprime [9, 2, 3].
- Un servicio V de larga duración se compila con
v -proden una máquina Windows con las Visual Studio Build Tools, y su memoria crece sin parar. ¿Qué compruebas primero?
Solución
Si el GC está activado: imprime gc_is_enabled(), o compila con v -showcc -prod y busca el cl.exe de MSVC. Con MSVC y sin opción -gc, V 0.5.2 compila sin GC, y cada asignación de memoria se pierde en fugas. Compila con -prod -gc boehm, o con otro compilador C (-cc gcc, por verificar en una máquina que tenga ambos).
Fuentes
Sección titulada «Fuentes»- Documentación de V — Arrays, Maps, Memory management, References
- Biblioteca estándar de V —
arrayymap,runtime,encoding.csv - C# —
List<T>,Dictionary<TKey,TValue>, Fundamentos de la recolección de elementos no utilizados - Java —
ArrayList,HashMap, Garbage collection tuning - Recolector de basura Boehm-Demers-Weiser