Skip to content

Visual Basic Hoja de referencia

Lenguaje simple dirigido por eventos para apps de Windows.

01

Variables, tipos y operadores

Declaración de variables y tipos integrados

VB.NET usa tipado estático con declaraciones Dim. Tipos comunes: String, Integer (32-bit), Long (64-bit), Double (64-bit float), Decimal (alta precisión para financiero), Boolean, Date, Char. Use el sufijo de tipo (D para Decimal, F para Single, L para Long) para forzar tipos literales. Option Explicit On fuerza la declaración de variables (previene errores tipográficos). Option Infer On habilita la inferencia de tipos con Dim sin tipos explícitos. Los tipos Nullable (Integer?) envuelven tipos de valor para que puedan contener Nothing — útil para bases de datos y APIs.

vb
' Option Explicit On  -- requires declaration (recommended)
Module Variables
    Sub Main()
        Dim name As String = "Alice"          ' text
        Dim age As Integer = 30                ' 32-bit integer
        Dim salary As Decimal = 50000.50D      ' precise money
        Dim pi As Double = 3.14159             ' 64-bit float
        Dim isDev As Boolean = True            ' True/False
        Dim letter As Char = "A"c              ' single character
        Dim today As Date = #2024-06-18#       ' date literal

        ' type inference (Option Infer On)
        Dim count = 10          ' inferred as Integer
        Dim message = "Hello"   ' inferred as String

        ' nullable types (Value types that can be Nothing)
        Dim score As Integer? = Nothing
        If score.HasValue Then Console.WriteLine(score.Value)

        ' varType() returns the Type object
        Console.WriteLine(name.GetType().Name)  ' String
    End Sub
End Module

Constantes, Enums y Structures

Const declara constantes en tiempo de compilación (implícitamente Shared, no puede cambiar). Enum define constantes enteras con nombre — use [Enum].Parse para convertir cadenas y CInt para obtener el valor numérico. La notación de corchetes [Error] escapa palabras clave reservadas. El atributo <Flags> marca un enum como bit field, permitiendo combinación con operadores Or/And — común para permisos y opciones. Siempre especifique el tipo subyacente (As Integer) para flags para controlar el ancho de bits.

vb
' constants (compile-time, implicitly Shared)
Public Const Pi As Double = 3.14159265358979
Public Const MaxRetries As Integer = 3

' enum: named integer constants
Public Enum LogLevel
    Debug = 0
    Info = 1
    Warning = 2
    [Error] = 3     ' brackets for reserved words
End Enum

Module EnumsDemo
    Sub Main()
        Dim level As LogLevel = LogLevel.Warning
        Console.WriteLine(level)         ' Warning
        Console.WriteLine(CInt(level))   ' 2

        ' parse string to enum
        Dim parsed = [Enum].Parse(GetType(LogLevel), "Info")
        Console.WriteLine(parsed)        ' Info

        ' Flags attribute for bit-field enums
        Dim perms As FilePermission = FilePermission.Read Or FilePermission.Write
        If (perms And FilePermission.Read) <> 0 Then
            Console.WriteLine("Has read")
        End If
    End Sub
End Module

<Flags>
Public Enum FilePermission As Integer
    None = 0
    Read = 1
    Write = 2
    Execute = 4
End Enum

Conversión de tipos y casting

Las conversiones de ampliación (Integer a Double) son implícitas y seguras. Las conversiones de reducción (Double a Integer) requieren casting explícito. CInt/CStr/CDate/CDec son las funciones de conversión de VB (CType es la versión genérica). DirectCast es el más estricto — solo funciona si el tipo en tiempo de ejecución coincide exactamente. TryCast devuelve Nothing en caso de fallo en lugar de lanzar (solo tipos de referencia). Siempre prefiera TryParse sobre Parse para entrada de usuario para evitar excepciones. IsNumeric/IsDate son helpers de validación útiles. CInt redondea (redondeo bancario) mientras que Int() trunca.

vb
' widening conversions (implicit, safe)
Dim i As Integer = 42
Dim d As Double = i        ' Integer -> Double (no data loss)

' narrowing conversions (explicit, may lose data)
Dim d2 As Double = 3.99
Dim i2 As Integer = CInt(d2)       ' 4 (rounds, not truncates)
Dim i3 As Integer = Int(d2)        ' 3 (floor)
Dim s As String = CStr(42)         ' "42"
Dim n As Integer = CInt("100")     ' 100 (throws if invalid)

' DirectCast vs CType vs TryCast
Dim obj As Object = "Hello"
Dim s2 As String = DirectCast(obj, String)  ' strict, same type only
Dim s3 As String = CType(obj, String)       ' flexible, converts

' TryCast: returns Nothing if cast fails (reference types only)
Dim s4 As String = TryCast(obj, String)
If s4 IsNot Nothing Then Console.WriteLine(s4)

' conversion functions
Dim b As Boolean = CBool(1)         ' True
Dim dt As Date = CDate("2024-06-18")
Dim dec As Decimal = CDec("99.99")

' IsNumeric, IsDate checks
If IsNumeric("42") Then Console.WriteLine("is number")
If IsDate("2024-06-18") Then Console.WriteLine("is date")

' Parse vs TryParse (safer)
Dim num As Integer
If Integer.TryParse("123", num) Then
    Console.WriteLine(num)   ' 123
End If

Operadores y expresiones

VB usa \ para división entera y / para división real (a diferencia de C# que usa / y %). Mod es el operador de resto. VB usa = tanto para asignación como para comparación de igualdad (el contexto determina cuál). And/Or evalúan ambos operandos; AndAlso/OrElse son de cortocircuito (preferidos por rendimiento y para evitar referencias nulas). & es el operador de concatenación de cadenas (no +, que puede hacer adición numérica). Use AndAlso/OrElse por defecto para cortocircuitar y evitar errores como comprobar objetos Nothing.

vb
' arithmetic
Dim a As Integer = 10
Console.WriteLine(a + 3)    ' 13
Console.WriteLine(a - 4)    ' 6
Console.WriteLine(a * 2)    ' 20
Console.WriteLine(a / 3)    ' 3.333 (Double, real division)
Console.WriteLine(a \ 3)   ' 3 (integer division)
Console.WriteLine(a Mod 3)  ' 1 (remainder)
Console.WriteLine(a ^ 2)    ' 100 (exponent)

' comparison (return Boolean)
Console.WriteLine(5 > 3)        ' True
Console.WriteLine(5 = 5)        ' True (VB uses = for equality)
Console.WriteLine(5 <> 4)       ' True (not equal)
Console.WriteLine("a" < "b")    ' True (string comparison)

' logical operators
Dim x As Boolean = True
Dim y As Boolean = False
Console.WriteLine(x And y)      ' False (evaluates both)
Console.WriteLine(x Or y)       ' True
Console.WriteLine(Not x)        ' False
Console.WriteLine(x Xor y)      ' True (exclusive or)
Console.WriteLine(x AndAlso y)  ' False (short-circuit)
Console.WriteLine(x OrElse y)   ' True (short-circuit)

' bitwise (on integers)
Console.WriteLine(5 And 3)      ' 1 (0101 And 0011 = 0001)
Console.WriteLine(5 Or 3)       ' 7
Console.WriteLine(5 Xor 3)      ' 6

' string concatenation
Dim s As String = "Hello" & " " & "World"   ' & operator
Dim s2 As String = $"Hello {a}"             ' interpolation

Entrada, salida y formato

Console.WriteLine/Write son los métodos básicos de salida. La interpolación de cadenas ($"...") es la forma moderna y legible de incrustar expresiones — soporta especificadores de formato como :F2 (2 decimales), :C (moneda), :P (porcentaje), :X (hex). String.Format usa placeholders posicionales {0}, {1} con alineación opcional ({0,-10} = alineado a la izquierda, ancho 10). Para bucles que construyen cadenas grandes, use StringBuilder (no &) para evitar crear muchas cadenas intermedias. Integer.Parse lanza con entrada inválida; use TryParse para seguridad.

vb
Module IO
    Sub Main()
        ' console output
        Console.WriteLine("Hello, World!")    ' with newline
        Console.Write("No newline")           ' without newline
        Console.WriteLine()                   ' blank line

        ' string interpolation ($ before the string)
        Dim name As String = "Alice"
        Dim age As Integer = 30
        Console.WriteLine($"Hello, {name}. You are {age} years old.")
        Console.WriteLine($"Next year: {age + 1}")     ' expressions work
        Console.WriteLine($"Pi: {3.14159:F2}")         ' format specifier

        ' String.Format (positional)
        Dim msg As String = String.Format("{0} is {1} years old", name, age)

        ' composite formatting with alignment
        Console.WriteLine("{0,-10} {1,5}", "Alice", 30)   ' left/right align
        Console.WriteLine("{0:C}", 1234.56)   ' currency: $1,234.56
        Console.WriteLine("{0:P}", 0.25)      ' percent: 25.00 %
        Console.WriteLine("{0:X}", 255)       ' hex: FF

        ' console input
        Console.Write("Enter name: ")
        Dim input As String = Console.ReadLine()
        Console.Write("Enter age: ")
        Dim ageInput As Integer = Integer.Parse(Console.ReadLine())

        ' StringBuilder for efficient concatenation
        Dim sb As New System.Text.StringBuilder()
        For i As Integer = 1 To 100
            sb.Append($"Line {i}").AppendLine()
        Next
        Console.WriteLine(sb.ToString())
    End Sub
End Module
02

Flujo de control

If...Then...Else y ternario

If...Then...ElseIf...Else es el condicional de VB. AndAlso/OrElse son de cortocircuito (no evalúan el lado derecho si es innecesario) — siempre prefiera estos sobre And/Or para condiciones. La función If() (VB 14+) es el operador ternario: If(condition, trueVal, falseVal). If() con dos argumentos es el operador de coalescencia nula: If(maybeNull, defaultValue). Evite la vieja función IIf() — siempre evalúa ambas ramas y devuelve Object (boxing). El If de una sola línea no necesita End If.

vb
Dim score As Integer = 85

' multi-line If
If score >= 90 Then
    Console.WriteLine("A")
ElseIf score >= 80 Then
    Console.WriteLine("B")
ElseIf score >= 70 Then
    Console.WriteLine("C")
Else
    Console.WriteLine("F")
End If

' single-line If (no End If)
If score > 50 Then Console.WriteLine("Pass")

' nested If with AndAlso (short-circuit)
Dim age As Integer = 25
Dim hasLicense As Boolean = True
If age >= 18 AndAlso hasLicense Then
    Console.WriteLine("Can drive")
End If

' If as expression (VB 14+ ternary)
Dim grade As String = If(score >= 60, "Pass", "Fail")

' nullable coalescing
Dim name As String = Nothing
Dim displayName As String = If(name, "Unknown")  ' "Unknown"

' IIf function (old-style, always evaluates both sides)
Dim result As String = IIf(score > 50, "Pass", "Fail")

Select Case (Switch)

Select Case es la sentencia switch de VB — más limpia que If...ElseIf encadenado. Case soporta múltiples valores (separados por comas), rangos (To) y operadores de comparación (Is >= 90). Case Else es la rama por defecto. A diferencia del switch de C#, VB no cae — cada Case se ejecuta independientemente y no se necesita Break. Select Case funciona con cadenas, números e incluso objetos (usando Is). Es ideal para despachar sobre valores discretos.

vb
Dim grade As String = "B"

' basic Select Case
Select Case grade
    Case "A"
        Console.WriteLine("Excellent")
    Case "B", "C"          ' multiple values
        Console.WriteLine("Good")
    Case "D" To "F"        ' range
        Console.WriteLine("Needs improvement")
    Case Else
        Console.WriteLine("Invalid grade")
End Select

' numeric ranges
Dim score As Integer = 85
Select Case score
    Case Is >= 90          ' comparison operator
        Console.WriteLine("A")
    Case 80 To 89          ' range
        Console.WriteLine("B")
    Case 70 To 79
        Console.WriteLine("C")
    Case Is < 70
        Console.WriteLine("F")
End Select

' Case with expressions
Dim day As Integer = 3
Select Case day
    Case 1, 2, 3, 4, 5
        Console.WriteLine("Weekday")
    Case 6, 7
        Console.WriteLine("Weekend")
End Select

For, For Each e iteradores

For...To...Next itera un rango con un Step opcional (negativo para cuenta atrás). For Each itera cualquier IEnumerable — más limpio para colecciones. Exit For rompe el bucle; Continue For salta a la siguiente iteración. Las funciones Iterator (con Yield) producen secuencias perezosamente — los valores se generan bajo demanda, lo que es eficiente en memoria para secuencias grandes o infinitas. Yield devuelve un valor, luego se reanuda donde lo dejó en la siguiente iteración. Este es el equivalente de VB a yield return de C#.

vb
' For loop with step
For i As Integer = 0 To 4
    Console.WriteLine(i)      ' 0 1 2 3 4
Next

For i As Integer = 10 To 1 Step -2
    Console.WriteLine(i)      ' 10 8 6 4 2
Next

' nested loops
For row As Integer = 1 To 3
    For col As Integer = 1 To 3
        Console.Write($"{row * col} ")
    Next
    Console.WriteLine()
Next

' For Each (iterates collections)
Dim fruits() As String = {"apple", "banana", "cherry"}
For Each fruit As String In fruits
    Console.WriteLine(fruit)
Next

' For Each with List
Dim nums As New List(Of Integer) From {1, 2, 3}
For Each n As Integer In nums
    Console.WriteLine(n)
Next

' Exit For / Continue For
For i As Integer = 1 To 100
    If i > 5 Then Exit For         ' break
    If i Mod 2 = 0 Then Continue For ' skip even
    Console.WriteLine(i)            ' 1 3 5
Next

' Iterator function (yields one item at a time)
Iterator Function Evens(max As Integer) As IEnumerable(Of Integer)
    For i As Integer = 2 To max Step 2
        Yield i
    Next
End Function

For Each e In Evens(10)
    Console.WriteLine(e)   ' 2 4 6 8 10
Next

Do While, Do Until y While

Do While se ejecuta mientras la condición sea True; Do Until se ejecuta mientras la condición sea False (hasta que se vuelva True). Colocar la condición al final (Loop While/Until) garantiza que el cuerpo se ejecute al menos una vez — útil para menús y validación de entrada. While...End While es la forma antigua y más simple. Exit Do rompe; Continue Do salta a la siguiente iteración. Elija Do While cuando podría no entrar al bucle; Do...Loop While cuando debe ejecutar al menos una vez.

vb
' Do While: test at top (may never run)
Dim count As Integer = 0
Do While count < 3
    Console.WriteLine(count)   ' 0 1 2
    count += 1
Loop

' Do Until: runs UNTIL condition is true
count = 0
Do Until count >= 3
    Console.WriteLine(count)   ' 0 1 2
    count += 1
Loop

' Do...Loop While: test at bottom (runs at least once)
count = 0
Do
    Console.WriteLine(count)
    count += 1
Loop While count < 3

' Do...Loop Until: test at bottom, runs until true
count = 0
Do
    Console.WriteLine(count)
    count += 1
Loop Until count >= 3

' While...End While (older syntax)
Dim i As Integer = 0
While i < 3
    Console.WriteLine(i)
    i += 1
End While

' Exit Do / Continue Do
Dim n As Integer = 0
Do While n < 100
    n += 1
    If n Mod 2 = 0 Then Continue Do   ' skip even
    If n > 10 Then Exit Do            ' break
    Console.WriteLine(n)              ' 1 3 5 7 9
Loop

With...End With y GoTo

With...End With es azúcar sintáctico para acceder a múltiples miembros de un objeto sin repetir el nombre de la variable — útil para inicialización y builders. GoTo salta a una etiqueta; el VB moderno lo desaconseja excepto para salir de bucles profundamente anidados o en el manejo de errores heredado On Error GoTo. On Error GoTo es manejo de errores estilo VB6 (Resume Next salta la línea que causa error) — prefiera Try/Catch estructurado en código nuevo. El objeto Err contiene la descripción y número del último error.

vb
' With...End With: access members of one object repeatedly
Dim sb As New System.Text.StringBuilder()
With sb
    .Append("Hello")
    .Append(", ")
    .Append("World")
    .AppendLine()
End With
Console.WriteLine(sb.ToString())

' With on anonymous types
Dim person = New With {.Name = "Alice", .Age = 30}
With person
    Console.WriteLine($"{.Name} is {.Age}")
End With

' nested With (use caution — confusing)
With New System.Drawing.Point(3, 4)
    Console.WriteLine($"X={.X}, Y={.Y}")
End With

' GoTo (use sparingly — only for error handling or deep breaks)
For i As Integer = 1 To 3
    For j As Integer = 1 To 3
        If i + j > 4 Then GoTo done   ' break out of both loops
        Console.WriteLine($"{i},{j}")
    Next
Next
done:
Console.WriteLine("Finished")

' On Error GoTo (legacy error handling — prefer Try/Catch)
On Error GoTo errorHandler
Dim result As Integer = 10 \ 0
Exit Sub
errorHandler:
Console.WriteLine("Error: " & Err.Description)
Resume Next
03

Procedimientos, funciones y eventos

Fundamentos de Sub y Function

Sub realiza una acción (sin valor de retorno); Function devuelve un valor. Return sale de la función con un valor. El estilo antiguo de 'asignar al nombre de la función' (Multiply = a * b) todavía funciona pero Return es más claro. Al llamar a un Sub, los paréntesis son opcionales (Greet "Alice" o Greet("Alice")). Las funciones llamadas por su valor de retorno requieren paréntesis. Por defecto, VB le permite llamar funciones sin asignar el valor de retorno, pero esto puede ser confuso — sea explícito.

vb
' Sub: performs an action, returns nothing
Sub Greet(name As String)
    Console.WriteLine($"Hello, {name}!")
End Sub

' Function: returns a value
Function Add(a As Integer, b As Integer) As Integer
    Return a + b          ' explicit return
End Function

' Function without Return: use function name
Function Multiply(a As Integer, b As Integer) As Integer
    Multiply = a * b      ' old-style: assign to function name
End Function

' calling
Greet("Alice")
Dim sum As Integer = Add(3, 4)
Console.WriteLine(sum)       ' 7

' parentheses are optional for Subs with no return value
Greet "Alice"                ' valid (no parens)

' passing arguments by position
Console.WriteLine(Add(1, 2))

' Sub with multiple statements
Sub PrintReport(title As String, lines As Integer)
    Console.WriteLine(New String("-"c, 40))
    Console.WriteLine(title)
    Console.WriteLine(New String("-"c, 40))
    For i As Integer = 1 To lines
        Console.WriteLine($"Line {i}")
    Next
End Sub

PrintReport("Monthly Report", 5)

Parámetros: ByVal, ByRef, Optional, ParamArray

ByVal (por defecto) pasa una copia — los cambios dentro no afectan al llamador. ByRef pasa una referencia — los cambios SÍ afectan al llamador (como ref en C#). Use ByRef cuando un procedimiento debe modificar la variable del llamador o devolver múltiples valores. Los parámetros Optional tienen valores por defecto y deben ir después de los requeridos. ParamArray acepta un número variable de argumentos (como params en C#) y debe ser el último parámetro. Los argumentos con nombre (name:=value) mejoran la legibilidad y permiten saltar parámetros opcionales.

vb
' ByVal (default): pass by value (copy)
Sub ResetValue(ByVal x As Integer)
    x = 0   ' only changes the copy
End Sub

' ByRef: pass by reference (can modify caller's variable)
Sub Increment(ByRef x As Integer)
    x += 1   ' modifies the original
End Sub

Dim n As Integer = 5
ResetValue(n)
Console.WriteLine(n)   ' 5 (unchanged)
Increment(n)
Console.WriteLine(n)   ' 6 (modified)

' Optional parameters (must have default values, must be last)
Function Power(base As Double, Optional exp As Double = 2) As Double
    Return Math.Pow(base, exp)
End Function

Console.WriteLine(Power(3))      ' 9 (exp defaults to 2)
Console.WriteLine(Power(2, 10))  ' 1024

' ParamArray: variable number of arguments (must be last)
Function Sum(ParamArray nums() As Integer) As Integer
    Dim total As Integer = 0
    For Each n In nums
        total += n
    Next
    Return total
End Function

Console.WriteLine(Sum(1, 2, 3))         ' 6
Console.WriteLine(Sum(1, 2, 3, 4, 5))   ' 15
Console.WriteLine(Sum())                 ' 0

' named arguments (improve readability)
Sub CreateAccount(name As String, age As Integer, active As Boolean)
    ' ...
End Sub
CreateAccount(name:="Bob", active:=True, age:=25)

Expresiones Lambda y Delegates

Las lambdas son funciones inline: Function(...) para las que devuelven un valor, Sub(...) para las que no. Func(Of T, TResult) es un tipo delegate integrado para funciones; Action(Of T) es para subs (sin retorno). AddressOf crea un delegate desde un método con nombre. Las lambdas son esenciales para LINQ (Where, Select, OrderBy toman argumentos de función). Las lambdas multilínea usan Function...End Function (o Sub...End Sub). Los delegates son punteros a función type-safe — útiles para callbacks, eventos y patrones strategy.

vb
' single-line lambda (Function returns a value)
Dim square As Func(Of Integer, Integer) = Function(x) x * x
Console.WriteLine(square(5))   ' 25

' multi-line lambda
Dim factorial As Func(Of Integer, Integer) = Function(n)
    Dim result As Integer = 1
    For i As Integer = 2 To n
        result *= i
    Next
    Return result
End Function
Console.WriteLine(factorial(5))   ' 120

' Sub lambda (no return value)
Dim log As Action(Of String) = Sub(msg) Console.WriteLine($"[LOG] {msg}")
log("Hello")

' lambda with multiple parameters
Dim add As Func(Of Integer, Integer, Integer) = Function(a, b) a + b
Console.WriteLine(add(3, 4))   ' 7

' use lambdas with LINQ
Dim nums = {1, 2, 3, 4, 5}
Dim evens = nums.Where(Function(n) n Mod 2 = 0).ToList()
Dim doubled = nums.Select(Function(n) n * 2).ToList()
Dim total = nums.Sum(Function(n) n)

' delegate definition
Delegate Function MathOp(a As Integer, b As Integer) As Integer

' assign methods to delegates
Dim op As MathOp
op = AddressOf AddNumbers    ' AddNumbers is a regular Function
Console.WriteLine(op(3, 4))  ' 7

' Action and Func (built-in delegate types)
Dim print As Action(Of String) = AddressOf Console.WriteLine
print("Hello")

Eventos y manejadores de eventos

Los eventos habilitan el patrón observer: una clase lanza eventos, y los suscriptores los manejan. Declare con Event, dispare con RaiseEvent, maneje con Handles (WithEvents, tiempo de compilación) o AddHandler (tiempo de ejecución, dinámico). WithEvents + Handles es declarativo pero la variable debe ser a nivel de módulo/clase. AddHandler/RemoveHandler permiten suscribirse/desuscribirse en tiempo de ejecución — útil para cableado dinámico. Los manejadores de eventos deben coincidir con la firma del evento. Los eventos son la columna vertebral de Windows Forms y WPF (clics de botón, carga de formulario, etc.).

vb
' define an event
Public Class TemperatureSensor
    Public Event TemperatureChanged(temp As Double)

    Private _current As Double

    Public Property Current As Double
        Get
            Return _current
        End Get
        Set(value As Double)
            If value <> _current Then
                _current = value
                RaiseEvent TemperatureChanged(value)   ' fire the event
            End If
        End Set
    End Property
End Class

' subscribe to an event
Module SensorDemo
    WithEvents sensor As New TemperatureSensor()

    Sub Main()
        sensor.Current = 20.5   ' triggers handler
        sensor.Current = 21.0   ' triggers handler
    End Sub

    ' event handler (must match the event signature)
    Sub OnTempChanged(temp As Double) Handles sensor.TemperatureChanged
        Console.WriteLine($"Temperature changed to {temp:F1}°C")
    End Sub
End Module

' alternative: AddHandler (dynamic, runtime)
Module DynamicDemo
    Sub Main()
        Dim s As New TemperatureSensor()
        AddHandler s.TemperatureChanged, AddressOf HandleTemp
        s.Current = 25.0
        RemoveHandler s.TemperatureChanged, AddressOf HandleTemp
    End Sub

    Sub HandleTemp(temp As Double)
        Console.WriteLine($"Got: {temp}")
    End Sub
End Module

Sobrecarga, recursión y miembros Shared

La sobrecarga permite que múltiples métodos compartan un nombre con diferentes listas de parámetros — el compilador elige la mejor coincidencia. La palabra clave Overloads es requerida al sobrecargar a través de herencia. La recursión es cuando una función se llama a sí misma — necesita un caso base para terminar. Vigile el stack overflow con recursión profunda. Los miembros Shared (static de C#) pertenecen al tipo, no a instancias — llámelos via ClassName.Member sin crear un objeto. Los constructores Shared se ejecutan una vez por tipo, ideales para inicializar datos static.

vb
' overloading: same name, different parameters
Class Calculator
    Overloads Function Add(a As Integer, b As Integer) As Integer
        Return a + b
    End Function

    Overloads Function Add(a As Double, b As Double) As Double
        Return a + b
    End Function

    Overloads Function Add(values() As Integer) As Integer
        Dim total As Integer = 0
        For Each v In values
            total += v
        Next
        Return total
    End Function
End Class

' recursion: function calls itself
Function Factorial(n As Integer) As Long
    If n <= 1 Then Return 1
    Return n * Factorial(n - 1)
End Function
Console.WriteLine(Factorial(5))   ' 120

' Fibonacci (inefficient — for demo)
Function Fib(n As Integer) As Integer
    If n < 2 Then Return n
    Return Fib(n - 1) + Fib(n - 2)
End Function

' Shared (static) members: belong to the class, not instances
Class MathHelper
    Public Shared ReadOnly Pi As Double = 3.14159265358979

    Public Shared Function CircleArea(radius As Double) As Double
        Return Pi * radius * radius
    End Function
End Class

' call without creating an instance
Console.WriteLine(MathHelper.Pi)
Console.WriteLine(MathHelper.CircleArea(5))

' Shared constructor (runs once, before any access)
Class Config
    Shared Sub New()
        Console.WriteLine("Config initialized")
    End Sub
End Class
04

Cadenas y procesamiento de texto

Fundamentos de cadenas e inmutabilidad

Las cadenas en VB.NET son inmutables — cada método (ToUpper, Replace, etc.) devuelve una NUEVA cadena; la original no cambia. Use = para comparación de igualdad (específico de VB). IndexOf devuelve el primer índice de una subcadena (-1 si no se encuentra). Split divide una cadena en un array; Join combina un array en una cadena. Trim elimina espacios en blanco (o caracteres especificados) de ambos extremos. Para comparación insensible a mayúsculas, use StringComparison.OrdinalIgnoreCase. Para manipulación intensiva de cadenas en bucles, use StringBuilder.

vb
Dim s As String = "Hello, World"

' properties
Console.WriteLine(s.Length)              ' 12
Console.WriteLine(s(0))                  ' H (indexer, read-only)

' comparison
Console.WriteLine("abc" = "abc")         ' True (= is equality in VB)
Console.WriteLine("abc".Equals("abc"))   ' True
Console.WriteLine(String.Equals("a", "A", StringComparison.OrdinalIgnoreCase))  ' True

' case conversion (returns new string — strings are immutable)
Console.WriteLine(s.ToUpper())           ' HELLO, WORLD
Console.WriteLine(s.ToLower())           ' hello, world

' searching
Console.WriteLine(s.IndexOf("o"))        ' 4 (first occurrence)
Console.WriteLine(s.LastIndexOf("o"))    ' 8
Console.WriteLine(s.Contains("World"))   ' True
Console.WriteLine(s.StartsWith("Hello")) ' True
Console.WriteLine(s.EndsWith("World"))   ' True

' substring
Console.WriteLine(s.Substring(7))        ' World
Console.WriteLine(s.Substring(0, 5))     ' Hello

' split and join
Dim parts() As String = "a,b,c,d".Split(","c)
Console.WriteLine(String.Join("-", parts))   ' a-b-c-d

' replace and trim
Console.WriteLine(s.Replace("o", "0"))   ' Hell0, W0rld
Console.WriteLine("  hi  ".Trim())       ' hi
Console.WriteLine("  hi  ".TrimStart())  ' hi<spaces>
Console.WriteLine("xxhelloxx".Trim("x"c)) ' hello

StringBuilder y rendimiento

String es inmutable — cada & o += crea una nueva cadena, copiando todo el contenido previo. En bucles, esto es O(n²). StringBuilder es mutable — Append/Insert/Remove modifican el mismo buffer, haciéndolo O(n). Siempre use StringBuilder para bucles que construyen cadenas grandes. StringBuilder es encadenable (Append devuelve la misma instancia). Establezca la Capacity inicial si conoce el tamaño aproximado para evitar reasignaciones. Para concatenaciones puntuales (no en bucles), & está bien y es más legible.

vb
Imports System.Text

' StringBuilder: mutable string for efficient concatenation
Dim sb As New StringBuilder()
For i As Integer = 1 To 1000
    sb.Append("Line ").Append(i).AppendLine()   ' chainable
Next
Dim result As String = sb.ToString()

' StringBuilder vs & concatenation
' BAD: creates 1000 intermediate strings
Dim bad As String = ""
For i As Integer = 1 To 1000
    bad &= $"Line {i}"   ' creates new string each time
Next

' GOOD: StringBuilder mutates in place
Dim good As New StringBuilder()
For i As Integer = 1 To 1000
    good.AppendLine($"Line {i}")
Next

' useful StringBuilder methods
Dim sb2 As New StringBuilder("Hello")
sb2.Append(", World")        ' append string
sb2.AppendLine()             ' append newline
sb2.AppendFormat("Count: {0}", 42)  ' formatted append
sb2.Insert(0, ">> ")         ' insert at position
sb2.Remove(0, 3)             ' remove 3 chars at position 0
sb2.Replace("World", "VB")   ' replace substring
Console.WriteLine(sb2.ToString())
Console.WriteLine(sb2.Length)

' pre-allocate capacity for known sizes
Dim sb3 As New StringBuilder(10000)   ' initial capacity

Interpolación de cadenas y formato

La interpolación de cadenas ($"...") es la forma moderna de incrustar variables y expresiones — legible y soporta especificadores de formato tras dos puntos (:C moneda, :F2 decimales fijos, :N separadores de miles, :X hex, :D5 rellenado con ceros, :P porcentaje). Formatos de fecha: yyyy-MM-dd, HH:mm:ss, dddd (nombre completo del día). Use String.Format para placeholders posicionales o cuando la cadena de formato se construye dinámicamente. Para apps conscientes de cultura (internacionalización), pase un CultureInfo a ToString/Format para controlar separadores decimales, símbolos de moneda y formatos de fecha.

vb
Dim name As String = "Alice"
Dim age As Integer = 30
Dim salary As Decimal = 50000.5D
Dim today As Date = Date.Now

' interpolated string ($ prefix)
Console.WriteLine($"Name: {name}, Age: {age}")
Console.WriteLine($"Next year: {age + 1}")          ' expressions
Console.WriteLine($"Upper: {name.ToUpper()}")        ' method calls

' format specifiers in interpolation
Console.WriteLine($"Salary: {salary:C}")             ' $50,000.50
Console.WriteLine($"Salary: {salary:N2}")            ' 50,000.50
Console.WriteLine($"Pi: {Math.PI:F4}")               ' 3.1416
Console.WriteLine($"Date: {today:yyyy-MM-dd}")       ' 2024-06-18
Console.WriteLine($"Time: {today:HH:mm:ss}")         ' 14:30:00
Console.WriteLine($"Hex: {255:X}")                   ' FF
Console.WriteLine($"Padded: {42:D5}")                ' 00042
Console.WriteLine($"Percent: {0.25:P0}")             ' 25%

' alignment in interpolation
Console.WriteLine($"{"Name",-10} | {"Age",5}")       ' left/right align
Console.WriteLine($"{name,-10} | {age,5}")

' String.Format (positional, older style)
Dim msg As String = String.Format("{0} is {1} years old", name, age)

' composite formatting with format string
Console.WriteLine(String.Format("Price: {0:C2}", 19.99))

' custom date formats
Console.WriteLine(today.ToString("dddd, MMMM d, yyyy"))
Console.WriteLine(today.ToString("yyyy/MM/dd HH:mm"))

' culture-specific formatting
Dim ci As Globalization.CultureInfo = New Globalization.CultureInfo("fr-FR")
Console.WriteLine(salary.ToString("C", ci))   ' 50 000,50 €

Expresiones regulares

Regex.IsMatch prueba una coincidencia; Regex.Matches encuentra todas; Regex.Match encuentra la primera. Los grupos capturan partes de la coincidencia con paréntesis — acceda via Groups(1), Groups(2). Regex.Replace sustituye coincidencias (use $1, $2 para referenciar grupos). RegexOptions.Compiled compila el patrón para coincidencia repetida más rápida. Patrones comunes: \d (dígito), \w (carácter de palabra), \s (espacio en blanco), + (uno o más), * (cero o más), {n} (exactamente n), ^/$ (inicio/fin). Siempre valide entrada de usuario con regex para emails, teléfonos, etc.

vb
Imports System.Text.RegularExpressions

' basic matching
Dim pattern As String = "d+"   ' one or more digits
Dim input As String = "abc 123 def 456"

If Regex.IsMatch(input, pattern) Then
    Console.WriteLine("Contains digits")
End If

' extract matches
For Each m As Match In Regex.Matches(input, "d+")
    Console.WriteLine(m.Value)   ' 123, 456
Next

' single match with groups
Dim m2 As Match = Regex.Match("2024-06-18", "(d{4})-(d{2})-(d{2})")
If m2.Success Then
    Console.WriteLine(m2.Groups(1).Value)   ' 2024 (year)
    Console.WriteLine(m2.Groups(2).Value)   ' 06 (month)
    Console.WriteLine(m2.Groups(3).Value)   ' 18 (day)
End If

' replace
Dim cleaned As String = Regex.Replace("phone: 123-456-7890", "D", "")
Console.WriteLine(cleaned)   ' 1234567890 (digits only)

' replace with match reference ($1, $2)
Dim swapped As String = Regex.Replace("John Doe", "(w+) (w+)", "$2, $1")
Console.WriteLine(swapped)   ' Doe, John

' split
Dim parts() As String = Regex.Split("a,b;;c", "[,;]+")

' common patterns
Dim emailPattern As String = "^[w.-]+@[w.-]+.w+$"
Dim urlPattern As String = "^https?://[w./-]+$"
Dim phonePattern As String = "^d{3}-d{3}-d{4}$"

' compiled regex (faster for repeated use)
Dim rx As New Regex("d+", RegexOptions.Compiled)
Dim count As Integer = rx.Matches(input).Count

Operaciones de caracteres y codificación

Char es un único carácter Unicode (2 bytes). Use el sufijo c para literales char ("A"c). Los métodos Char (IsDigit, IsLetter, IsUpper) son útiles para validación. Asc/Chr convierten entre caracteres y códigos ASCII. ToCharArray convierte una cadena en un array de caracteres mutable (las cadenas mismas son inmutables). Encoding.UTF8.GetBytes convierte cadenas a arrays de bytes (esencial para E/S de archivos y networking). Base64 (Convert.ToBase64String) codifica datos binarios como texto — usado en data URIs, adjuntos de email y APIs.

vb
' Char basics
Dim c As Char = "A"c          ' char literal (c suffix)
Console.WriteLine(Char.IsDigit("5"c))      ' True
Console.WriteLine(Char.IsLetter("A"c))     ' True
Console.WriteLine(Char.IsWhiteSpace(" "c)) ' True
Console.WriteLine(Char.ToUpper("a"c))      ' A
Console.WriteLine(Char.ToLower("A"c))      ' a

' convert between char and ASCII code
Dim code As Integer = Asc("A"c)    ' 65
Dim ch As Char = Chr(66)           ' B

' iterate characters in a string
Dim s As String = "Hello"
For Each c In s
    Console.Write($"{Asc(c)} ")    ' 72 101 108 108 111
Next

' char array <-> string
Dim chars() As Char = s.ToCharArray()
Array.Reverse(chars)
Dim reversed As String = New String(chars)
Console.WriteLine(reversed)   ' olleH

' encoding: string <-> bytes
Dim text As String = "Hello"
Dim bytes() As Byte = System.Text.Encoding.UTF8.GetBytes(text)
Console.WriteLine(bytes.Length)   ' 5

Dim decoded As String = System.Text.Encoding.UTF8.GetString(bytes)

' ASCII encoding (1 byte per char, no Unicode)
Dim asciiBytes() As Byte = System.Text.Encoding.ASCII.GetBytes(text)

' Base64 encoding (for binary data in text)
Dim b64 As String = Convert.ToBase64String(bytes)
Dim original() As Byte = Convert.FromBase64String(b64)

' StringBuilder from char array
Dim sb As New System.Text.StringBuilder(New String(chars))
05

Arrays y colecciones

Arrays: declaración, indexación y métodos

Los arrays en VB son indexados base 0 y de tamaño fijo. Dim arr(n) crea n+1 elementos (0 a n). Los literales de array usan { }. Los arrays multidimensionales (,) son rectangulares; los arrays escalonados ()() son arrays de arrays (cada fila puede tener diferente longitud). ReDim redimensiona un array — Preserve mantiene los valores existentes (sin él, los valores se borran). Array.Sort/Reverse/Clear/IndexOf/Copy son helpers estáticos. Use List(Of T) en lugar de arrays cuando el tamaño cambia frecuentemente.

vb
' fixed-size array
Dim nums(4) As Integer          ' 5 elements (0-4), default 0
nums(0) = 10
nums(1) = 20

' array literal
Dim fruits() As String = {"apple", "banana", "cherry"}
Dim nums2() As Integer = {1, 2, 3, 4, 5}

' type inference
Dim mixed = {1, 2, 3}           ' Integer()
Dim names = {"Alice", "Bob"}    ' String()

' indexing (0-based)
Console.WriteLine(fruits(0))         ' apple
Console.WriteLine(fruits.Length)     ' 3
Console.WriteLine(nums2.GetUpperBound(0))  ' 4 (last index)

' multi-dimensional arrays
Dim grid(2, 2) As Integer      ' 3x3
grid(0, 0) = 1
grid(1, 2) = 5

' jagged arrays (array of arrays)
Dim jagged()() As Integer = {
    New Integer() {1, 2},
    New Integer() {3, 4, 5},
    New Integer() {6}
}
Console.WriteLine(jagged(1)(2))   ' 5

' Array class methods
Array.Sort(nums2)                    ' sort in place
Array.Reverse(nums2)                 ' reverse
Array.Clear(nums2, 0, 2)             ' set first 2 to 0
Dim found As Integer = Array.IndexOf(nums2, 3)   ' search
Dim copy(4) As Integer
Array.Copy(nums2, copy, 5)           ' copy

' resize (creates new array)
ReDim Preserve nums2(9)              ' resize, keep existing values
' ReDim without Preserve clears values

' iterate
For Each n As Integer In nums2
    Console.WriteLine(n)
Next

List(Of T): arrays dinámicos

List(Of T) es la colección dinámica por excelencia — crece automáticamente al Add. Count da el número de elementos (no la capacidad). Add/Insert/Remove/RemoveAt/Contains/IndexOf son los métodos core. Find/FindAll/Exists toman un predicado (lambda) para búsquedas personalizadas. Sort puede tomar un delegate Comparison para ordenación personalizada. List(Of T) envuelve un array internamente y lo redimensiona cuando está lleno (Add O(1) amortizado). Convierta a array con ToArray() cuando necesite una colección de tamaño fijo.

vb
Imports System.Collections.Generic

' create and initialize
Dim nums As New List(Of Integer) From {1, 2, 3}
Dim names As New List(Of String)

' add and insert
nums.Add(4)                    ' add to end
nums.AddRange({5, 6, 7})       ' add multiple
nums.Insert(0, 0)              ' insert at index
Console.WriteLine(nums.Count)  ' 8

' access and modify
Console.WriteLine(nums(0))     ' 0 (indexer)
nums(0) = 100                  ' modify

' remove
nums.Remove(3)                 ' remove by value (first match)
nums.RemoveAt(0)               ' remove by index
nums.RemoveRange(0, 2)         ' remove 2 starting at index 0
nums.Clear()                   ' remove all

' search
nums = New List(Of Integer) From {1, 2, 3, 4, 5}
Console.WriteLine(nums.Contains(3))           ' True
Console.WriteLine(nums.IndexOf(3))            ' 2
Dim found As Boolean = nums.Exists(Function(n) n > 3)
Dim first As Integer = nums.Find(Function(n) n > 3)        ' 4
Dim all As List(Of Integer) = nums.FindAll(Function(n) n > 2)  ' 3,4,5

' iterate
For Each n As Integer In nums
    Console.WriteLine(n)
Next

' convert to array
Dim arr() As Integer = nums.ToArray()

' sort and reverse
nums.Sort()
nums.Reverse()

' List with custom objects
Dim people As New List(Of (Name As String, Age As Integer))
people.Add(("Alice", 30))
people.Add(("Bob", 25))
people.Sort(Function(a, b) a.Age.CompareTo(b.Age))   ' sort by age

Dictionary, HashSet y Queue/Stack

Dictionary(Of TKey, TValue) mapea claves a valores — búsqueda O(1) por clave. Use TryGetValue para evitar excepciones y dobles búsquedas (ContainsKey + indexer). Itera con KeyValuePair. HashSet(Of T) almacena elementos únicos con add/contains O(1) — use para deduplicación y operaciones de conjuntos (UnionWith, IntersectWith, ExceptWith). Queue es FIFO (Enqueue/Dequeue); Stack es LIFO (Push/Pop). Todas estas colecciones son genéricas (type-safe, sin boxing). Elija basándose en patrón de acceso: lookup → Dictionary, unicidad → HashSet, orden → Queue/Stack.

vb
Imports System.Collections.Generic

' Dictionary: key-value pairs
Dim ages As New Dictionary(Of String, Integer) From {
    {"Alice", 30},
    {"Bob", 25}
}
ages("Eve") = 28               ' add or update
ages.Add("Carol", 22)          ' add (throws if key exists)

' access
Console.WriteLine(ages("Alice"))           ' 30
Console.WriteLine(ages.ContainsKey("Bob")) ' True
Console.WriteLine(ages.ContainsValue(28))  ' True

' safe access (TryGetValue avoids double lookup)
Dim age As Integer
If ages.TryGetValue("Alice", age) Then
    Console.WriteLine(age)   ' 30
End If

' iterate
For Each kv As KeyValuePair(Of String, Integer) In ages
    Console.WriteLine($"{kv.Key}: {kv.Value}")
Next

' remove
ages.Remove("Bob")

' HashSet: unique elements
Dim unique As New HashSet(Of Integer) From {1, 2, 3}
unique.Add(2)                 ' no effect (already exists)
unique.Add(4)
Console.WriteLine(unique.Count)  ' 4

' set operations
Dim setA As New HashSet(Of Integer) From {1, 2, 3}
Dim setB As New HashSet(Of Integer) From {3, 4, 5}
setA.UnionWith(setB)          ' {1,2,3,4,5}
setA.IntersectWith(setB)      ' {3}
setA.ExceptWith(setB)         ' remove setB from setA

' Queue: FIFO
Dim q As New Queue(Of String)
q.Enqueue("first")
q.Enqueue("second")
Console.WriteLine(q.Dequeue())  ' first
Console.WriteLine(q.Peek())     ' second (without removing)

' Stack: LIFO
Dim stk As New Stack(Of Integer)
stk.Push(1)
stk.Push(2)
Console.WriteLine(stk.Pop())    ' 2
Console.WriteLine(stk.Peek())   ' 1

LINQ: sintaxis de consulta y de método

LINQ (Language Integrated Query) transforma colecciones declarativamente. La sintaxis de consulta (From...Where...Select) es tipo SQL y legible para consultas complejas. La sintaxis de método (.Where().Select()) es fluida y encadena bien. Ambas compilan al mismo código. Operadores clave: Where (filtrar), Select (transformar), OrderBy (ordenar), GroupBy (agrupar), Take/Skip (paginar), First/FirstOrDefault (buscar), Any/All (probar), Sum/Average/Max/Min (agregar). FirstOrDefault devuelve el valor por defecto (0, Nothing) si no hay coincidencia — evita excepciones. LINQ funciona en cualquier IEnumerable (arrays, listas, diccionarios).

vb
Imports System.Linq

Dim nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Dim people = {
    New With {.Name = "Alice", .Age = 30, .City = "NYC"},
    New With {.Name = "Bob", .Age = 25, .City = "LA"},
    New With {.Name = "Carol", .Age = 35, .City = "NYC"}
}

' query syntax (SQL-like)
Dim adults = From p In people
             Where p.Age >= 30
             Select p.Name
' Alice, Carol

Dim nycNames = From p In people
               Where p.City = "NYC"
               Order By p.Age
               Select p.Name, p.Age

' method syntax (fluent, with lambdas)
Dim evens = nums.Where(Function(n) n Mod 2 = 0).ToList()       ' 2,4,6,8,10
Dim doubled = nums.Select(Function(n) n * 2).ToList()
Dim sum = nums.Sum()                                            ' 55
Dim avg = nums.Average()                                        ' 5.5
Dim max = nums.Max()
Dim count = nums.Count(Function(n) n > 5)                       ' 5

' ordering
Dim sorted = nums.OrderBy(Function(n) n).ToList()
Dim descSorted = nums.OrderByDescending(Function(n) n).ToList()

' grouping
Dim byCity = people.GroupBy(Function(p) p.City)
For Each g In byCity
    Console.WriteLine($"{g.Key}: {g.Count()} people")
Next

' take / skip (pagination)
Dim first3 = nums.Take(3).ToList()        ' 1,2,3
Dim skip3 = nums.Skip(3).Take(3).ToList() ' 4,5,6

' first / single / elementAt
Dim first = nums.First()                          ' 1
Dim firstEven = nums.First(Function(n) n Mod 2 = 0)  ' 2
Dim maybeFirst = nums.FirstOrDefault(Function(n) n > 100)  ' 0 (default)

' any / all
Dim hasEven = nums.Any(Function(n) n Mod 2 = 0)   ' True
Dim allPositive = nums.All(Function(n) n > 0)     ' True

' aggregate
Dim product = nums.Aggregate(Function(acc, n) acc * n)  ' factorial-like

Inicialización de colecciones y tuplas

Los inicializadores de colección (From { }) crean y rellenan colecciones en una sentencia. Las tuplas (VB 2017+) agrupan múltiples valores — los campos con nombre (X, Y) son más claros que Item1, Item2. Las tuplas son perfectas para devolver múltiples valores de una función sin definir una clase. La deconstrucción (Dim (a, b) = tuple) divide una tupla en variables. ToDictionary convierte una lista en un diccionario (selector de clave + selector de valor). Las tuplas son tipos de valor (structs), por lo que son eficientes para agrupaciones pequeñas y temporales.

vb
Imports System.Collections.Generic

' collection initializers (From clause)
Dim nums As New List(Of Integer) From {1, 2, 3, 4, 5}
Dim dict As New Dictionary(Of String, Integer) From {
    {"one", 1},
    {"two", 2},
    {"three", 3}
}
Dim set_ As New HashSet(Of String) From {"a", "b", "c"}

' tuples (VB 2017+)
Dim point As (X As Integer, Y As Integer) = (X := 3, Y := 4)
Console.WriteLine(point.X)   ' 3
Console.WriteLine(point.Y)   ' 4

' tuple without named fields (Item1, Item2, ...)
Dim t As (String, Integer) = ("Alice", 30)
Console.WriteLine(t.Item1)   ' Alice
Console.WriteLine(t.Item2)   ' 30

' tuple as function return (multiple values)
Function GetStats(nums As List(Of Integer)) As (Count As Integer, Sum As Integer, Avg As Double)
    Return (
        nums.Count,
        nums.Sum(),
        nums.Average()
    )
End Function

Dim stats = GetStats(nums)
Console.WriteLine($"Count={stats.Count}, Sum={stats.Sum}, Avg={stats.Avg:F2}")

' deconstruct tuples
Dim (name, age) = ("Bob", 25)
Console.WriteLine($"{name}, {age}")

' list of tuples
Dim people As New List(Of (Name As String, Age As Integer)) From {
    ("Alice", 30),
    ("Bob", 25)
}

' iterate with index
For i As Integer = 0 To nums.Count - 1
    Console.WriteLine($"{i}: {nums(i)}")
Next

' LINQ to dictionary
Dim nameToAge = people.ToDictionary(Function(p) p.Name, Function(p) p.Age)
Console.WriteLine(nameToAge("Alice"))   ' 30
06

Programación orientada a objetos

Clase, campos y propiedades

Las clases empaquetan datos (campos/propiedades) y comportamiento (métodos). Las propiedades auto-implementadas (Property X As Type) auto-generan un campo de respaldo oculto — conciso para datos simples. Las propiedades completas (bloques Get/Set) permiten validación, computación o efectos secundarios. Las propiedades ReadOnly tienen solo Get. Me se refiere a la instancia actual (como this en C#). Los constructores (Sub New) inicializan objetos. Las propiedades parecen campos a los llamadores (p.Name) pero ejecutan código — esta encapsulación es el beneficio central de OOP.

vb
Public Class Person
    ' private field (backing store)
    Private _name As String
    Private _age As Integer

    ' auto-implemented property (compiler creates the field)
    Public Property Email As String

    ' property with custom logic
    Public Property Name As String
        Get
            Return _name
        End Get
        Set(value As String)
            If String.IsNullOrEmpty(value) Then
                Throw New ArgumentException("Name required")
            End If
            _name = value
        End Set
    End Property

    ' property with validation
    Public Property Age As Integer
        Get
            Return _age
        End Get
        Set(value As Integer)
            If value < 0 OrElse value > 150 Then
                Throw New ArgumentOutOfRangeException("Age")
            End If
            _age = value
        End Set
    End Property

    ' read-only property (computed)
    Public ReadOnly Property IsAdult As Boolean
        Get
            Return _age >= 18
        End Get
    End Property

    ' constructor
    Public Sub New(name As String, age As Integer)
        Me.Name = name    ' Me refers to the current instance
        Me.Age = age
    End Sub
End Class

' usage
Dim p As New Person("Alice", 30)
Console.WriteLine(p.Name)      ' Alice
Console.WriteLine(p.IsAdult)   ' True
p.Age = 31                     ' uses setter
' p.Age = 200                  ' throws (validation)

Herencia y MyBase

Inherits establece una relación IS-A (Dog IS an Animal). MyBase.New() llama al constructor base (debe ser la primera sentencia). Overridable (base) + Overrides (derivada) habilitan polimorfismo — el método derivado se ejecuta incluso cuando se accede via una referencia base. Shadows (o Overloads) oculta un miembro base en lugar de sobrescribirlo (sin polimorfismo). Use Overridable/Overrides para verdadero polimorfismo; Shadows solo cuando debe ocultar un miembro que no puede cambiar. VB soporta herencia simple (una clase base) pero múltiples interfaces.

vb
' base class
Public Class Animal
    Public Property Name As String

    Public Sub New(name As String)
        Me.Name = name
    End Sub

    Public Overridable Function Speak() As String
        Return Name & " makes a sound"
    End Function

    Public Overridable Sub Eat()
        Console.WriteLine(Name & " is eating")
    End Sub
End Class

' derived class (Inherits)
Public Class Dog
    Inherits Animal

    Public Property Breed As String

    ' call base constructor with MyBase
    Public Sub New(name As String, breed As String)
        MyBase.New(name)      ' must be first line
        Me.Breed = breed
    End Sub

    ' override virtual method
    Public Overrides Function Speak() As String
        Return Name & " says Woof"
    End Function

    ' extend base method
    Public Overrides Sub Eat()
        MyBase.Eat()          ' call base implementation
        Console.WriteLine(Name & " wags tail")
    End Sub

    ' new method (not override — hides base)
    Public Shadows Sub Description()
        Console.WriteLine($"{Name} ({Breed})")
    End Sub
End Class

Dim d As New Dog("Rex", "Labrador")
Console.WriteLine(d.Speak())   ' Rex says Woof
d.Eat()                        ' Rex is eating / Rex wags tail

' polymorphism: base reference, derived object
Dim a As Animal = New Dog("Buddy", "Poodle")
Console.WriteLine(a.Speak())   ' Buddy says Woof (virtual dispatch)

Interfaces y polimorfismo

Las interfaces definen un contrato (métodos, propiedades) sin implementación — las clases las Implement. A diferencia de la herencia, una clase puede implementar múltiples interfaces. La palabra clave Implements enlaza un miembro con su declaración de interfaz (sintaxis específica de VB). Las interfaces habilitan polimorfismo: el código puede trabajar con cualquier IDrawable sin saber si es Circle o Square. TypeOf x Is T comprueba el tipo en tiempo de ejecución; DirectCast hace cast (lanza si es inválido). Use interfaces para desacoplar código: dependa de IDrawable, no de Circle. Este es el principio de Inversión de Dependencias.

vb
' interface: contract (no implementation)
Public Interface IComparable(Of T)
    Function CompareTo(other As T) As Integer
End Interface

Public Interface IDrawable
    Sub Draw()
    Property X As Integer
    Property Y As Integer
End Interface

' implement an interface
Public Class Circle
    Implements IDrawable

    Public Property X As Integer Implements IDrawable.X
    Public Property Y As Integer Implements IDrawable.Y
    Public Property Radius As Integer

    Public Sub Draw() Implements IDrawable.Draw
        Console.WriteLine($"Drawing circle at ({X}, {Y}) r={Radius}")
    End Sub
End Class

Public Class Square
    Implements IDrawable

    Public Property X As Integer Implements IDrawable.X
    Public Property Y As Integer Implements IDrawable.Y
    Public Property Side As Integer

    Public Sub Draw() Implements IDrawable.Draw
        Console.WriteLine($"Drawing square at ({X}, {Y}) side={Side}")
    End Sub
End Class

' polymorphism via interface
Dim shapes As New List(Of IDrawable) From {
    New Circle With {.X = 0, .Y = 0, .Radius = 5},
    New Square With {.X = 10, .Y = 10, .Side = 4}
}
For Each s In shapes
    s.Draw()   ' calls the right implementation
Next

' check and cast
Dim shape As IDrawable = New Circle()
If TypeOf shape Is Circle Then
    Dim c As Circle = DirectCast(shape, Circle)
    c.Radius = 10
End If

' multiple interfaces
Public Class TextBox
    Implements IDrawable, IComparable(Of TextBox)
    ' ... implement both
End Class

Genéricos y restricciones

Los genéricos (Of T) le permiten escribir código type-safe y reutilizable que funciona con cualquier tipo — sin boxing, sin casts, comprobación de tipos en tiempo de compilación. List(Of T), Dictionary(Of K,V), Stack(Of T) son colecciones genéricas. Las restricciones limitan el parámetro de tipo: Class (tipo referencia), Structure (tipo valor), New (tiene constructor sin parámetros — permite New T()), clase base específica o interfaz. Los genéricos evitan el impacto de rendimiento del boxing (tipos de valor) y la fragilidad de los casts de Object. Siempre prefiera colecciones genéricas (List(Of T)) sobre no genéricas (ArrayList).

vb
' generic class
Public Class Stack(Of T)
    Private items As New List(Of T)

    Public Sub Push(item As T)
        items.Add(item)
    End Sub

    Public Function Pop() As T
        If items.Count = 0 Then Throw New InvalidOperationException("Empty")
        Dim last As T = items(items.Count - 1)
        items.RemoveAt(items.Count - 1)
        Return last
    End Function

    Public ReadOnly Property Count As Integer
        Get
            Return items.Count
        End Get
    End Property
End Class

' use with different types
Dim intStack As New Stack(Of Integer)
intStack.Push(1)
intStack.Push(2)
Console.WriteLine(intStack.Pop())   ' 2

Dim strStack As New Stack(Of String)
strStack.Push("Hello")
Console.WriteLine(strStack.Pop())   ' Hello

' generic method with constraint
Public Class Repository(Of T As {Class, New})
    ' T must be a reference type (Class) and have a parameterless constructor (New)
    Private items As New List(Of T)

    Public Sub Add(item As T)
        items.Add(item)
    End Sub

    Public Function Find(match As Predicate(Of T)) As T
        Return items.Find(match)
    End Function
End Class

' generic function
Function FirstOrDefault(Of T)(list As IEnumerable(Of T), predicate As Func(Of T, Boolean)) As T
    For Each item In list
        If predicate(item) Then Return item
    Next
    Return Nothing    ' default for reference types
End Function

' constraints:
'   Class      - reference type
'   Structure  - value type
'   New        - has parameterless constructor
'   BaseClass  - inherits from a specific class
'   Interface  - implements an interface

Clases Partial y Namespaces

Las clases Partial dividen una clase entre múltiples archivos — el compilador los fusiona. Útil para separar código generado (archivos designer) de código escrito a mano, o dividir clases grandes. Los Namespaces organizan tipos y previenen colisiones de nombres (MyApp.Models.User vs MyApp.Services.User). Imports trae nombres de namespace al alcance (sin necesidad de calificación completa). El RootNamespace (configuración del proyecto) envuelve todos los tipos — establézcalo al nombre de su empresa/producto. Convenciones: los namespaces coinciden con la estructura de carpetas; un namespace por área lógica.

vb
' File1.vb
Namespace MyApp.Models
    Partial Public Class User
        Public Property Id As Integer
        Public Property Name As String
        Public Property Email As String
    End Class
End Namespace

' File2.vb (same class, different file)
Namespace MyApp.Models
    Partial Public Class User
        Public Sub SendWelcomeEmail()
            Console.WriteLine($"Welcome {Name}!")
        End Sub

        Public Function Validate() As Boolean
            Return Not String.IsNullOrEmpty(Email)
        End Function
    End Class
End Namespace

' usage (combines both files)
Dim u As New MyApp.Models.User With {
    .Id = 1,
    .Name = "Alice",
    .Email = "[email protected]"
}
u.SendWelcomeEmail()
Console.WriteLine(u.Validate())   ' True

' namespace nesting
Namespace MyApp.Services
    Public Class UserService
        Public Sub Create(name As String)
            ' ...
        End Sub
    End Class
End Namespace

' import to use short names
Imports MyApp.Models
Imports MyApp.Services

Module Program
    Sub Main()
        Dim user As New User()        ' no full namespace needed
        Dim svc As New UserService()
    End Sub
End Module
07

Manejo de errores y excepciones

Try...Catch...Finally

Try/Catch/Finally es manejo de excepciones estructurado. Los bloques Catch se comprueban en orden — ponga excepciones específicas primero, Exception (base) último. Finally siempre se ejecuta (incluso en Return o excepciones no capturadas) — úselo para limpieza (cerrar archivos, liberar recursos). Throw ( desnudo) relanza la excepción actual, preservando el stack trace. When filtra condicionalmente (Catch...When condition). Propiedades de Exception: Message (descripción), StackTrace (cadena de llamadas), Source (ensamblado). Nunca capture Exception silenciosamente sin loguear — eso oculta bugs.

vb
Try
    Dim x As Integer = 10
    Dim y As Integer = 0
    Dim z As Integer = x \ y          ' integer division by zero
Catch ex As DivideByZeroException
    Console.WriteLine("Cannot divide by zero: " & ex.Message)
Catch ex As OverflowException
    Console.WriteLine("Number too large: " & ex.Message)
Catch ex As Exception                  ' catch-all (must be last)
    Console.WriteLine("Unexpected error: " & ex.Message)
Finally
    ' always runs (even with Return or exception)
    Console.WriteLine("Cleanup complete")
End Try

' nested Try
Try
    Try
        Dim arr() As Integer = {1, 2, 3}
        Console.WriteLine(arr(10))     ' IndexOutOfRangeException
    Catch ex As IndexOutOfRangeException
        Console.WriteLine("Index error: " & ex.Message)
        Throw                          ' re-throw to outer handler
    End Try
Catch ex As Exception
    Console.WriteLine("Outer caught: " & ex.Message)
End Try

' When filter (conditional catch)
Try
    ' risky code
Catch ex As Exception When ex.Message.Contains("network")
    Console.WriteLine("Network error, retrying...")
End Try

' exception properties
Try
    Dim n As Integer = Integer.Parse("abc")
Catch ex As FormatException
    Console.WriteLine(ex.Message)        ' human-readable
    Console.WriteLine(ex.GetType().Name) ' FormatException
    Console.WriteLine(ex.StackTrace)     ' where it happened
    Console.WriteLine(ex.Source)         ' which assembly
End Try

Lanzamiento y excepciones personalizadas

Throw New ExceptionType(...) lanza una excepción. Siempre pase un mensaje significativo y use NameOf() para nombres de parámetros (seguro para refactoring). Al envolver excepciones, pase la original como innerException para preservar la causa. Las excepciones personalizadas deberían heredar de Exception (o una base más específica), marcarse <Serializable>, y proporcionar constructores (message, message+inner). Sobrescriba ToString() para visualización personalizada. Las excepciones personalizadas dejan a los llamadores capturar tipos de error específicos y manejarlos apropiadamente — úselas para errores de dominio (InvalidTransactionException, PaymentFailedException).

vb
' throw a built-in exception
Sub CheckAge(age As Integer)
    If age < 0 Then
        Throw New ArgumentOutOfRangeException(NameOf(age), "Age cannot be negative")
    ElseIf age > 150 Then
        Throw New ArgumentOutOfRangeException(NameOf(age), "Age unrealistic")
    End If
End Sub

' throw with inner exception (wrapping)
Function LoadConfig(path As String) As String
    Try
        Return File.ReadAllText(path)
    Catch ex As FileNotFoundException
        Throw New ConfigurationException("Config not found", ex)   ' wrap
    End Try
End Function

' custom exception (inherit from Exception)
<Serializable>
Public Class InvalidTransactionException
    Inherits Exception

    Public Property TransactionId As String

    Public Sub New(message As String, transactionId As String)
        MyBase.New(message)
        Me.TransactionId = transactionId
    End Sub

    Public Sub New(message As String, transactionId As String, inner As Exception)
        MyBase.New(message, inner)
        Me.TransactionId = transactionId
    End Sub

    Public Overrides Function ToString() As String
        Return $"Transaction {TransactionId} failed: {Message}"
    End Function
End Class

' using the custom exception
Sub ProcessPayment(amount As Decimal, txnId As String)
    If amount <= 0 Then
        Throw New InvalidTransactionException("Amount must be positive", txnId)
    End If
    ' ... process
End Sub

' catch custom exception
Try
    ProcessPayment(-100, "TXN-001")
Catch ex As InvalidTransactionException
    Console.WriteLine($"Failed: {ex.TransactionId} - {ex.Message}")
End Try

Sentencias Using e IDisposable

Using asegura que Dispose() se llame al salir del bloque — crítico para recursos (archivos, conexiones a base de datos, streams de red) que no son recolectados como basura prontamente. Múltiples recursos pueden declararse en un Using (separados por comas). Implemente IDisposable cuando su clase contenga recursos no gestionados u otros objetos IDisposable. El patrón Dispose: Dispose(disposing As Boolean) libera recursos gestionados (cuando disposing=True) y no gestionados; GC.SuppressFinalize previene que el finalizador se ejecute. Siempre envuelva objetos IDisposable en Using para prevenir fugas de recursos.

vb
Imports System.IO

' Using ensures Dispose is called (even on exception)
Using writer As New StreamWriter("output.txt")
    writer.WriteLine("Hello")
    writer.WriteLine("World")
End Using   ' writer.Dispose() called automatically

' equivalent to:
Dim writer2 As StreamWriter = Nothing
Try
    writer2 = New StreamWriter("output.txt")
    writer2.WriteLine("Hello")
Finally
    writer2?.Dispose()
End Using

' multiple resources in one Using
Using conn As New SqlClient.SqlConnection(connStr),
      cmd As New SqlClient.SqlCommand("SELECT 1", conn)
    conn.Open()
    Dim result = cmd.ExecuteScalar()
End Using

' implement IDisposable for your class
Public Class FileManager
    Implements IDisposable

    Private stream As FileStream
    Private disposed As Boolean = False

    Public Sub New(path As String)
        stream = New FileStream(path, FileMode.Open)
    End Sub

    Public Function ReadLine() As String
        ' ... read from stream
    End Function

    Protected Overridable Sub Dispose(disposing As Boolean)
        If Not disposed Then
            If disposing Then
                ' free managed resources
                stream?.Dispose()
            End If
            ' free unmanaged resources (if any)
            disposed = True
        End If
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub
End Class

' usage
Using fm As New FileManager("data.txt")
    Dim line = fm.ReadLine()
End Using

Mejores prácticas de excepciones

Mejores prácticas de excepciones: (1) No capture excepciones que no puede manejar realmente — déjelas propagar. (2) Nunca trague excepciones silenciosamente (Catch vacío) — como mínimo logueelas. (3) Use TryParse en lugar de Parse para entrada de usuario (las excepciones son costosas y para casos verdaderamente excepcionales). (4) Lance tipos de excepción específicos (ArgumentOutOfRangeException, no Exception). (5) Use filtros When para loguear sin capturar (devuelva False para no capturar). (6) Relance con Throw (desnudo) para preservar el stack trace — no Throw ex que lo reinicia. Las excepciones son para condiciones excepcionales, no flujo de control normal.

vb
' 1. Don't catch exceptions you can't handle
' BAD: swallows all errors
Try
    File.Delete(path)
Catch ex As Exception
    ' silent failure — bug hidden!
End Try

' GOOD: only catch what you expect
Try
    File.Delete(path)
Catch ex As FileNotFoundException
    ' file already gone — that's fine
Catch ex As UnauthorizedAccessException
    Console.WriteLine("Permission denied: " & ex.Message)
    ' re-throw or handle meaningfully
End Try

' 2. Log exceptions (don't swallow)
Try
    ProcessData()
Catch ex As Exception
    Logger.Error(ex, "ProcessData failed")
    Throw   ' re-throw after logging
End Try

' 3. Validate input (avoid exceptions for expected conditions)
' BAD: exception for control flow
Try
    Dim n = Integer.Parse(input)
Catch ex As FormatException
    n = 0
End Try

' GOOD: TryParse
Dim n As Integer
If Not Integer.TryParse(input, n) Then
    n = 0
    Console.WriteLine("Invalid number, using 0")
End If

' 4. Use specific exception types
If amount < 0 Then
    Throw New ArgumentOutOfRangeException(NameOf(amount))
End If

' 5. Exception filters (When) for logging without catching
Try
    RiskyOperation()
Catch ex As Exception When LogError(ex)
    ' only enters if LogError returns True
End Try

Function LogError(ex As Exception) As Boolean
    Logger.Error(ex)
    Return False   ' don't catch, just log
End Function

Depuración y diagnósticos

Debug.Assert/WriteLine solo se ejecutan en builds Debug (compilados fuera en Release) — use para invariantes y diagnósticos. Trace funciona en ambos builds — para logging de producción. #If DEBUG...#End If habilita compilación condicional. Debugger.Break() actúa como un breakpoint programático. Stopwatch mide tiempo transcurrido con precisión (para benchmarking). StackTrace captura la cadena de llamadas (útil para logging). EventLog escribe al Windows Event Log (requiere admin para crear el source). Use estas herramientas para diagnosticar problemas sin modificar el comportamiento de producción.

vb
Imports System.Diagnostics

' Debug.Assert: only in Debug builds
Debug.Assert(age >= 0, "Age should be non-negative")

' Debug.WriteLine: only in Debug builds
Debug.WriteLine($"Processing {name}, age {age}")

' Trace: works in Release builds too
Trace.WriteLine("App started")
Trace.WriteLineIf(age > 100, "Unusual age")

' conditional compilation
#If DEBUG Then
    Console.WriteLine("Debug build")
#Else
    Console.WriteLine("Release build")
#End If

' Debugger.Break: pause in debugger (like a breakpoint)
If count > 1000 Then
    Debugger.Break()
End If

' Stopwatch for performance measurement
Dim sw As Stopwatch = Stopwatch.StartNew()
For i As Integer = 1 To 1000000
    ' some work
Next
sw.Stop()
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms")

' StackTrace for debugging
Dim st As New StackTrace(True)
Console.WriteLine(st.ToString())   ' current call stack

' EventLog for Windows event logging (requires admin)
If Not EventLog.SourceExists("MyApp") Then
    EventLog.CreateEventSource("MyApp", "Application")
End If
EventLog.WriteEntry("MyApp", "Started", EventLogEntryType.Information)

' process info
Dim p As Process = Process.GetCurrentProcess()
Console.WriteLine($"Memory: {p.WorkingSet64 \ 1024} KB")
Console.WriteLine($"PID: {p.Id}")
08

E/S de archivos y streams

Lectura y escritura de archivos de texto

File.WriteAllText/ReadAllText son los más simples para archivos pequeños. File.ReadAllLines devuelve un array de String (carga todo el archivo en memoria). Para archivos grandes, use StreamReader/StreamWriter con Using — leen/escriben línea por línea, manteniendo baja la memoria. Using asegura que el stream se cierre incluso en excepciones. AppendAllText/AppendAllLines añaden a archivos existentes. Siempre compruebe File.Exists antes de leer para dar un error amigable. Para E/S async (UI no bloqueante), use ReadAllTextAsync/WriteAllTextAsync con Await.

vb
Imports System.IO

' write all text at once (simple)
File.WriteAllText("output.txt", "Hello, World!")

' append text
File.AppendAllText("log.txt", $"[{Date.Now}] Started{Environment.NewLine}")

' read all text
Dim content As String = File.ReadAllText("output.txt")

' read all lines into an array
Dim lines() As String = File.ReadAllLines("data.csv")
For Each line In lines
    Console.WriteLine(line)
Next

' write all lines
File.WriteAllLines("nums.txt", {"one", "two", "three"})

' append lines
File.AppendAllLines("log.txt", {"line1", "line2"})

' stream-based writing (for large files)
Using writer As New StreamWriter("big.txt")
    For i As Integer = 1 To 1000000
        writer.WriteLine($"Line {i}")
    Next
End Using   ' flushes and closes automatically

' stream-based reading (line by line, memory-efficient)
Using reader As New StreamReader("big.txt")
    Dim line As String
    Do
        line = reader.ReadLine()
        If line Is Nothing Then Exit Do
        Console.WriteLine(line)
    Loop
End Using

' check if file exists
If File.Exists("data.txt") Then
    Console.WriteLine("Found")
End If

' File.ReadAllLinesAsync (VB 15+ with async)
Async Function ReadAsync(path As String) As Task(Of String())
    Return Await File.ReadAllLinesAsync(path)
End Function

Archivos binarios y serialización

BinaryWriter/Reader leen/escriben tipos primitivos en un formato binario compacto — el orden de lectura debe coincidir con el de escritura. Para datos estructurados, prefiera JSON (legible por humanos, agnóstico de lenguaje). System.Text.Json (integrado, rápido) o Newtonsoft.Json (popular, rico en características) serializan objetos a/desde JSON. JsonSerializer.Serialize/Deserialize(Of T) son los métodos core. Use JsonSerializerOptions para formato (indentado, camelCase). JSON es ideal para archivos de configuración, APIs e intercambio de datos. Evite BinaryFormatter (riesgo de seguridad — eliminado en .NET 5+).

vb
Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary

' write binary data
Using fs As New FileStream("data.bin", FileMode.Create)
    Using writer As New BinaryWriter(fs)
        writer.Write(42)              ' Integer
        writer.Write(3.14)            ' Double
        writer.Write("Hello")         ' String (length-prefixed)
        writer.Write(True)            ' Boolean
        writer.Write(New Byte() {1, 2, 3})  ' byte array
    End Using
End Using

' read binary data (must read in same order!)
Using fs As New FileStream("data.bin", FileMode.Open)
    Using reader As New BinaryReader(fs)
        Dim n As Integer = reader.ReadInt32()
        Dim d As Double = reader.ReadDouble()
        Dim s As String = reader.ReadString()
        Dim b As Boolean = reader.ReadBoolean()
        Dim bytes() As Byte = reader.ReadBytes(3)
        Console.WriteLine($"{n}, {d}, {s}, {b}")
    End Using
End Using

' JSON serialization (Newtonsoft.Json or System.Text.Json)
Imports System.Text.Json

Public Class User
    Public Property Name As String
    Public Property Age As Integer
End Class

Dim u As New User With {.Name = "Alice", .Age = 30}
Dim json As String = JsonSerializer.Serialize(u)
' {"Name":"Alice","Age":30}
File.WriteAllText("user.json", json)

Dim restored As User = JsonSerializer.Deserialize(Of User)(json)

' JSON with options
Dim opts As New JsonSerializerOptions With {
    .WriteIndented = True,
    .PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}
Dim prettyJson = JsonSerializer.Serialize(u, opts)

Operaciones de directorio y ruta

Directory.CreateDirectory/GetFiles/GetDirectories/Exists/Delete gestionan carpetas. GetFiles soporta patrones de búsqueda (*.csv) y SearchOption.AllDirectories para recursión. La clase Path maneja rutas de forma segura entre plataformas — siempre use Path.Combine (no &) para unir rutas (maneja separadores). Path.GetTempFileName crea un archivo temporal único. FileInfo/DirectoryInfo son wrappers orientados a objetos sobre operaciones de archivo/directorio con propiedades (Length, CreationTime) y métodos (CopyTo, MoveTo, Delete). Use estos en lugar de manipulación de cadenas para rutas.

vb
Imports System.IO

' directory operations
Directory.CreateDirectory("backup/2024/june")

' list files
Dim files() As String = Directory.GetFiles("C:\temp")
Dim csvFiles() As String = Directory.GetFiles("C:\temp", "*.csv")
Dim allFiles() As String = Directory.GetFiles("C:\temp", "*.*", SearchOption.AllDirectories)

' list directories
Dim dirs() As String = Directory.GetDirectories("C:\temp")

' check and delete
If Directory.Exists("old") Then
    Directory.Delete("old", recursive := True)   ' delete with contents
End If

' Path class (cross-platform path handling)
Dim fullPath As String = Path.Combine("folder", "sub", "file.txt")
' folder\sub\file.txt (on Windows)

Console.WriteLine(Path.GetFileName("C:\temp\data.txt"))    ' data.txt
Console.WriteLine(Path.GetExtension("photo.JPG"))            ' .JPG
Console.WriteLine(Path.GetFileNameWithoutExtension("data.txt"))  ' data
Console.WriteLine(Path.GetDirectoryName("C:\temp\data.txt"))   ' C:\temp
Console.WriteLine(Path.GetFullPath("data.txt"))              ' absolute path

' temp files
Dim tempFile As String = Path.GetTempFileName()   ' creates a temp file
Dim tempDir As String = Path.GetTempPath()
Console.WriteLine(tempFile)

' file info
Dim fi As New FileInfo("data.txt")
If fi.Exists Then
    Console.WriteLine($"Size: {fi.Length} bytes")
    Console.WriteLine($"Created: {fi.CreationTime}")
    Console.WriteLine($"Modified: {fi.LastWriteTime}")
    Console.WriteLine($"Extension: {fi.Extension}")
    fi.CopyTo("data_backup.txt", overwrite := True)
    fi.MoveTo("renamed.txt")
    fi.Delete()
End If

' directory info
Dim di As New DirectoryInfo("C:\temp")
For Each f As FileInfo In di.GetFiles("*.txt")
    Console.WriteLine(f.Name)
Next

Parseo de CSV y archivos de datos

Para CSV simple, Split(","c) funciona — pero se rompe en campos entre comillas con comas ("Smith, John"). TextFieldParser (en Microsoft.VisualBasic.FileIO) maneja comillas, comillas escapadas y delimitadores correctamente — úselo para CSV real. También parsea archivos de ancho fijo (FieldWidths). Para escribir CSV, entrecomille manualmente campos que contienen comas/comillas. My.Computer.FileSystem es un wrapper de conveniencia específico de VB (API más simple, menos control). Para CSVs grandes, considere CsvHelper (NuGet) — una librería CSV robusta y de streaming. Siempre maneje la fila de encabezado por separado.

vb
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

' simple CSV split (doesn't handle quoted commas)
Dim lines() As String = File.ReadAllLines("data.csv")
For Each line In lines
    Dim fields() As String = line.Split(","c)
    Console.WriteLine($"{fields(0)}, {fields(1)}")
Next

' robust CSV with TextFieldParser (handles quotes, commas)
Using parser As New TextFieldParser("data.csv")
    parser.TextFieldType = FieldType.Delimited
    parser.Delimiters = New String() {","}
    parser.HasFieldsEnclosedInQuotes = True

    ' skip header
    If Not parser.EndOfData Then parser.ReadLine()

    While Not parser.EndOfData
        Dim fields() As String = parser.ReadFields()
        Console.WriteLine($"Name: {fields(0)}, Age: {fields(1)}")
    End While
End Using

' write CSV
Using writer As New StreamWriter("output.csv")
    writer.WriteLine("Name,Age,City")
    writer.WriteLine("Alice,30,NYC")
    writer.WriteLine("Bob,25,LA")
    ' quote fields with commas
    writer.WriteLine($"""Smith, John"",40,Chicago")
End Using

' parse fixed-width files
Using parser As New TextFieldParser("fixed.txt")
    parser.TextFieldType = FieldType.FixedWidth
    parser.FieldWidths = New Integer() {10, 5, 20}
End Using

' read with My.Computer.FileSystem (VB-specific shortcut)
Dim contents As String = My.Computer.FileSystem.ReadAllText("data.txt")
Dim allLines As Object = My.Computer.FileSystem.ReadAllBytes("data.bin")

' write bytes
File.WriteAllBytes("data.bin", New Byte() {1, 2, 3, 4, 5})

Operaciones de archivos async

La E/S de archivos async usa ReadToEndAsync/ReadLineAsync/WriteAsync con Await — el hilo no se bloquea durante la E/S, mejorando la responsividad (especialmente en apps UI). Los métodos async devuelven Task o Task(Of T); Await desenvuelve el resultado. Para procesar muchos archivos concurrentemente, inicie todas las tareas y Await Task.WhenAll (E/S paralela). Async Sub es para manejadores de eventos; Async Function para todo lo demás. La E/S async brilla en servidores web (manejando muchas peticiones) y apps de escritorio (manteniendo la UI responsiva). La sobrecarga es pequeña, así que use async para cualquier E/S que pueda ser lenta.

vb
Imports System.IO
Imports System.Threading.Tasks

' async read (non-blocking)
Async Function ReadFileAsync(path As String) As Task(Of String)
    Using reader As New StreamReader(path)
        Return Await reader.ReadToEndAsync()
    End Using
End Function

' async write
Async Function WriteFileAsync(path As String, content As String) As Task
    Using writer As New StreamWriter(path)
        Await writer.WriteAsync(content)
    End Using
End Function

' async line-by-line (memory-efficient for large files)
Async Function ProcessLargeFileAsync(path As String) As Task
    Using reader As New StreamReader(path)
        Dim line As String
        Do
            line = Await reader.ReadLineAsync()
            If line Is Nothing Then Exit Do
            ' process line
            Console.WriteLine(line)
        Loop
    End Using
End Function

' call async methods (must be in an Async Sub/Function)
Async Sub Main()
    Dim content As String = Await ReadFileAsync("data.txt")
    Console.WriteLine(content)

    Await WriteFileAsync("output.txt", "Async write")
End Sub

' parallel file processing
Async Function ProcessFilesAsync(paths As String()) As Task
    Dim tasks As New List(Of Task)
    For Each path In paths
        tasks.Add(ProcessFileAsync(path))
    Next
    Await Task.WhenAll(tasks)   ' wait for all
End Function

Async Function ProcessFileAsync(path As String) As Task
    Dim content = Await File.ReadAllTextAsync(path)
    ' process content
End Function

' File.ReadAllLinesAsync (.NET Core+)
Async Function CountLinesAsync(path As String) As Task(Of Integer)
    Dim lines = Await File.ReadAllLinesAsync(path)
    Return lines.Length
End Function
09

Windows Forms y UI

Fundamentos de Form y eventos

Windows Forms (WinForms) construye UIs de escritorio añadiendo Controls a un Form. Cada control tiene propiedades (Text, Location, Size) y eventos (Click, Load, FormClosing). AddHandler cablea eventos en tiempo de ejecución; Handles (con WithEvents) cablea en tiempo de compilación. MessageBox.Show muestra diálogos. OnLoad/OnFormClosing son métodos protegidos overridable para el ciclo de vida del formulario. Application.Run inicia el bucle de mensajes. WinForms usa el designer (drag-and-drop en Visual Studio) — el código generado va en una clase partial .Designer.vb. Para apps modernas, considere WPF o WinUI.

vb
Imports System.Windows.Forms
Imports System.Drawing

Public Class MainForm
    Inherits Form

    Public Sub New()
        Me.Text = "My App"             ' form title
        Me.Size = New Size(400, 300)
        Me.StartPosition = FormStartPosition.CenterScreen

        ' create a button
        Dim btn As New Button With {
            .Text = "Click Me",
            .Location = New Point(150, 100),
            .Size = New Size(100, 30)
        }
        AddHandler btn.Click, AddressOf OnButtonClick
        Me.Controls.Add(btn)

        ' create a label
        Dim lbl As New Label With {
            .Text = "Hello",
            .Location = New Point(150, 50),
            .AutoSize = True
        }
        Me.Controls.Add(lbl)
    End Sub

    Private Sub OnButtonClick(sender As Object, e As EventArgs)
        MessageBox.Show("Button clicked!", "Info",
                        MessageBoxButtons.OK, MessageBoxIcon.Information)
    End Sub

    ' form events
    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
        Console.WriteLine("Form loaded")
    End Sub

    Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
        Dim result = MessageBox.Show("Close?", "Confirm",
                                     MessageBoxButtons.YesNo)
        If result = DialogResult.No Then
            e.Cancel = True
        End If
        MyBase.OnFormClosing(e)
    End Sub
End Class

' run the app
Module Program
    Sub Main()
        Application.EnableVisualStyles()
        Application.Run(New MainForm())
    End Sub
End Module

Controles comunes

Controles WinForms comunes: TextBox (entrada de texto, Multiline para área), CheckBox (booleano), RadioButton (excluyentes mutuamente — agrupe en GroupBox), ComboBox (desplegable), NumericUpDown (entrada numérica con spinner), ListBox (lista seleccionable). Establezca Location (Point) y Size. Use inicializadores de objeto (With {.Prop = value}) para configuración concisa. Controls.AddRange añade múltiples controles a la vez. Cada control tiene un evento por defecto (Button.Click, TextBox.TextChanged, ComboBox.SelectedIndexChanged) — doble clic en el designer para generar un handler.

vb
Imports System.Windows.Forms
Imports System.Drawing

Public Class ControlsForm
    Inherits Form

    Public Sub New()
        ' TextBox (single-line input)
        Dim txt As New TextBox With {
            .Location = New Point(20, 20),
            .Width = 200
        }

        ' multiline TextBox
        Dim txtMulti As New TextBox With {
            .Multiline = True,
            .Location = New Point(20, 60),
            .Size = New Size(200, 80),
            .ScrollBars = ScrollBars.Vertical
        }

        ' CheckBox
        Dim chk As New CheckBox With {
            .Text = "Enable feature",
            .Location = New Point(20, 160),
            .Checked = True
        }

        ' RadioButton (group in GroupBox for mutual exclusion)
        Dim grp As New GroupBox With {.Text = "Gender", .Location = New Point(20, 190), .Size = New Size(120, 80)}
        Dim rb1 As New RadioButton With {.Text = "Male", .Location = New Point(10, 20), .Checked = True}
        Dim rb2 As New RadioButton With {.Text = "Female", .Location = New Point(10, 40)}
        grp.Controls.AddRange({rb1, rb2})

        ' ComboBox (dropdown)
        Dim cmb As New ComboBox With {.Location = New Point(150, 190), .DropDownStyle = ComboBoxStyle.DropDownList}
        cmb.Items.AddRange({"Red", "Green", "Blue"})
        cmb.SelectedIndex = 0

        ' NumericUpDown
        Dim num As New NumericUpDown With {
            .Location = New Point(150, 220),
            .Minimum = 0,
            .Maximum = 100,
            .Value = 50
        }

        ' ListBox
        Dim lst As New ListBox With {.Location = New Point(20, 280), .Size = New Size(150, 80)}
        lst.Items.AddRange({"Apple", "Banana", "Cherry"})

        Me.Controls.AddRange({txt, txtMulti, chk, grp, cmb, num, lst})
    End Sub
End Class

Diseño: paneles, docking y anclaje

Diseño WinForms: Dock (Top/Bottom/Left/Right/Fill) hace que un control llene un borde — añada Fill último para que tome el espacio restante. Anchor fija un control a los bordes del padre (se redimensiona cuando el formulario se redimensiona). TableLayoutPanel organiza controles en una cuadrícula (filas/columnas con tamaños porcentaje/absoluto) — mejor para formularios. FlowLayoutPanel apila controles en un flujo (se ajusta automáticamente). Panel es un contenedor simple para agrupar. Para diseños responsivos, prefiera TableLayoutPanel sobre posicionamiento manual. El orden de Controls.Add importa para docking (añadidos posteriores solapan anteriores).

vb
Imports System.Windows.Forms
Imports System.Drawing

Public Class LayoutForm
    Inherits Form

    Public Sub New()
        Me.Size = New Size(600, 400)

        ' Panel: container for grouping controls
        Dim topPanel As New Panel With {
            .Dock = DockStyle.Top,        ' fill the top
            .Height = 50,
            .BackColor = Color.LightBlue
        }

        Dim bottomPanel As New Panel With {
            .Dock = DockStyle.Bottom,
            .Height = 40,
            .BackColor = Color.LightGray
        }

        ' Fill remaining space
        Dim centerPanel As New Panel With {
            .Dock = DockStyle.Fill,
            .BackColor = Color.White
        }

        ' Add in reverse Z-order (Fill last to actually fill)
        Me.Controls.Add(centerPanel)
        Me.Controls.Add(bottomPanel)
        Me.Controls.Add(topPanel)

        ' Anchoring: control resizes with parent edges
        Dim btn As New Button With {
            .Text = "Anchored",
            .Location = New Point(10, 10),
            .Anchor = AnchorStyles.Top Or AnchorStyles.Left Or AnchorStyles.Right
        }
        centerPanel.Controls.Add(btn)

        ' TableLayoutPanel: grid layout
        Dim tlp As New TableLayoutPanel With {
            .Dock = DockStyle.Fill,
            .ColumnCount = 2,
            .RowCount = 2
        }
        tlp.ColumnStyles.Add(New ColumnStyle(SizeType.Percent, 50))
        tlp.ColumnStyles.Add(New ColumnStyle(SizeType.Percent, 50))
        tlp.Controls.Add(New Label With {.Text = "Name:"}, 0, 0)
        tlp.Controls.Add(New TextBox(), 1, 0)
        tlp.Controls.Add(New Label With {.Text = "Age:"}, 0, 1)
        tlp.Controls.Add(New NumericUpDown(), 1, 1)

        ' FlowLayoutPanel: stacks controls left-to-right or top-to-bottom
        Dim flp As New FlowLayoutPanel With {
            .FlowDirection = FlowDirection.LeftToRight,
            .Dock = DockStyle.Top,
            .Height = 40
        }
        flp.Controls.AddRange({New Button With {.Text = "New"}, New Button With {.Text = "Open"}})

        Me.Controls.Add(flp)
        Me.Controls.Add(tlp)
    End Sub
End Class

Diálogos: OpenFileDialog, SaveFileDialog, ColorDialog

Diálogos comunes: OpenFileDialog (seleccionar archivo para abrir), SaveFileDialog (elegir dónde guardar), ColorDialog (elegir color), FontDialog (elegir fuente), FolderBrowserDialog (seleccionar directorio). Filter usa el formato "Descripción|patrón|Descripción|patrón". ShowDialog devuelve DialogResult (OK/Cancel) — siempre compruebe antes de usar el resultado. Envuelva diálogos en Using (son IDisposable). OverwritePrompt previene sobrescrituras accidentales. InitialDirectory inicia el diálogo en una ubicación útil. Estos diálogos proporcionan UI nativa de Windows sin código personalizado.

vb
Imports System.Windows.Forms

Public Class DialogForm
    Inherits Form

    Private Sub OpenFile()
        Using ofd As New OpenFileDialog
            ofd.Title = "Select a file"
            ofd.Filter = "Text files (*.txt)|*.txt|CSV files (*.csv)|*.csv|All files (*.*)|*.*"
            ofd.FilterIndex = 1
            ofd.Multiselect = False
            ofd.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)

            If ofd.ShowDialog() = DialogResult.OK Then
                Dim path As String = ofd.FileName
                MessageBox.Show($"Selected: {path}")
            End If
        End Using
    End Sub

    Private Sub SaveFile()
        Using sfd As New SaveFileDialog
            sfd.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*"
            sfd.DefaultExt = "txt"
            sfd.AddExtension = True
            sfd.OverwritePrompt = True    ' ask before overwriting

            If sfd.ShowDialog() = DialogResult.OK Then
                System.IO.File.WriteAllText(sfd.FileName, "Saved content")
            End If
        End Using
    End Sub

    Private Sub PickColor()
        Using cd As New ColorDialog
            cd.AllowFullOpen = True
            cd.FullOpen = True
            cd.Color = Color.Red

            If cd.ShowDialog() = DialogResult.OK Then
                Me.BackColor = cd.Color
            End If
        End Using
    End Sub

    Private Sub PickFont()
        Using fd As New FontDialog
            fd.ShowColor = True
            fd.Font = Me.Font

            If fd.ShowDialog() = DialogResult.OK Then
                Me.Font = fd.Font
            End If
        End Using
    End Sub

    Private Sub PickFolder()
        Using fbd As New FolderBrowserDialog
            fbd.Description = "Select a folder"
            fbd.SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)

            If fbd.ShowDialog() = DialogResult.OK Then
                MessageBox.Show($"Folder: {fbd.SelectedPath}")
            End If
        End Using
    End Sub
End Class

Data Binding y DataGridView

El data binding conecta controles UI a objetos de datos automáticamente. Simple binding: txt.DataBindings.Add("Text", obj, "PropName"). Para listas, establezca DataGridView.DataSource a un BindingList(Of T) o DataTable. INotifyPropertyChanged habilita binding bidireccional — la UI se actualiza cuando la propiedad cambia (lanze PropertyChanged en el setter). BindingList(Of T) es como ObservableCollection — notifica a la cuadrícula cuando se añaden/eliminan elementos. BindingSource añade navegación y filtrado. AutoGenerateColumns crea columnas desde propiedades. El data binding elimina código de sincronización manual entre UI y datos.

vb
Imports System.Windows.Forms
Imports System.ComponentModel

Public Class User
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private _name As String
    Public Property Name As String
        Get
            Return _name
        End Get
        Set(value As String)
            If _name <> value Then
                _name = value
                RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(Name)))
            End If
        End Set
    End Property

    Public Property Age As Integer
End Class

Public Class DataForm
    Inherits Form

    Public Sub New()
        ' simple binding: TextBox <-> object property
        Dim user As New User With {.Name = "Alice", .Age = 30}
        Dim txt As New TextBox With {.Location = New Point(20, 20)}
        txt.DataBindings.Add("Text", user, "Name")   ' two-way binding

        ' DataGridView with a list
        Dim people As New BindingList(Of User) From {
            New User With {.Name = "Alice", .Age = 30},
            New User With {.Name = "Bob", .Age = 25}
        }

        Dim dgv As New DataGridView With {
            .Location = New Point(20, 60),
            .Size = New Size(350, 200),
            .AutoGenerateColumns = True,
            .AllowUserToAddRows = True,
            .AllowUserToDeleteRows = True,
            .AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill
        }
        dgv.DataSource = people   ' bind to list

        ' BindingSource for navigation
        Dim bs As New BindingSource With {.DataSource = people}
        dgv.DataSource = bs

        ' DataTable binding
        Dim dt As New DataTable()
        dt.Columns.Add("Name", GetType(String))
        dt.Columns.Add("Age", GetType(Integer))
        dt.Rows.Add("Carol", 28)
        dt.Rows.Add("Dave", 35)
        dgv.DataSource = dt

        Me.Controls.Add(txt)
        Me.Controls.Add(dgv)
    End Sub
End Class
10

E/S de archivos en profundidad

StreamReader y StreamWriter

StreamReader/StreamWriter manejan archivos de texto con codificación y disposición apropiadas. El bloque Using asegura que los archivos se cierren incluso en excepciones — siempre úselo. File.ReadAllLines/ReadAllText son convenientes para archivos pequeños; ReadLine en un bucle es eficiente en memoria para archivos grandes. Los métodos async (ReadAllTextAsync) previenen congelamiento de UI durante la E/S. StreamWriter con append:=True añade a archivos existentes. La codificación por defecto es UTF-8; especifique explícitamente con New StreamWriter(path, append, Encoding.UTF8) para formatos heredados.

vb
Imports System.IO

' write text file
Using writer As New StreamWriter("output.txt")
    writer.WriteLine("First line")
    writer.WriteLine("Second line")
    writer.Write("No newline ")
    writer.WriteLine("appended")
End Using  ' automatically disposes/closes

' append to file
Using writer As New StreamWriter("log.txt", append:=True)
    writer.WriteLine($"{Date.Now}: Application started")
End Using

' read all lines
Dim lines() As String = File.ReadAllLines("input.txt")
For Each line In lines
    Console.WriteLine(line)
Next

' read line by line (memory efficient for big files)
Using reader As New StreamReader("big.txt")
    Dim line As String
    Do While reader.Peek() >= 0
        line = reader.ReadLine()
        ' process line
    Loop
End Using

' read all text at once
Dim content As String = File.ReadAllText("input.txt")

' async I/O (non-blocking)
Dim text As String = Await File.ReadAllTextAsync("input.txt")
Await File.WriteAllTextAsync("output.txt", "Hello")

Manejo de archivos binarios y CSV

BinaryWriter/Reader almacenan tipos primitivos en un formato binario compacto — mucho más pequeño que texto y más rápido de parsear. Siempre lea en el mismo orden y tipos que escribió. Para CSV, TextFieldParser maneja campos entre comillas con comas incrustadas correctamente (a diferencia de Split ingenuo). Para CSV de producción, use una librería como CsvHelper (NuGet) que maneja casos límite, mapeo de tipos y streaming. Los archivos binarios son específicos de plataforma (endianness); use BinaryWriter con little-endian para compatibilidad multiplataforma.

vb
Imports System.IO

' binary file with BinaryWriter/Reader
Using fs As New FileStream("data.bin", FileMode.Create),
      bw As New BinaryWriter(fs)
    bw.Write(42)              ' Integer
    bw.Write(3.14)            ' Double
    bw.Write("Hello")         ' String (length-prefixed)
    bw.Write(True)            ' Boolean
End Using

Using fs As New FileStream("data.bin", FileMode.Open),
      br As New BinaryReader(fs)
    Dim num As Integer = br.ReadInt32()
    Dim pi As Double = br.ReadDouble()
    Dim s As String = br.ReadString()
    Dim flag As Boolean = br.ReadBoolean()
End Using

' CSV parsing (manual)
Dim csvLines = File.ReadAllLines("data.csv")
For Each line In csvLines.Skip(1)  ' skip header
    Dim fields = line.Split(","c)
    Dim name = fields(0)
    Dim age = Integer.Parse(fields(1))
Next

' CSV with TextFieldParser (handles quoted fields)
Using parser As New Microsoft.VisualBasic.FileIO.TextFieldParser("data.csv")
    parser.TextFieldType = FileIO.FieldType.Delimited
    parser.SetDelimiters(",")
    While Not parser.EndOfData
        Dim fields() As String = parser.ReadFields()
    End While
End Using

Operaciones del sistema de archivos

File y FileInfo proporcionan operaciones de archivo; Directory y DirectoryInfo manejan carpetas. FileInfo/DirectoryInfo son más eficientes cuando necesita múltiples atributos (una llamada al sistema). EnumerateFiles es perezoso (devuelve uno a uno) vs GetFiles que carga todas las rutas en memoria — use Enumerate para directorios grandes. Path.Combine construye rutas con separadores correctos multiplataforma; nunca concatene cadenas manualmente. SearchOption.AllDirectories puede lanzar en acceso denegado; capture UnauthorizedAccessException o use un helper recursivo.

vb
Imports System.IO

' file operations
File.Copy("source.txt", "dest.txt", overwrite:=True)
File.Move("old.txt", "new.txt")
File.Delete("unwanted.txt")
File.Exists("check.txt")          ' Boolean

' file info
Dim fi As New FileInfo("data.txt")
Console.WriteLine($"Size: {fi.Length} bytes")
Console.WriteLine($"Created: {fi.CreationTime}")
Console.WriteLine($"Modified: {fi.LastWriteTime}")
Console.WriteLine($"Extension: {fi.Extension}")

' directory operations
Directory.CreateDirectory("path	o
ewdir")
Directory.Exists("somedir")
Directory.Delete("dir", recursive:=True)

' enumerate files (lazy, memory-efficient)
For Each path In Directory.EnumerateFiles("C:data", "*.txt", SearchOption.AllDirectories)
    Console.WriteLine(path)
Next

' directory info
Dim di As New DirectoryInfo("C:data")
For Each file In di.GetFiles("*.csv")
    Console.WriteLine($"{file.Name} - {file.Length}")
Next

' path operations
Path.Combine("C:", "data", "file.txt")  ' "C:dataile.txt"
Path.GetExtension("file.txt")           ' ".txt"
Path.GetFileNameWithoutExtension("file.txt")  ' "file"
Path.GetTempFileName()                  ' temp file path

Serialización (JSON y XML)

System.Text.Json (moderno, rápido) es preferido sobre el heredado DataContractJsonSerializer. JsonSerializer.Serialize/Deserialize manejan la mayoría de tipos; use JsonSerializerOptions para formato y políticas de nombres. Para XML, XmlSerializer es simple pero limitado (sin diccionarios, requiere constructor sin parámetros). Para escenarios complejos (polimorfismo, referencias circulares), considere Newtonsoft.Json (Json.NET) via NuGet — más características pero más lento. Siempre maneje errores de deserialización (JsonException) para entrada no confiable.

vb
Imports System.Text.Json

' define a class
Public Class Person
    Public Property Name As String
    Public Property Age As Integer
    Public Property Email As String
End Class

' JSON serialize
Dim p As New Person With {.Name = "Alice", .Age = 30, .Email = "[email protected]"}
Dim json As String = JsonSerializer.Serialize(p)
File.WriteAllText("person.json", json)

' JSON with options
Dim opts As New JsonSerializerOptions With {
    .WriteIndented = True,
    .PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}
json = JsonSerializer.Serialize(p, opts)

' JSON deserialize
Dim text = File.ReadAllText("person.json")
Dim p2 = JsonSerializer.Deserialize(Of Person)(text)

' serialize a list
Dim people = New List(Of Person) From {p, p2}
json = JsonSerializer.Serialize(people)

' XML serialization
Imports System.Xml.Serialization
Dim xs As New XmlSerializer(GetType(Person))
Using fs As New FileStream("person.xml", FileMode.Create)
    xs.Serialize(fs, p)
End Using
Using fs As New FileStream("person.xml", FileMode.Open)
    Dim p3 = CType(xs.Deserialize(fs), Person)
End Using

Operaciones de archivos async y streams

La E/S de archivos async mantiene las UIs responsivas durante operaciones grandes. ReadAsync/WriteAsync trabajan con buffers para reporte de progreso de grano fino. IProgress(Of T) reporta progreso de vuelta al hilo UI de forma segura. MemoryStream es para datos en memoria (sin disco). GZipStream comprime/descomprime streams al vuelo. Para E/S de red, use HttpClient (async). Siempre use bloques Using para asegurar que los streams se cierren. El tamaño de buffer 81920 (80KB) es un buen valor por defecto — equilibra memoria y sobrecarga de syscall.

vb
Imports System.IO

' async copy with progress
Async Function CopyWithProgress(src As String, dest As String, progress As IProgress(Of Long)) As Task
    Const bufferSize As Integer = 81920
    Dim buffer(bufferSize - 1) As Byte
    Using srcStream As FileStream = File.OpenRead(src),
          destStream As FileStream = File.Create(dest)
        Dim read As Integer
        Dim total As Long = 0
        Do
            read = Await srcStream.ReadAsync(buffer, 0, bufferSize)
            If read = 0 Then Exit Do
            Await destStream.WriteAsync(buffer, 0, read)
            total += read
            progress?.Report(total)
        Loop
    End Using
End Function

' usage with progress bar
Dim prog As New Progress(Of Long)(Sub(bytes)
    ProgressBar1.Value = CInt(bytes * 100  totalSize)
End Sub)
Await CopyWithProgress("big.zip", "copy.zip", prog)

' memory stream (in-memory)
Using ms As New MemoryStream()
    Dim bw As New BinaryWriter(ms)
    bw.Write("data")
    ms.Position = 0
    ' read back
End Using

' GZip compression
Imports System.IO.Compression
Using src = File.OpenRead("data.txt"),
      dest = File.Create("data.txt.gz"),
      gz As New GZipStream(dest, CompressionMode.Compress)
    src.CopyTo(gz)
End Using
11

Acceso a base de datos ADO.NET

Conexiones y comandos

Siempre use consultas parametrizadas (Parameters.AddWithValue) para prevenir inyección SQL — nunca concatene entrada de usuario en cadenas SQL. SqlConnection, SqlCommand y SqlDataReader son todos IDisposable; envuélvalos en bloques Using. ExecuteReader devuelve filas; ExecuteNonQuery devuelve filas afectadas (INSERT/UPDATE/DELETE); ExecuteScalar devuelve un único valor (COUNT, SUM). Para otras bases de datos, use el proveedor apropiado (OracleConnection, OleDbConnection, OdbcConnection) o el DbProviderFactory genérico.

vb
Imports System.Data.SqlClient  ' or Microsoft.Data.SqlClient

' connection string (SQL Server)
Dim connStr = "Server=localhost;Database=MyDb;Integrated Security=True;"

' basic query
Using conn As New SqlConnection(connStr)
    conn.Open()
    Using cmd As New SqlCommand("SELECT Id, Name FROM Users WHERE Age > @age", conn)
        cmd.Parameters.AddWithValue("@age", 25)
        Using reader As SqlDataReader = cmd.ExecuteReader()
            While reader.Read()
                Dim id = reader.GetInt32(0)
                Dim name = reader.GetString(1)
                Console.WriteLine($"{id}: {name}")
            End While
        End Using
    End Using
End Using  ' connection auto-closed

' insert with parameters (prevent SQL injection!)
Using conn As New SqlConnection(connStr), _
      cmd As New SqlCommand("INSERT INTO Users (Name, Age) VALUES (@name, @age)", conn)
    cmd.Parameters.AddWithValue("@name", "Bob")
    cmd.Parameters.AddWithValue("@age", 30)
    conn.Open()
    Dim rowsAffected = cmd.ExecuteNonQuery()
End Using

' scalar query (single value)
Using conn As New SqlConnection(connStr), _
      cmd As New SqlCommand("SELECT COUNT(*) FROM Users", conn)
    conn.Open()
    Dim count = CInt(cmd.ExecuteScalar())
End Using

DataSets y DataTables

DataTable/DataSet son contenedores de datos desconectados — cargue una vez, trabaje offline, actualice después. SqlDataAdapter.Fill carga datos; SqlCommandBuilder auto-genera comandos INSERT/UPDATE/DELETE para escenarios simples de tabla única. Los DataSets tipados (via designer .xsd) proporcionan comprobación de tipos en tiempo de compilación e IntelliSense. DataView filtra y ordena sin modificar el DataTable subyacente. Para aplicaciones modernas, prefiera Entity Framework o Dapper sobre DataSets en bruto — son más mantenibles y testeables. Los DataSets siguen siendo útiles para reportes e interop heredado.

vb
Imports System.Data.SqlClient

' fill DataTable
Dim dt As New DataTable()
Using adapter As New SqlDataAdapter("SELECT * FROM Products", connStr)
    adapter.Fill(dt)
End Using

' access rows
For Each row As DataRow In dt.Rows
    Console.WriteLine(row("Name"))
    Console.WriteLine(row.Field(Of Integer)("Price"))
Next

' modify and update
Dim newRow = dt.NewRow()
newRow("Name") = "Widget"
newRow("Price") = 9.99
dt.Rows.Add(newRow)

Using adapter As New SqlDataAdapter("SELECT * FROM Products", connStr),
      builder As New SqlCommandBuilder(adapter)
    adapter.Update(dt)  ' generates INSERT/UPDATE/DELETE
End Using

' typed DataSet (strongly typed)
' add .xsd file to project, drag tables from Server Explorer
' Dim ta As New ProductsTableAdapter()
' Dim products = ta.GetData()
' For Each p In products
'     Console.WriteLine(p.ProductName)
' Next

' filter and sort
Dim view As New DataView(dt)
view.RowFilter = "Price > 10"
view.Sort = "Price DESC"
For Each row As DataRowView In view
    Console.WriteLine(row("Name"))
Next

Transacciones y connection pooling

Las transacciones aseguran atomicidad — todas las operaciones tienen éxito o todas fallan. SqlTransaction envuelve múltiples comandos en una conexión. TransactionScope habilita transacciones distribuidas a través de múltiples conexiones/recursos (usa MS DTC si es necesario) — llame scope.Complete() para confirmar. El connection pooling es automático en ADO.NET: las conexiones se reutilizan en lugar de recrearse, mejorando dramáticamente el rendimiento. Siempre Close/Dispose conexiones (bloques Using) para devolverlas al pool. Configure el tamaño del pool y tiempo de vida en la cadena de conexión para apps de alto rendimiento.

vb
Imports System.Data.SqlClient

' explicit transaction
Using conn As New SqlConnection(connStr)
    conn.Open()
    Using tran As SqlTransaction = conn.BeginTransaction()
        Try
            Using cmd1 As New SqlCommand(
                "UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1",
                conn, tran)
                cmd1.ExecuteNonQuery()
            End Using
            Using cmd2 As New SqlCommand(
                "UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2",
                conn, tran)
                cmd2.ExecuteNonQuery()
            End Using
            tran.Commit()
        Catch ex As Exception
            tran.Rollback()
            Throw
        End Try
    End Using
End Using

' transaction scope (distributed, multi-resource)
Imports System.Transactions
Using scope As New TransactionScope()
    ' multiple connections, even different databases
    Using conn1 As New SqlConnection(connStr1)
        ' operations on DB1
    End Using
    Using conn2 As New SqlConnection(connStr2)
        ' operations on DB2
    End Using
    scope.Complete()  ' commit all
End Using  ' rollback if Complete not called

' connection pooling (automatic, configure in connection string)
' "Server=...;Pooling=True;Max Pool Size=100;Connection Lifetime=300"
' connections are reused — always Close/Dispose to return to pool

Operaciones de base de datos async

Las operaciones de base de datos async (OpenAsync, ExecuteReaderAsync, ReadAsync) previenen congelamiento de UI durante consultas largas. El patrón refleja ADO.NET síncrono pero con Await. Siempre use Async en aplicaciones UI para mantenerlas responsivas. Para servidores de alto rendimiento, las llamadas async a BD liberan hilos para manejar otras peticiones. MARS (Multiple Active Result Sets) permite múltiples lectores en una conexión — habilite con 'MultipleActiveResultSets=True' en la cadena de conexión. Maneje SqlException para errores específicos de base de datos (deadlocks, violaciones de restricciones).

vb
Imports System.Data.SqlClient

' async query
Async Function GetUsersAsync(minAge As Integer) As Task(Of List(Of User))
    Dim users As New List(Of User)()
    Using conn As New SqlConnection(connStr)
        Await conn.OpenAsync()
        Using cmd As New SqlCommand("SELECT Id, Name, Age FROM Users WHERE Age > @age", conn)
            cmd.Parameters.AddWithValue("@age", minAge)
            Using reader = Await cmd.ExecuteReaderAsync()
                While Await reader.ReadAsync()
                    users.Add(New User With {
                        .Id = reader.GetInt32(0),
                        .Name = reader.GetString(1),
                        .Age = reader.GetInt32(2)
                    })
                End While
            End Using
        End Using
    End Using
    Return users
End Function

' async execute (INSERT/UPDATE)
Async Function UpdateUserAsync(user As User) As Task(Of Integer)
    Using conn As New SqlConnection(connStr)
        Await conn.OpenAsync()
        Using cmd As New SqlCommand(
            "UPDATE Users SET Name=@name, Age=@age WHERE Id=@id", conn)
            cmd.Parameters.AddWithValue("@name", user.Name)
            cmd.Parameters.AddWithValue("@age", user.Age)
            cmd.Parameters.AddWithValue("@id", user.Id)
            Return Await cmd.ExecuteNonQueryAsync()
        End Using
    End Using
End Function

' usage in event handler
Private Async Sub btnLoad_Click(sender As Object, e As EventArgs) Handles btnLoad.Click
    btnLoad.Enabled = False
    Try
        Dim users = Await GetUsersAsync(25)
        DataGridView1.DataSource = users
    Catch ex As Exception
        MessageBox.Show(ex.Message)
    Finally
        btnLoad.Enabled = True
    End Try
End Sub

Dapper Micro-ORM

Dapper es un micro-ORM que extiende IDbConnection con métodos de extensión — rápido (casi velocidad ADO.NET en bruto) y simple. Query<T> mapea filas a objetos automáticamente coincidiendo nombres de columna con propiedades. Los objetos anónimos proporcionan parámetros (seguros contra inyección SQL). La inserción bulk pasa una lista y Dapper ejecuta una vez por elemento. El multi-mapping maneja joins dividiendo filas en una columna. Dapper es ideal cuando quiere control SQL con menos boilerplate que ADO.NET en bruto. Para grafos de objetos complejos y change tracking, use Entity Framework.

vb
Imports Dapper  ' NuGet: Install-Package Dapper
Imports System.Data.SqlClient

' simple query
Using conn As New SqlConnection(connStr)
    Dim users = conn.Query(Of User)("SELECT * FROM Users WHERE Age > @age",
                                     New With { .age = 25 })
    For Each u In users
        Console.WriteLine(u.Name)
    Next
End Using

' single row
Using conn As New SqlConnection(connStr)
    Dim user = conn.QuerySingleOrDefault(Of User)(
        "SELECT * FROM Users WHERE Id = @id", New With { .id = 42 })
End Using

' insert
Using conn As New SqlConnection(connStr)
    Dim sql = "INSERT INTO Users (Name, Age, Email) VALUES (@Name, @Age, @Email)"
    conn.Execute(sql, New With { .Name = "Alice", .Age = 30, .Email = "[email protected]" })
End Using

' bulk insert
Using conn As New SqlConnection(connStr)
    Dim users = New List(Of User) From {
        New User With {.Name = "A", .Age = 1},
        New User With {.Name = "B", .Age = 2}
    }
    conn.Execute("INSERT INTO Users (Name, Age) VALUES (@Name, @Age)", users)
End Using

' multi-mapping (join)
Dim sql = "SELECT * FROM Orders o INNER JOIN Users u ON o.UserId = u.Id"
Dim orders = conn.Query(Of Order, User, Order)(sql,
    Function(o, u)
        o.User = u
        Return o
    End Function, splitOn:="Id")

' stored procedure
Dim result = conn.Query(Of User)("sp_GetActiveUsers",
    commandType:=Data.CommandType.StoredProcedure)
12

LINQ to Objects

Sintaxis de consulta y método

LINQ proporciona dos sintaxis: consulta (tipo SQL, From...Where...Select) y método (fluida, .Where().Select()). Ambas compilan al mismo IL — elija basándose en legibilidad. La sintaxis de consulta soporta menos operadores (sin Sum, Count directamente); use sintaxis de método para esos. LINQ usa ejecución diferida — la consulta no se ejecuta hasta que itera (For Each) o llama un operador materializador (ToList, Count). Esto significa que modificar el source tras definir la consulta afecta los resultados. Use .ToList() para forzar ejecución inmediata y snapshot los datos.

vb
Dim numbers = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

' query syntax (SQL-like)
Dim evens = From n In numbers
            Where n Mod 2 = 0
            Select n
            Order By n Descending

' method syntax (fluent)
Dim evens2 = numbers.Where(Function(n) n Mod 2 = 0).
                     OrderByDescending(Function(n) n)

' both produce the same result
For Each n In evens
    Console.WriteLine(n)  ' 10, 8, 6, 4, 2
Next

' projection (transform)
Dim squares = numbers.Select(Function(n) n * n)
Dim names = numbers.Select(Function(n) $"Number {n}")

' filtering with multiple conditions
Dim filtered = From n In numbers
               Where n > 3 AndAlso n < 8
               Select n

' type inference (Dim is required)
Dim result = From p In people
             Where p.Age >= 18
             Select p.Name, p.Age
             Order By Age

' deferred execution (query runs when iterated)
Dim query = numbers.Where(Function(n) n > 5)  ' not executed yet
numbers.Add(11)  ' query will include 11!
For Each n In query  ' executes now
Next

Agregación y agrupación

Los operadores de agregación LINQ (Sum, Average, Min, Max, Count, Any, All) colapsan secuencias a valores únicos. GroupBy particiona elementos por una clave; cada grupo tiene un .Key y es enumerable por sí mismo. La sintaxis de consulta 'Group By...Into Group' es específica de VB. Múltiples agregados por grupo son comunes para reportes. Any/All son de cortocircuito (paran temprano) — útiles para comprobar condiciones sin iteración completa. Las funciones agregadas lanzan en secuencias vacías; use las variantes nullable (Sum devuelve 0, Average lanza) o DefaultIfEmpty para manejar esto.

vb
Dim people = New List(Of Person) From {
    New Person With {.Name = "Al", .Dept = "Eng", .Salary = 90000},
    New Person With {.Name = "Bo", .Dept = "Eng", .Salary = 70000},
    New Person With {.Name = "Cy", .Dept = "Sales", .Salary = 60000}
}

' aggregation
Dim total = people.Sum(Function(p) p.Salary)
Dim avg = people.Average(Function(p) p.Salary)
Dim max = people.Max(Function(p) p.Salary)
Dim count = people.Count(Function(p) p.Dept = "Eng")
Dim any = people.Any(Function(p) p.Salary > 80000)
Dim all = people.All(Function(p) p.Salary > 0)

' grouping
Dim byDept = From p In people
             Group p By p.Dept Into Group
             Select Dept, Count = Group.Count(), Avg = Group.Average(Function(x) x.Salary)

For Each g In byDept
    Console.WriteLine($"{g.Dept}: {g.Count} people, avg ${g.Avg}")
Next

' method syntax grouping
Dim grouped = people.GroupBy(Function(p) p.Dept)
For Each g In grouped
    Console.WriteLine($"{g.Key}: {g.Count()} people")
Next

' group with multiple aggregates
Dim stats = people.GroupBy(Function(p) p.Dept).
    Select(Function(g) New With {
        .Dept = g.Key,
        .Count = g.Count(),
        .Total = g.Sum(Function(p) p.Salary),
        .Min = g.Min(Function(p) p.Salary)
    })

Joining, Zip y operaciones de conjuntos

Join realiza inner joins (claves coincidentes); Group Join con DefaultIfEmpty crea left joins. Zip empareja elementos posición por posición. Las operaciones de conjuntos (Union, Intersect, Except, Distinct) usan comparadores de igualdad por defecto — sobrescriba Equals/GetHashCode o pase un IEqualityComparer personalizado para tipos complejos. Concat añade sin eliminar duplicados (a diferencia de Union). Todas estas son perezosas excepto cuando se materializan. Para datasets grandes, considere usar un HashSet o Dictionary para búsquedas O(1) en lugar de los bucles anidados O(n*m) de Join.

vb
Dim employees = New List(Of Employee) From {
    New Employee With {.Id = 1, .Name = "Al", .DeptId = 10},
    New Employee With {.Id = 2, .Name = "Bo", .DeptId = 20}
}
Dim departments = New List(Of Department) From {
    New Department With {.Id = 10, .Name = "Eng"},
    New Department With {.Id = 20, .Name = "Sales"},
    New Department With {.Id = 30, .Name = "HR"}
}

' inner join
Dim result = From e In employees
             Join d In departments On e.DeptId Equals d.Id
             Select e.Name, Dept = d.Name

' group join (left join with grouped results)
Dim leftJoin = From d In departments
               Group Join e In employees On d.Id Equals e.DeptId Into Group
               From e In Group.DefaultIfEmpty()
               Select Dept = d.Name, Emp = If(e Is Nothing, "Vacant", e.Name)

' method syntax join
Dim joined = employees.Join(departments,
    Function(e) e.DeptId,
    Function(d) d.Id,
    Function(e, d) New With { .Name = e.Name, .Dept = d.Name })

' zip (pair elements)
Dim nums = {1, 2, 3}
Dim words = {"one", "two", "three"}
Dim pairs = nums.Zip(words, Function(n, w) $"{n}={w}")

' set operations
Dim a = {1, 2, 3, 4}
Dim b = {3, 4, 5, 6}
Dim union = a.Union(b)        ' 1,2,3,4,5,6
Dim intersect = a.Intersect(b) ' 3,4
Dim except = a.Except(b)       ' 1,2
Dim concat = a.Concat(b)       ' 1,2,3,4,3,4,5,6
Dim distinct = concat.Distinct() ' 1,2,3,4,5,6

Ordenación, paginación y operaciones de elemento

OrderBy/ThenBy ordenan (estable); OrderByDescending/ThenByDescending ordenan descendente. Múltiples claves de ordenación usan ThenBy tras OrderBy. La paginación usa Skip (offset) y Take (limit) — esencial para datasets grandes en UIs. First/Last lanzan en vacío; FirstOrDefault devuelve el valor por defecto (Nothing para tipos referencia, 0 para números). Single exige exactamente una coincidencia (lanza en caso contrario) — útil para validación. TakeWhile/SkipWhile particionan basándose en un predicado hasta que falla. Todas estas son perezosas excepto First/Last/Single que fuerzan iteración.

vb
Dim nums = {5, 2, 8, 1, 9, 3, 7, 4, 6}

' sorting
Dim asc = nums.OrderBy(Function(n) n)
Dim desc = nums.OrderByDescending(Function(n) n)
Dim multi = people.OrderBy(Function(p) p.Dept).
                  ThenBy(Function(p) p.Name)
Dim multiDesc = people.OrderByDescending(Function(p) p.Dept).
                       ThenByDescending(Function(p) p.Salary)

' reverse
Dim reversed = nums.Reverse()

' paging (skip + take)
Dim page = nums.Skip(5).Take(3)   ' elements 6,7,8 (0-indexed)
Dim firstPage = nums.Take(5)       ' first 5

' element operations
Dim first = nums.First()           ' throws if empty
Dim firstOr = nums.FirstOrDefault() ' default (0) if empty
Dim last = nums.Last()
Dim single = nums.Single(Function(n) n = 5)  ' throws if not exactly one
Dim singleOr = nums.SingleOrDefault(Function(n) n = 99) ' 0 if not found

' element at position
Dim third = nums.ElementAt(2)       ' throws if out of range
Dim thirdOr = nums.ElementAtOrDefault(99)  ' 0 if out of range

' find index (not LINQ, but related)
Dim idx = Array.FindIndex(nums, Function(n) n > 5)

' partition
Dim large = nums.Where(Function(n) n > 5)
Dim small = nums.TakeWhile(Function(n) n < 8)  ' stops at first false
Dim skipSmall = nums.SkipWhile(Function(n) n < 8)

LINQ personalizado y árboles de expresión

Los métodos de extensión (decorados con <Extension()>) añaden operadores tipo LINQ a cualquier tipo. Las agregaciones personalizadas (Median, Mode, StandardDeviation) llenan huecos en los operadores integrados. IEnumerable(Of T) usa delegates (en memoria); IQueryable(Of T) usa árboles de expresión que se traducen a SQL (EF/LINQ to SQL) — mezclarlos puede causar evaluación del lado cliente (lenta). ToList/ToArray/ToDictionary fuerzan evaluación y snapshot resultados. PLINQ (.AsParallel) paraleliza consultas — use para operaciones CPU-bound en colecciones grandes, pero cuidado con ordenación y thread safety.

vb
' custom extension method
Imports System.Runtime.CompilerServices

<Extension()>
Public Function WhereNot(Of T)(source As IEnumerable(Of T),
                                predicate As Func(Of T, Boolean)) As IEnumerable(Of T)
    Return source.Where(Function(x) Not predicate(x))
End Function

' usage
Dim odds = numbers.WhereNot(Function(n) n Mod 2 = 0)

' custom aggregation
<Extension()>
Public Function Median(source As IEnumerable(Of Double)) As Double
    Dim sorted = source.OrderBy(Function(x) x).ToList()
    Dim count = sorted.Count
    If count = 0 Then Return 0
    If count Mod 2 = 0 Then
        Return (sorted(count  2 - 1) + sorted(count  2)) / 2
    Else
        Return sorted(count  2)
    End If
End Function

' usage
Dim med = numbers.Median()

' IEnumerable vs IQueryable
' IEnumerable: LINQ to Objects (in-memory, delegates)
' IQueryable: LINQ to SQL/EF (translates to SQL, expression trees)

' force evaluation
Dim list = query.ToList()      ' List(Of T)
Dim array = query.ToArray()    ' T()
Dim dict = query.ToDictionary(Function(x) x.Id)
Dim lookup = query.ToLookup(Function(x) x.Category)

' PLINQ (parallel)
Dim parallel = nums.AsParallel().
    Where(Function(n) IsPrime(n)).
    Select(Function(n) n * 2).
    ToList()
13

Multithreading y async

Tasks y Async/Await

Async/Await es la forma moderna de escribir código asincrónico — parece síncrono pero no bloquea hilos. Marque métodos Async y use Await en llamadas que devuelven Task. Los manejadores de eventos UI pueden ser Async Sub (el único lugar donde Async Sub es aceptable). Task.Run descarga trabajo CPU-bound al thread pool. Task.WhenAll espera a todas las tareas (paralelo); Task.WhenAny espera a la primera en completar. El compilador genera una máquina de estados que maneja continuaciones, propagación de excepciones y captura de contexto. Siempre prefiera Async/Await sobre threading manual o callbacks.

vb
Imports System.Threading.Tasks

' async function
Async Function DownloadAsync(url As String) As Task(Of String)
    Using client As New Net.Http.HttpClient()
        Return Await client.GetStringAsync(url)
    End Function
End Function

' call async from event handler
Private Async Sub btnDownload_Click(sender As Object, e As EventArgs) Handles btnDownload.Click
    btnDownload.Enabled = False
    Try
        Dim content = Await DownloadAsync("https://example.com")
        txtResult.Text = content
    Catch ex As Exception
        MessageBox.Show(ex.Message)
    Finally
        btnDownload.Enabled = True
    End Try
End Sub

' run CPU-bound work on background thread
Async Function ProcessDataAsync(data As Byte()) As Task(Of Result)
    Return Await Task.Run(Function()
        ' CPU-intensive work
        Return Analyze(data)
    End Function)
End Function

' Task.Run overloads
Dim t1 = Task.Run(Function() Compute(1))
Dim t2 = Task.Run(Async Function() Await FetchAsync())

' wait for multiple tasks
Dim tasks = New List(Of Task) From {t1, t2, t3}
Await Task.WhenAll(tasks)

' wait for any
Dim firstDone = Await Task.WhenAny(t1, t2, t3)

' task continuation
Dim result = Await t1.ContinueWith(Function(t) t.Result * 2)

Thread safety y sincronización

SyncLock (VB) / lock (C#) proporciona exclusión mutua — solo un hilo entra al bloque a la vez. Bloquee en un Object privado, nunca en Me o una cadena. Interlocked proporciona operaciones atómicas sin locks (más rápido para casos simples). Mutex funciona entre procesos (apps de instancia única). SemaphoreSlim limita acceso concurrente (p. ej., máximo 3 llamadas API a la vez). Siempre libere locks/semáforos en bloques Finally. Los deadlocks ocurren cuando dos hilos se esperan mutuamente — adquiera locks en orden consistente. Use ConcurrentDictionary, ConcurrentQueue para colecciones thread-safe sin locks.

vb
Imports System.Threading

' lock (mutual exclusion)
Private ReadOnly _lock As New Object
Private _counter As Integer = 0

Sub Increment()
    SyncLock _lock
        _counter += 1
    End SyncLock
End Sub

' Monitor (equivalent to SyncLock)
Monitor.Enter(_lock)
Try
    _counter += 1
Finally
    Monitor.Exit(_lock)
End Try

' Interlocked (atomic operations, no lock needed)
Interlocked.Increment(_counter)
Interlocked.Add(_counter, 10)
Interlocked.Exchange(_counter, 0)
Interlocked.CompareExchange(_counter, 1, 0)  ' if 0, set to 1

' Mutex (cross-process)
Dim mutex As New Mutex(False, "GlobalMyAppMutex")
mutex.WaitOne()
Try
    ' critical section
Finally
    mutex.ReleaseMutex()
End Try

' SemaphoreSlim (limit concurrent access)
Dim sem As New SemaphoreSlim(3)  ' max 3 concurrent
Await sem.WaitAsync()
Try
    ' limited concurrency
Finally
    sem.Release()
End Try

' CountdownEvent
Dim cde As New CountdownEvent(5)
Parallel.For(0, 5, Sub(i)
    ' work
    cde.Signal()
End Sub)
cde.Wait()

Parallel y BackgroundWorker

Parallel.For/ForEach particionan trabajo entre hilos del thread pool — ideal para bucles CPU-bound. PLINQ (.AsParallel) paraleliza consultas LINQ. Use ParallelOptions.MaxDegreeOfParallelism para limitar hilos. CancellationToken habilita cancelación cooperativa. BackgroundWorker es heredado pero conveniente para WinForms — serializa ProgressChanged y RunWorkerCompleted al hilo UI automáticamente. Para código nuevo, prefiera Task.Run + IProgress(Of T) + Async/Await. Los bucles paralelos son bloqueantes (a diferencia de Async); no los use en hilos UI.

vb
Imports System.Threading.Tasks

' Parallel.For (CPU-bound parallelism)
Parallel.For(0, 1000, Sub(i)
    Process(i)
End Sub)

' Parallel.ForEach
Parallel.ForEach(files, Sub(f)
    Compress(f)
End Sub)

' with options
Dim opts As New ParallelOptions With {.MaxDegreeOfParallelism = 4}
Parallel.For(0, 1000, opts, Sub(i) Process(i))

' PLINQ
Dim results = numbers.AsParallel().
    Where(Function(n) IsPrime(n)).
    Select(Function(n) n * 2).
    ToArray()

' cancel parallel work
Dim cts As New CancellationTokenSource()
Parallel.For(0, 1000,
    New ParallelOptions With {.CancellationToken = cts.Token},
    Sub(i, state)
        If SomeCondition Then state.Break()
        Process(i)
    End Sub)

' BackgroundWorker (legacy, but simple for WinForms)
Dim bgw As New BackgroundWorker With {.WorkerReportsProgress = True, .WorkerSupportsCancellation = True}
AddHandler bgw.DoWork, Sub(sender, e)
    For i = 1 To 100
        If bgw.CancellationPending Then
            e.Cancel = True
            Return
        End If
        bgw.ReportProgress(i)
        Thread.Sleep(50)
    Next
End Sub
AddHandler bgw.ProgressChanged, Sub(sender, e)
    ProgressBar1.Value = e.ProgressPercentage
End Sub
AddHandler bgw.RunWorkerCompleted, Sub(sender, e)
    MessageBox.Show("Done!")
End Sub
bgw.RunWorkerAsync()

Timers y marshaling al hilo UI

Hay tres Timers en .NET: Windows.Forms.Timer (hilo UI, más simple para formularios), System.Threading.Timer (thread pool, ligero) y System.Timers.Timer (componente, escenarios de servidor). Solo Forms.Timer puede actualizar UI directamente; otros requieren Invoke. Control.Invoke serializa un delegate al hilo UI — compruebe InvokeRequired primero. ConfigureAwait(False) mejora el rendimiento en código de librería (sin captura de contexto) pero previene acceso UI después. SynchronizationContext.Post es una forma más general de serializar a un contexto específico (UI, ASP.NET).

vb
' Windows.Forms.Timer (UI thread, simple)
Dim timer1 As New Windows.Forms.Timer With {.Interval = 1000}
AddHandler timer1.Tick, Sub(sender, e)
    Label1.Text = Date.Now.ToString()
End Sub
timer1.Start()

' System.Threading.Timer (thread pool, not UI thread)
Dim timer2 As New Threading.Timer(
    Sub(state)
        ' runs on thread pool thread!
        ' must marshal to UI thread to update controls:
        Me.Invoke(Sub() Label1.Text = Date.Now.ToString())
    End Sub,
    Nothing, 0, 1000)

' System.Timers.Timer (component, fires on thread pool)
Dim timer3 As New Timers.Timer With {.Interval = 1000, .AutoReset = True}
AddHandler timer3.Elapsed, Sub(sender, e)
    Me.Invoke(Sub() Label1.Text = Date.Now.ToString())
End Sub
timer3.Start()

' UI thread marshaling
If Label1.InvokeRequired Then
    Label1.Invoke(Sub() Label1.Text = "updated")
Else
    Label1.Text = "updated"
End If

' async with ConfigureAwait
Await SomeAsync().ConfigureAwait(True)   ' capture context (default)
Await SomeAsync().ConfigureAwait(False)  ' don't capture (faster, but no UI access)

' SynchronizationContext for custom marshaling
Dim uiContext = SynchronizationContext.Current
uiContext.Post(Sub() Label1.Text = "done", Nothing)

Colecciones concurrentes y Channels

Las colecciones concurrentes (ConcurrentDictionary, Queue, Stack, Bag) son alternativas thread-safe a colecciones regulares — úselas en lugar de bloquear. AddOrUpdate/GetOrAdd en ConcurrentDictionary son operaciones compuestas atómicas. BlockingCollection implementa el patrón productor-consumidor con Add/Take bloqueante — ideal para colas de trabajo. Channels (System.Threading.Channels) son la alternativa moderna y async-friendly con backpressure. Para thread safety en UI, use Invoke/BeginInvoke en lugar de colecciones concurrentes. Siempre prefiera estas primitivas integradas sobre locking manual — están testeadas y optimizadas.

vb
Imports System.Collections.Concurrent

' ConcurrentDictionary (thread-safe dictionary)
Dim dict As New ConcurrentDictionary(Of String, Integer)
dict.TryAdd("a", 1)
dict.AddOrUpdate("a", 1, Function(key, old) old + 1)  ' atomic
Dim val = dict.GetOrAdd("b", Function(key) ComputeValue(key))

' ConcurrentQueue (FIFO, thread-safe)
Dim queue As New ConcurrentQueue(Of String)
queue.Enqueue("first")
Dim item As String
If queue.TryDequeue(item) Then
    Console.WriteLine(item)
End If

' ConcurrentStack (LIFO)
Dim stack As New ConcurrentStack(Of Integer)
stack.Push(1)
Dim popped As Integer
stack.TryPop(popped)

' ConcurrentBag (unordered, fast for producer=consumer)
Dim bag As New ConcurrentBag(Of Integer)
bag.Add(42)

' BlockingCollection (producer-consumer pattern)
Dim bc As New BlockingCollection(Of Integer)(boundedCapacity:=100)
' producer
Task.Run(Sub()
    For i = 1 To 1000
        bc.Add(i)  ' blocks if full
    Next
    bc.CompleteAdding()
End Sub)
' consumer
Task.Run(Sub()
    For Each x In bc.GetConsumingEnumerable()  ' blocks if empty
        Process(x)
    Next
End Sub)

' Channels (modern, high-performance)
' NuGet: System.Threading.Channels
Dim channel = System.Threading.Channels.Channel.CreateBounded(Of String)(100)
Await channel.Writer.WriteAsync("message")
Dim msg = Await channel.Reader.ReadAsync()
14

Registro y API de Windows

Lectura y escritura del Registro

El Registro de Windows almacena configuraciones de aplicación y configuración del sistema. Use Microsoft.Win32.Registry para acceso. HKEY_CURRENT_USER (HKCU) es por usuario (sin admin); HKEY_LOCAL_MACHINE (HKLM) es para todo el sistema (requiere admin). Siempre proporcione un valor por defecto al leer (devuelve Nothing si falta). Use RegistryValueKind para especificar tipo (String, DWord, Binary). La clave Run auto-inicia apps al iniciar sesión. Envuelva RegistryKey en Using para cerrar handles. Sea cauteloso al editar el registro — los errores pueden romper Windows.

vb
Imports Microsoft.Win32

' read a value
Dim value As String = Registry.GetValue(
    "HKEY_CURRENT_USERSoftwareMyApp",
    "Setting1",
    "default")  ' default if not found
Console.WriteLine(value)

' write a value
Registry.SetValue(
    "HKEY_CURRENT_USERSoftwareMyApp",
    "Setting1",
    "my value")

' using RegistryKey (more control)
Using key As RegistryKey = Registry.CurrentUser.OpenSubKey("SoftwareMyApp", writable:=True)
    If key IsNot Nothing Then
        Dim setting = key.GetValue("Setting1")
        key.SetValue("Setting2", 42, RegistryValueKind.DWord)
        key.DeleteValue("Setting2", throwOnMissingValue:=False)
    End If
End Using

' create subkey
Using key As RegistryKey = Registry.CurrentUser.CreateSubKey("SoftwareMyAppSub")
    key.SetValue("Name", "Test")
End Using

' enumerate subkeys and values
Using key As RegistryKey = Registry.LocalMachine.OpenSubKey("SOFTWARE")
    For Each subkeyName In key.GetSubKeyNames()
        Console.WriteLine(subkeyName)
    Next
    For Each valueName In key.GetValueNames()
        Console.WriteLine($"{valueName}: {key.GetValue(valueName)}")
    Next
End Using

' run on startup (add to Run key)
Registry.SetValue("HKEY_CURRENT_USERSoftwareMicrosoftWindowsCurrentVersionRun",
    "MyApp", Application.ExecutablePath)

P/Invoke: llamando a la API de Windows

P/Invoke (Platform Invoke) permite a VB llamar a funciones de la API de Windows no gestionada via DllImport. Declare la firma de función coincidiendo con la API de C; el runtime serializa tipos automáticamente (String ↔ LPSTR, Integer ↔ DWORD). Use ByRef para parámetros de salida y StructLayout para estructuras pasadas por referencia. CharSet.Auto selecciona ANSI o Unicode basándose en el SO. Librerías comunes: user32 (ventanas, mensajes), kernel32 (sistema, archivos), gdi32 (gráficos). Siempre envuelva P/Invoke en una clase NativeMethods. Pinvoke.net es un gran recurso para firmas.

vb
Imports System.Runtime.InteropServices

Public Class NativeMethods
    ' MessageBox from user32.dll
    <DllImport("user32.dll", CharSet:=CharSet.Auto)>
    Public Shared Function MessageBox(hWnd As IntPtr, text As String,
        caption As String, type As Integer) As Integer
    End Function

    ' GetSystemMetrics
    <DllImport("user32.dll")>
    Public Shared Function GetSystemMetrics(nIndex As Integer) As Integer
    End Function

    ' Beep
    <DllImport("kernel32.dll")>
    Public Shared Function Beep(frequency As Integer, duration As Integer) As Boolean
    End Function

    ' SendMessage (for custom control messages)
    <DllImport("user32.dll", CharSet:=CharSet.Auto)>
    Public Shared Function SendMessage(hWnd As IntPtr, msg As Integer,
        wParam As IntPtr, lParam As IntPtr) As IntPtr
    End Function

    ' structures
    <StructLayout(LayoutKind.Sequential)>
    Public Structure POINT
        Public X As Integer
        Public Y As Integer
    End Structure

    <DllImport("user32.dll")>
    Public Shared Function GetCursorPos(ByRef lpPoint As POINT) As Boolean
    End Function
End Class

' usage
NativeMethods.MessageBox(IntPtr.Zero, "Hello from API!", "Test", 0)
NativeMethods.Beep(440, 500)  ' 440 Hz for 500ms

Dim p As NativeMethods.POINT
NativeMethods.GetCursorPos(p)
Console.WriteLine($"Cursor at {p.X}, {p.Y}")

' screen dimensions
Dim width = NativeMethods.GetSystemMetrics(0)   ' SM_CXSCREEN
Dim height = NativeMethods.GetSystemMetrics(1)  ' SM_CYSCREEN

Operaciones de proceso y shell

Process.Start lanza programas externos. UseShellExecute=False con RedirectStandardOutput le permite capturar salida programáticamente — esencial para herramientas de línea de comandos. Siempre WaitForExit y lea salida para evitar deadlocks (el buffer de salida puede llenarse y bloquear el proceso). GetProcesses enumera procesos en ejecución; GetProcessesByName encuentra específicos. Verb='runas' eleva a admin (dispara UAC). Para procesos de larga ejecución, suscríbase al evento Exited o use patrones async. Sea cauteloso con rutas de archivo suministradas por el usuario — valide para prevenir inyección de comandos.

vb
Imports System.Diagnostics

' start a process
Process.Start("notepad.exe")
Process.Start("notepad.exe", "file.txt")
Process.Start("https://example.com")  ' opens default browser
Process.Start("mailto:[email protected]")

' with ProcessStartInfo (more control)
Dim psi As New ProcessStartInfo With {
    .FileName = "cmd.exe",
    .Arguments = "/c dir",
    .UseShellExecute = False,
    .RedirectStandardOutput = True,
    .CreateNoWindow = True
}
Using p As Process = Process.Start(psi)
    Dim output = p.StandardOutput.ReadToEnd()
    p.WaitForExit()
    Console.WriteLine(output)
End Using

' async read output
Using p As Process = Process.Start(psi)
    Dim output = Await p.StandardOutput.ReadToEndAsync()
    Await Task.Run(Sub() p.WaitForExit())
End Using

' get running processes
For Each proc In Process.GetProcesses()
    Console.WriteLine($"{proc.ProcessName} (PID {proc.Id})")
Next

' find specific process
Dim excel = Process.GetProcessesByName("EXCEL").FirstOrDefault()
If excel IsNot Nothing Then
    Console.WriteLine($"Excel memory: {excel.WorkingSet64  1024} KB")
End If

' run as admin
psi.Verb = "runas"  ' triggers UAC prompt
Process.Start(psi)

Entorno e información del sistema

Environment proporciona información del sistema y usuario. GetFolderPath con SpecialFolder es la forma correcta de encontrar directorios estándar (AppData, MyDocuments, Temp) — nunca hardcodee rutas. Las variables de entorno configuran comportamiento entre máquinas. GetCommandLineArgs incluye el ejecutable como primer elemento. Para información detallada de hardware, use WMI (System.Management) — consulte clases como Win32_Processor, Win32_LogicalDisk. Screen.AllScreens (Windows Forms) da información de monitores para configuraciones multi-pantalla. Siempre use estas APIs en lugar de adivinar rutas o configuraciones.

vb
' environment variables
Dim path = Environment.GetEnvironmentVariable("PATH")
Environment.SetEnvironmentVariable("MY_VAR", "value")
Dim allVars = Environment.GetEnvironmentVariables()

' special folders
Dim appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
Dim myDocs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
Dim temp = Environment.GetFolderPath(Environment.SpecialFolder.Temp)
Dim programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)

' system info
Console.WriteLine($"Machine: {Environment.MachineName}")
Console.WriteLine($"User: {Environment.UserName}")
Console.WriteLine($"OS: {Environment.OSVersion}")
Console.WriteLine($"64-bit: {Environment.Is64BitOperatingSystem}")
Console.WriteLine($"Processors: {Environment.ProcessorCount}")
Console.WriteLine($".NET: {Environment.Version}")
Console.WriteLine($"Uptime: {Environment.TickCount64  1000} seconds")

' command line args
Dim args = Environment.GetCommandLineArgs()
' args(0) is the executable path

' current directory
Environment.CurrentDirectory = "C:	emp"

' system power
Console.WriteLine($"System uptime: {TimeSpan.FromMilliseconds(Environment.TickCount64)}")

' WMI for detailed hardware info
Imports System.Management
Dim searcher As New ManagementObjectSearcher("SELECT * FROM Win32_Processor")
For Each obj In searcher.Get()
    Console.WriteLine($"CPU: {obj("Name")}")
Next

' screen info
Dim screens = Screen.AllScreens
For Each s In screens
    Console.WriteLine($"{s.DeviceName}: {s.Bounds.Width}x{s.Bounds.Height}")
Next

Archivos INI y configuración

Los archivos INI son heredados pero aún se usan para configuraciones simples. Las funciones de la API de Windows (WritePrivateProfileString, GetPrivateProfileString) manejan el parseo. Para .NET moderno, use app.config (XML) o appsettings.json (JSON) con ConfigurationManager o Microsoft.Extensions.Configuration. La configuración JSON soporta estructuras anidadas, arrays y sobrescrituras específicas de entorno (appsettings.Production.json). Los configuration builders pueden combinar múltiples fuentes (archivos, variables de entorno, línea de comandos). Siempre separe la configuración del código para flexibilidad de despliegue.

vb
' INI files (legacy but still common)
Imports System.Runtime.InteropServices

Public Class IniFile
    Private path As String

    <DllImport("kernel32.dll", CharSet:=CharSet.Unicode)>
    Private Shared Function WritePrivateProfileString(section As String,
        key As String, value As String, filePath As String) As Boolean
    End Function

    <DllImport("kernel32.dll", CharSet:=CharSet.Unicode)>
    Private Shared Function GetPrivateProfileString(section As String,
        key As String, defaultVal As String,
        <Out> ByVal retVal As StringBuilder, size As Integer,
        filePath As String) As Integer
    End Function

    Public Sub New(filePath As String)
        path = filePath
    End Sub

    Public Function Read(section As String, key As String, Optional defaultVal As String = "") As String
        Dim sb As New StringBuilder(255)
        GetPrivateProfileString(section, key, defaultVal, sb, 255, path)
        Return sb.ToString()
    End Function

    Public Sub Write(section As String, key As String, value As String)
        WritePrivateProfileString(section, key, value, path)
    End Sub
End Class

' usage
Dim ini As New IniFile("config.ini")
ini.Write("Settings", "Theme", "Dark")
Dim theme = ini.Read("Settings", "Theme", "Light")

' modern: app.config / appsettings.json
' app.config:
' <appSettings>
'   <add key="Theme" value="Dark" />
' </appSettings>
Dim theme2 = Configuration.ConfigurationManager.AppSettings("Theme")

' JSON config (with System.Configuration or Microsoft.Extensions.Configuration)
' appsettings.json:
' { "Theme": "Dark", "MaxItems": 100 }
15

Colecciones y genéricos en profundidad

List, Dictionary y HashSet

List(Of T) es el caballo de batalla — array dinámico con append O(1), insert/remove O(n). Dictionary(Of K,V) proporciona lookup por clave O(1) (hash table). HashSet(Of T) almacena elementos únicos con operaciones de conjuntos rápidas (Union, Intersect, Except). SortedDictionary mantiene claves ordenadas (árbol de búsqueda binaria). LinkedList es doblemente enlazada (inserción/eliminación rápida en cualquier sitio, pero sin acceso indexado). Elija basándose en patrones de acceso: List para indexado, Dictionary para clave, HashSet para pertenencia, SortedDictionary para iteración ordenada. Todas son genéricas (type-safe, sin boxing).

vb
Imports System.Collections.Generic

' List(Of T)
Dim list As New List(Of String) From {"a", "b", "c"}
list.Add("d")
list.AddRange({"e", "f"})
list.Insert(0, "first")
list.Remove("b")
list.RemoveAt(0)
list.Sort()
list.Reverse()
Dim count = list.Count
Dim hasA = list.Contains("a")
Dim idx = list.IndexOf("c")
Dim slice = list.GetRange(0, 2)  ' first 2 elements
Dim arr = list.ToArray()

' Dictionary(Of TKey, TValue)
Dim dict As New Dictionary(Of String, Integer)
dict.Add("one", 1)
dict("two") = 2
dict.TryGetValue("one", Dim val As Integer)  ' val = 1, returns True
For Each kvp In dict
    Console.WriteLine($"{kvp.Key} = {kvp.Value}")
Next
Dim keys = dict.Keys.ToList()
Dim vals = dict.Values.ToList()

' HashSet(Of T) (unique elements, O(1) lookup)
Dim set1 As New HashSet(Of Integer) From {1, 2, 3}
Dim set2 As New HashSet(Of Integer) From {3, 4, 5}
set1.Add(3)  ' no effect (already exists)
Dim union = New HashSet(Of Integer)(set1) : union.UnionWith(set2)  ' 1,2,3,4,5
Dim inter = New HashSet(Of Integer)(set1) : inter.IntersectWith(set2)  ' 3
Dim diff = New HashSet(Of Integer)(set1) : diff.ExceptWith(set2)  ' 1,2

' SortedDictionary / SortedList (sorted by key)
Dim sorted As New SortedDictionary(Of String, Integer)
sorted("zebra") = 1
sorted("apple") = 2  ' iterates in sorted order

' LinkedList (fast insert/remove at ends)
Dim ll As New LinkedList(Of Integer)
ll.AddFirst(1)
ll.AddLast(2)

Queue, Stack y ObservableCollection

Queue (FIFO) y Stack (LIFO) están especializadas para patrones de acceso ordenado. ObservableCollection lanza eventos CollectionChanged cuando se añaden/eliminan/reemplazan elementos — esencial para data binding MVVM. BindingList extiende esto con capacidades de edición (AddingNew, AllowEdit, AllowRemove). ReadOnlyCollection envuelve una lista para prevenir modificación (devuelve la original, no una copia). KeyedCollection combina semánticas de lista y diccionario (acceso por índice o clave). Para versiones thread-safe, use ConcurrentQueue, ConcurrentStack, ConcurrentDictionary de System.Collections.Concurrent.

vb
Imports System.Collections.Generic
Imports System.Collections.ObjectModel

' Queue(Of T) (FIFO)
Dim queue As New Queue(Of String)
queue.Enqueue("first")
queue.Enqueue("second")
Dim next1 = queue.Dequeue()  ' "first"
Dim peek = queue.Peek()       ' "second" (doesn't remove)
Dim hasItems = queue.Count > 0

' Stack(Of T) (LIFO)
Dim stack As New Stack(Of Integer)
stack.Push(1)
stack.Push(2)
stack.Push(3)
Dim top = stack.Pop()   ' 3
Dim peek = stack.Peek() ' 2

' ObservableCollection (notifies on changes - for data binding)
Dim people As New ObservableCollection(Of Person)()
AddHandler people.CollectionChanged, Sub(sender, e)
    If e.Action = NotifyCollectionChangedAction.Add Then
        Console.WriteLine($"Added: {CType(e.NewItems(0), Person).Name}")
    End If
End Sub
people.Add(New Person With {.Name = "Alice"})  ' triggers event

' BindingList(Of T) (editable, for DataGridView)
Dim list As New BindingList(Of Person)()
list.AddingNew = Function() New Person()
list.RaiseListChangedEvents = True

' ReadOnlyCollection (wrapper, prevents modification)
Dim ro As New ReadOnlyCollection(Of String)(list)
' ro.Add("x")  ' throws NotSupportedException

' KeyedCollection (abstract, dictionary-like by key)
Public Class PersonCollection
    Inherits KeyedCollection(Of String, Person)
    Protected Overrides Function GetKeyForItem(item As Person) As String
        Return item.Name
    End Function
End Class

Métodos genéricos y restricciones

Los genéricos proporcionan seguridad de tipos sin overhead de boxing/unboxing. Las restricciones (Of T As ...) limitan parámetros de tipo: Class (tipos referencia), Structure (tipos valor), New (constructor sin parámetros), IComparable (interfaz), o una clase base. Múltiples restricciones usan llaves: Of T As {Class, New, IComparable(Of T)}. Los métodos genéricos infieren el tipo desde argumentos. Los genéricos están reificados en .NET (información de tipo disponible en tiempo de ejecución, a diferencia del type erasure de Java). Use genéricos para colecciones, algoritmos y clases utilitarias para evitar duplicación de código manteniendo seguridad de tipos.

vb
' generic method
Public Function Max(Of T As IComparable(Of T))(a As T, b As T) As T
    If a.CompareTo(b) > 0 Then Return a
    Return b
End Function

Dim m1 = Max(3, 5)          ' Integer
Dim m2 = Max("apple", "banana")  ' String
Dim m3 = Max(3.14, 2.71)    ' Double

' multiple type parameters
Public Function Pair(Of T1, T2)(first As T1, second As T2) As Tuple(Of T1, T2)
    Return Tuple.Create(first, second)
End Function

' constraints
Public Class Repository(Of T As Class, New)
    ' T must be a reference type with parameterless constructor
    Public Function Create() As T
        Return New T()
    End Function
End Class

' constraint types:
' Class      - reference type
' Structure  - value type
' New        - has parameterless constructor
' IComparable(Of T) - implements interface
' BaseClass  - inherits from base class

' generic method with interface constraint
Public Sub Sort(Of T As IComparable(Of T))(ByRef arr As T())
    Array.Sort(arr)
End Sub

' multiple constraints
Public Function Process(Of T As {Class, IComparable(Of T), New})(items As List(Of T)) As T
    Dim item As New T()
    items.Sort()
    Return items(0)
End Function

' generic class with method
Public Class Cache(Of TKey, TValue)
    Private dict As New Dictionary(Of TKey, TValue)
    Public Sub Add(key As TKey, value As TValue)
        dict.Add(key, value)
    End Sub
    Public Function Get(key As TKey) As TValue
        Return dict(key)
    End Function
End Class

Colecciones personalizadas e IEnumerable

Implementar IEnumerable(Of T) habilita la iteración For Each. El patrón Iterator/Yield (VB 14+) genera máquinas de estados para evaluación diferida — los valores se producen bajo demanda. Esto es eficiente en memoria para secuencias grandes o infinitas. CollectionBase es una clase base heredada; prefiera implementar ICollection(Of T) o heredar de List(Of T) para código nuevo. Las colecciones personalizadas tienen sentido cuando necesita comportamiento especial (validación, notificaciones, carga diferida). Para la mayoría de los casos, componga colecciones existentes en lugar de construir desde cero.

vb
Imports System.Collections
Imports System.Collections.Generic

' implement IEnumerable (foreach support)
Public Class NumberSequence
    Implements IEnumerable(Of Integer)

    Private start, count As Integer

    Public Sub New(start As Integer, count As Integer)
        Me.start = start
        Me.count = count
    End Sub

    Public Function GetEnumerator() As IEnumerator(Of Integer) _
        Implements IEnumerable(Of Integer).GetEnumerator
        For i = 0 To count - 1
            Yield start + i
        Next
    End Function

    Private Function GetEnumerator1() As IEnumerator _
        Implements IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function
End Class

' usage
Dim seq As New NumberSequence(10, 5)
For Each n In seq
    Console.WriteLine(n)  ' 10, 11, 12, 13, 14
Next

' iterator with Yield (lazy evaluation)
Public Iterator Function Evens(max As Integer) As IEnumerable(Of Integer)
    For i = 0 To max
        If i Mod 2 = 0 Then Yield i
    Next
End Function

' custom collection class
Public Class PersonList
    Inherits CollectionBase

    Default Public Property Item(index As Integer) As Person
        Get
            Return CType(List(index), Person)
        End Get
        Set(value As Person)
            List(index) = value
        End Set
    End Property

    Public Function Add(p As Person) As Integer
        Return List.Add(p)
    End Function
End Class

' implement ICollection(Of T) for full collection semantics
Public Class CircularBuffer(Of T)
    Implements ICollection(Of T)
    ' ... implement all members
End Class

Tuples & ValueTuple

ValueTuple (VB 15+) es la forma moderna de devolver múltiples valores — ligero (tipo de valor), con campos nombrados para legibilidad. Tuple (System.Tuple) es un tipo de referencia con nombres Item1/Item2 únicamente. ValueTuple soporta deconstrucción (Dim (a, b) = ...) e igualdad elemento por elemento. Use ValueTuple para devoluciones internas de métodos y resultados intermedios de LINQ. Para APIs públicas, prefiera records o clases con nombres significativos. El patrón discard (_) ignora valores no deseados. ValueTuple es especialmente útil en LINQ para proyecciones anónimas que necesitan ser devueltas desde métodos.

vb
' Tuple (reference type, up to 8 items)
Dim t1 As New Tuple(Of Integer, String, Boolean)(1, "hello", True)
Console.WriteLine(t1.Item1)  ' 1
Console.WriteLine(t1.Item2)  ' "hello"

' ValueTuple (value type, lightweight, named fields)
Dim vt1 As (Id As Integer, Name As String, Active As Boolean) =
    (1, "hello", True)
Console.WriteLine(vt1.Id)      ' 1
Console.WriteLine(vt1.Name)    ' "hello"

' without names (Item1, Item2, ...)
Dim vt2 = (1, "hello", True)
Console.WriteLine(vt2.Item1)

' return multiple values
Public Function FindMinMax(numbers As Integer()) As (Min As Integer, Max As Integer)
    Return (numbers.Min(), numbers.Max())
End Function

Dim result = FindMinMax({3, 1, 4, 1, 5, 9})
Console.WriteLine($"Min: {result.Min}, Max: {result.Max}")

' deconstruction
Dim (id, name, active) = GetRecord()
Dim (min, max) = FindMinMax({1, 2, 3})

' tuple in LINQ
Dim stats = From p In people
            Group By p.Dept Into Group
            Select Dept,
                   Count = Group.Count(),
                   AvgSalary = Group.Average(Function(x) x.Salary)

' tuple comparison
Dim a = (1, 2)
Dim b = (1, 2)
Console.WriteLine(a = b)  ' True (element-wise comparison)

' discard
Dim (first, _, last) = GetThreeValues()  ' ignore middle
16

Depuración y Diagnóstico

Debug y Trace

La clase Debug se compila fuera en builds Release (usa ConditionalAttribute); Trace funciona en todos los builds. TraceListeners dirigen la salida a archivos, consola o registro de eventos. TraceSwitch (configurable vía app.config) controla la verbosidad en tiempo de ejecución sin recompilar. TraceSource proporciona trazado nombrado y granular con tipos de eventos (Error, Warning, Info, Verbose). Siempre use Trace para diagnóstico de producción y Debug para aserciones de desarrollo. El atributo Conditional elimina las llamadas a métodos completamente en builds Release — sobrecarga cero. Configure listeners en app.config para flexibilidad de despliegue.

vb
Imports System.Diagnostics

' Debug class (Debug builds only)
Debug.WriteLine("Debug message")
Debug.Assert(balance >= 0, "Balance is negative!")
Debug.WriteLineIf(verbose, "Verbose output")
Debug.Indent()
Debug.WriteLine("Indented")
Debug.Unindent()

' Trace class (Release builds too)
Trace.WriteLine("Trace message")
Trace.TraceInformation("Info: {0} at {1}", value, Date.Now)
Trace.TraceWarning("Warning: low memory")
Trace.TraceError("Error: connection failed")

' trace listeners
Trace.Listeners.Add(New TextWriterTraceListener("trace.log"))
Trace.Listeners.Add(New ConsoleTraceListener())
Trace.AutoFlush = True

' conditional tracing
#If DEBUG Then
    Debug.WriteLine("Only in debug")
#End If

<Conditional("DEBUG")>
Public Sub DebugLog(msg As String)
    Console.WriteLine(msg)
End Sub

' TraceSwitch (configurable level)
Dim ts As New TraceSwitch("MySwitch", "Application tracing")
' app.config:
' <switches><add name="MySwitch" value="3" /></switches>
' 0=Off, 1=Error, 2=Warning, 3=Info, 4=Verbose
If ts.TraceInfo Then Trace.WriteLine("Info message")
If ts.TraceVerbose Then Trace.WriteLine("Verbose message")

' TraceSource (named, granular)
Dim ts2 As New TraceSource("MyApp")
ts2.TraceEvent(TraceEventType.Error, 0, "Something failed")
ts2.Switch.Level = SourceLevels.All

Depurador y Breakpoints

Los atributos del depurador personalizan cómo aparecen los objetos en el depurador. DebuggerDisplay controla la cadena de resumen; DebuggerBrowsable controla la visibilidad de miembros (RootHidden aplana colecciones). DebuggerStepThrough/Hidden controlan el comportamiento de paso. Los breakpoints condicionales pausan solo cuando una condición es verdadera — esenciales para encontrar bugs en bucles grandes. Los tracepoints registran mensajes sin modificar código. La Ventana Inmediata evalúa expresiones en tiempo de ejecución. Edit and Continue le permite corregir código sin reiniciar. Domine estas características del depurador para acelerar drásticamente la depuración.

vb
Imports System.Diagnostics

' break into debugger
Debugger.Break()  ' if attached, pauses; otherwise prompts

' check if debugger attached
If Debugger.IsAttached Then
    Console.WriteLine("Running in debugger")
End If

' launch debugger
Debugger.Launch()

' DebuggerDisplay attribute (custom display in watch window)
<DebuggerDisplay("Name = {Name}, Age = {Age}")>
Public Class Person
    Public Property Name As String
    Public Property Age As Integer

    <DebuggerBrowsable(DebuggerBrowsableState.RootHidden)>
    Public Property Items As Integer() = {1, 2, 3}
End Class

' DebuggerStepThrough (skip in F11 step-into)
<DebuggerStepThrough>
Public Sub TrivialMethod()
    ' debugger won't step into this
End Sub

' DebuggerHidden (not visible in debugger)
<DebuggerHidden>
Public Sub InternalHelper()
End Sub

' conditional breakpoints in IDE:
' Right-click breakpoint > Settings > Condition
' e.g., i == 42 or str.Contains("error")

' tracepoints (log message without code):
' Right-click > Actions > Log message
' "i = {i}, value = {value}"

' edit and continue:
' modify code while paused, continue (limited support)

' immediate window (while debugging):
' ? variableName
' ? methodCall()
' variableName = newValue

Estrategias de Manejo de Excepciones

Capture excepciones específicas antes que las genéricas (la más específica primero). Use Throw (no Throw ex) para preservar el stack trace original. Los filtros de excepciones (cláusula When) añaden condiciones sin capturar — la excepción se propaga si el filtro es falso. Las excepciones personalizadas deben heredar de Exception (no ApplicationException, que está obsoleto), ser Serializable, e implementar los tres constructores. Envuelva excepciones de bajo nivel en unas específicas del dominio para abstraer los detalles de implementación. Nunca capture y trague excepciones silenciosamente — como mínimo regístrelas. Use manejadores de excepciones globales (AppDomain.UnhandledException, Application.ThreadException) como red de seguridad.

vb
' try/catch/finally
Try
    RiskyOperation()
Catch ex As FileNotFoundException
    ' specific exception first
    Logger.Error(ex, "File not found")
    Throw New UserFriendlyException("Please check the file path", ex)
Catch ex As IOException
    Logger.Error(ex, "IO error")
    Throw
Catch ex As Exception
    ' generic last resort
    Logger.Error(ex, "Unexpected error")
    Throw
Finally
    ' always runs (cleanup)
    resource?.Dispose()
End Try

' exception filters (VB 14+)
Catch ex As HttpException When ex.StatusCode = 404
    ' only catch 404s
End Try

' custom exception
<Serializable>
Public Class BusinessRuleException
    Inherits Exception

    Public Property RuleName As String

    Public Sub New(message As String, ruleName As String)
        MyBase.New(message)
        RuleName = ruleName
    End Sub

    Public Sub New(message As String, ruleName As String, inner As Exception)
        MyBase.New(message, inner)
        RuleName = ruleName
    End Sub

    Protected Sub New(info As Runtime.Serialization.SerializationInfo,
                     context As Runtime.Serialization.StreamingContext)
        MyBase.New(info, context)
        RuleName = info.GetString("RuleName")
    End Sub

    Public Overrides Sub GetObjectData(info As Runtime.Serialization.SerializationInfo,
                                       context As Runtime.Serialization.StreamingContext)
        MyBase.GetObjectData(info, context)
        info.AddValue("RuleName", RuleName)
    End Sub
End Class

Registro y Registro de Eventos

El Registro de Eventos de Windows es ideal para eventos a nivel de sistema (inicio/parada de servicios, errores críticos) — requiere admin para crear el origen. Para registro de aplicaciones, use un framework de registro estructurado: Serilog (moderno, compatible con JSON), NLog, o log4net. El registro estructurado (logger.Information("User {UserId}...", id)) preserva los tipos de datos para consulta, a diferencia de la concatenación de cadenas. Los niveles de registro (Debug, Info, Warning, Error, Fatal) le permiten filtrar en tiempo de ejecución. Siempre incluya contexto (ID de usuario, ID de orden) en los logs para depuración. Los contadores de rendimiento monitorean CPU, memoria y métricas personalizadas en producción.

vb
Imports System.Diagnostics

' write to Windows Event Log
If Not EventLog.SourceExists("MyApp") Then
    EventLog.CreateEventSource("MyApp", "Application")
End If
EventLog.WriteEntry("MyApp", "Application started", EventLogEntryType.Information)
EventLog.WriteEntry("MyApp", "Disk full!", EventLogEntryType.Error, 1001)
EventLog.WriteEntry("MyApp", "Retrying operation", EventLogEntryType.Warning, 2001)

' read from Event Log
Dim log As New EventLog("Application")
For Each entry In log.Entries.Cast(Of EventLogEntry)().
    Where(Function(e) e.Source = "MyApp").
    Take(10)
    Console.WriteLine($"{entry.TimeGenerated}: {entry.Message}")
Next

' structured logging with Serilog (NuGet)
' Install-Package Serilog.Sinks.File
' Dim logger = New LoggerConfiguration().
'     WriteTo.File("log.txt", rollingInterval:=RollingInterval.Day).
'     CreateLogger()
' logger.Information("User {UserId} logged in", userId)
' logger.Error(ex, "Failed to process {OrderId}", orderId)

' log4net (alternative)
' <log4net>
'   <appender name="FileAppender" type="log4net.Appender.FileAppender">
'     <file value="log.txt" />
'   </appender>
' </log4net>

' NLog (alternative)
' Dim logger = LogManager.GetCurrentClassLogger()
' logger.Info("Processing started")
' logger.Error(ex, "Processing failed")

' performance counters
Dim pc As New PerformanceCounter("Processor", "% Processor Time", "_Total")
Dim cpuUsage = pc.NextValue()
Thread.Sleep(1000)
cpuUsage = pc.NextValue()  ' actual value (needs two samples)

Pruebas Unitarias y TDD

Las pruebas unitarias verifican métodos individuales de forma aislada. MSTest (integrado), xUnit, y NUnit son los principales frameworks. TestInitialize/TestCleanup se ejecutan antes/después de cada prueba. Los métodos Assert verifican resultados (AreEqual, IsTrue, ThrowsException). DataTestMethod/DataRow parametrizan pruebas con múltiples entradas. ExpectedException (o Try/Catch/Assert.Fail) prueba casos de excepción. Use Moq para simular dependencias (interfaces) — configure el comportamiento esperado y verifique las llamadas. TDD (Desarrollo Guiado por Pruebas) escribe pruebas primero, luego código. Apunte a alta cobertura de la lógica de negocio; omita getters/setters triviales de propiedades. Ejecute pruebas en CI/CD para detectar regresiones.

vb
' MSTest (built-in)
Imports Microsoft.VisualStudio.TestTools.UnitTesting

<TestClass>
Public Class CalculatorTests
    Private calc As Calculator

    <TestInitialize>
    Public Sub Setup()
        calc = New Calculator()
    End Sub

    <TestCleanup>
    Public Sub Cleanup()
        ' cleanup after each test
    End Sub

    <TestMethod>
    Public Sub Add_TwoNumbers_ReturnsSum()
        Dim result = calc.Add(2, 3)
        Assert.AreEqual(5, result)
    End Sub

    <TestMethod>
    Public Sub Divide_ByZero_ThrowsException()
        Try
            calc.Divide(10, 0)
            Assert.Fail("Should have thrown")
        Catch ex As DivideByZeroException
            ' expected
        End Try
    End Sub

    <TestMethod, ExpectedException(GetType(DivideByZeroException))>
    Public Sub Divide_ByZero_Throws()
        calc.Divide(10, 0)
    End Sub

    <DataTestMethod>
    <DataRow(1, 2, 3)>
    <DataRow(10, 20, 30)>
    <DataRow(-1, 1, 0)>
    Public Sub Add_VariousInputs(a As Integer, b As Integer, expected As Integer)
        Assert.AreEqual(expected, calc.Add(a, b))
    End Sub
End Class

' xUnit (alternative)
' <Fact> instead of <TestMethod>
' <Theory> with <InlineData> instead of <DataTestMethod>/<DataRow>

' Moq for mocking
' Dim mockRepo = New Mock(Of IUserRepository)()
' mockRepo.Setup(Function(r) r.GetById(1)).Returns(New User With {.Name = "Test"})
' Dim service = New UserService(mockRepo.Object)
17

Colecciones y Diccionarios

ArrayList y List(Of T)

List(Of T) es el reemplazo moderno y con seguridad de tipos de ArrayList. Siempre prefieralo — obtiene verificación de tipos en tiempo de compilación y mejor rendimiento (sin boxing para tipos de valor). La cláusula From inicializa la lista inline. FindAll/Find usan predicados (lambdas). AddRange añade múltiples elementos a la vez.

vb
' ArrayList (loosely typed, legacy)
Dim arr As New ArrayList()
arr.Add("hello")
arr.Add(42)
arr.Add(3.14)
arr.Remove("hello")
arr.RemoveAt(0)
For Each item In arr
    Console.WriteLine(item)
Next

' List(Of T) — strongly typed, preferred
Dim nums As New List(Of Integer) From {1, 2, 3, 4, 5}
nums.Add(6)
nums.AddRange({7, 8, 9})
nums.Remove(3)
nums.RemoveAt(0)
nums.Sort()
nums.Reverse()

' find and convert
Dim evens = nums.FindAll(Function(n) n Mod 2 = 0)
Dim firstEven = nums.Find(Function(n) n Mod 2 = 0)
Dim arr2 = nums.ToArray()

' index
Dim idx = nums.IndexOf(5)
Console.WriteLine($"Count: {nums.Count}, Contains 5: {nums.Contains(5)}")

Dictionary

Dictionary(Of TKey, TValue) es el hash map estándar. Use TryGetValue para obtener un valor de forma segura (devuelve False si no existe, sin excepción). Add lanza una excepción si la clave existe; el indexador (dict(key)) sobrescribe silenciosamente. Las colecciones Keys/Values le permiten iterar o extraer. LINQ funciona en diccionarios — son IEnumerable(Of KeyValuePair).

vb
' Dictionary(Of TKey, TValue)
Dim ages As New Dictionary(Of String, Integer) From {
    {"Alice", 30},
    {"Bob", 25}
}

' add and update
ages("Carol") = 42                  ' add or update
ages.Add("Dave", 35)                ' throws if key exists
ages.Remove("Bob")

' access
If ages.ContainsKey("Alice") Then
    Console.WriteLine(ages("Alice"))
End If

' safe access
Dim age As Integer
If ages.TryGetValue("Eve", age) Then
    Console.WriteLine($"Eve is {age}")
End If

' iterate
For Each kvp In ages
    Console.WriteLine($"{kvp.Key}: {kvp.Value}")
Next

' keys and values
Dim names = ages.Keys.ToList()
Dim allAges = ages.Values.ToList()

' LINQ on dictionaries
Dim adults = ages.Where(Function(kvp) kvp.Value >= 30).ToDictionary(Function(kvp) kvp.Key, Function(kvp) kvp.Value)

HashSet y SortedSet

HashSet es para pruebas de pertenencia rápidas y operaciones de conjuntos (unión, intersección, diferencia). SortedSet mantiene elementos en orden ordenado (árbol de búsqueda binaria). Ambos tienen operaciones O(1) o O(log n) — mucho más rápidas que List.Contains (O(n)). Use HashSet cuando el orden no importa; SortedSet cuando necesita iteración ordenada.

vb
' HashSet(Of T) — unique elements, fast lookup
Dim set1 As New HashSet(Of Integer) From {1, 2, 3, 4, 5}
Dim set2 As New HashSet(Of Integer) From {3, 4, 5, 6, 7}

set1.Add(6)                         ' adds only if not present
set1.Remove(1)

' set operations
Dim union = New HashSet(Of Integer)(set1)
union.UnionWith(set2)               ' {1,2,3,4,5,6,7}

Dim intersect = New HashSet(Of Integer)(set1)
intersect.IntersectWith(set2)       ' {3,4,5,6}

Dim diff = New HashSet(Of Integer)(set1)
diff.ExceptWith(set2)               ' {1,2}

Dim symDiff = New HashSet(Of Integer)(set1)
symDiff.SymmetricExceptWith(set2)   ' {1,2,7}

' checks
set1.IsSubsetOf(set2)
set1.IsSupersetOf(set2)
set1.Overlaps(set2)                 ' any common elements

' SortedSet keeps elements sorted
Dim sorted As New SortedSet(Of Integer) From {5, 2, 8, 1, 9}
' {1, 2, 5, 8, 9}
Console.WriteLine(sorted.Min)
Console.WriteLine(sorted.Max)

Queue y Stack

Queue es FIFO (primero en entrar, primero en salir) — use para programación de tareas, BFS. Stack es LIFO (último en entrar, primero en salir) — use para deshacer, evaluación de expresiones, DFS. Ambos tienen enqueue/dequeue y push/pop en O(1). Construya un Stack desde un array para invertirlo en una línea. Peek mira el siguiente elemento sin removerlo.

vb
' Queue(Of T) — FIFO
Dim queue As New Queue(Of String)()
queue.Enqueue("first")
queue.Enqueue("second")
queue.Enqueue("third")

Dim next1 = queue.Dequeue()         ' "first"
Dim peek = queue.Peek()             ' "second" (doesn't remove)
Console.WriteLine($"Count: {queue.Count}")

' process queue
Do While queue.Count > 0
    Dim item = queue.Dequeue()
    Console.WriteLine(item)
Loop

' Stack(Of T) — LIFO
Dim stack As New Stack(Of Integer)()
stack.Push(1)
stack.Push(2)
stack.Push(3)

Dim top = stack.Pop()               ' 3
Dim peek = stack.Peek()             ' 2

' reverse using stack
Dim arr = {1, 2, 3, 4, 5}
Dim revStack As New Stack(Of Integer)(arr)
Dim reversed = revStack.ToArray()   ' {5, 4, 3, 2, 1}

Colecciones concurrentes

Las colecciones concurrentes son thread-safe — úselas en lugar de bloquear colecciones regulares. AddOrUpdate y GetOrAdd de ConcurrentDictionary son atómicos. ConcurrentQueue/Stack son lock-free. ConcurrentBag es más rápido cuando cada hilo principalmente produce y consume sus propios elementos. BlockingCollection es el patrón estándar para colas productor-consumidor.

vb
Imports System.Collections.Concurrent

' thread-safe collections for parallel programming

' ConcurrentDictionary
Dim cd As New ConcurrentDictionary(Of String, Integer)()
cd("a") = 1
cd.AddOrUpdate("a", 1, Function(key, oldVal) oldVal + 1)
cd.GetOrAdd("b", Function(key) 42)

' ConcurrentQueue (lock-free FIFO)
Dim cq As New ConcurrentQueue(Of Integer)()
cq.Enqueue(1)
Dim val As Integer
If cq.TryDequeue(val) Then
    Console.WriteLine(val)
End If

' ConcurrentStack (lock-free LIFO)
Dim cs As New ConcurrentStack(Of Integer)()
cs.Push(1)
cs.TryPop(val)

' ConcurrentBag (unordered, fast for same-thread adds)
Dim cb As New ConcurrentBag(Of Integer)()
cb.Add(1)
cb.TryTake(val)

' BlockingCollection (producer-consumer)
Dim bc As New BlockingCollection(Of Integer)(100)  ' bounded
' Producer:
Task.Run(Sub()
    For i = 1 To 100
        bc.Add(i)
    Next
    bc.CompleteAdding()
End Sub)
' Consumer:
Task.Run(Sub()
    For Each item In bc.GetConsumingEnumerable()
        Console.WriteLine(item)
    Next
End Sub)
18

E/S de Archivos Avanzada

Lectura y escritura de streams

Siempre envuelva streams en bloques Using para asegurar la disposición (cierra el archivo incluso en excepciones). File.ReadAllText/ReadAllLines son convenientes para archivos pequeños. Para archivos grandes, use StreamReader línea por línea. La E/S asíncrona (ReadToEndAsync) mantiene las UIs responsivas — nunca llame a E/S de archivos sincrónica en el hilo de UI.

vb
Imports System.IO

' StreamReader
Using reader As New StreamReader("input.txt")
    Dim line As String
    Do While reader.Peek() >= 0
        line = reader.ReadLine()
        Console.WriteLine(line)
    Loop
End Using

' read all at once
Dim allText = File.ReadAllText("input.txt")
Dim allLines = File.ReadAllLines("input.txt")

' StreamWriter
Using writer As New StreamWriter("output.txt")
    writer.WriteLine("First line")
    writer.WriteLine("Second line")
    writer.Write("No newline")
End Using

' append
File.AppendAllText("log.txt", $"{Date.Now}: message{Environment.NewLine}")

' async I/O (don't block UI)
Async Function ReadAsync(path As String) As Task(Of String)
    Using reader As New StreamReader(path)
        Return Await reader.ReadToEndAsync()
    End Using
End Function

' write async
Using writer As New StreamWriter("out.txt")
    Await writer.WriteLineAsync("async line")
End Using

Binario y CSV

BinaryWriter/Reader manejan datos binarios tipados — ReadInt32/ReadDouble deben coincidir con el orden de escritura exactamente. TextFieldParser (en Microsoft.VisualBasic.FileIO) es la forma robusta de parsear CSV — maneja campos entre comillas con comas incrustadas y nuevas líneas. No haga su propio parser de CSV con Split(","). Para CSV complejo, considere el paquete NuGet CsvHelper.

vb
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

' BinaryWriter/Reader
Using fs As New FileStream("data.bin", FileMode.Create),
      bw As New BinaryWriter(fs)
    bw.Write(42I)                    ' Integer
    bw.Write(3.14R)                  ' Double
    bw.Write("hello")                ' String (length-prefixed)
End Using

Using fs As New FileStream("data.bin", FileMode.Open),
      br As New BinaryReader(fs)
    Dim n = br.ReadInt32()
    Dim d = br.ReadDouble()
    Dim s = br.ReadString()
End Using

' CSV with TextFieldParser (handles quoted fields)
Using parser As New TextFieldParser("data.csv")
    parser.TextFieldType = FieldType.Delimited
    parser.SetDelimiters(",")
    parser.HasFieldsEnclosedInQuotes = True

    ' skip header
    If Not parser.EndOfData Then parser.ReadFields()

    While Not parser.EndOfData
        Dim fields = parser.ReadFields()
        Console.WriteLine($"Name: {fields(0)}, Age: {fields(1)}")
    End While
End Using

' write CSV
Dim lines = {"name,age", "Alice,30", "Bob,25"}
File.WriteAllLines("out.csv", lines)

Operaciones de Path

Siempre use Path.Combine (no concatenación de cadenas) para unir paths — maneja los separadores correctamente. Path.GetInvalidFileNameChars ayuda a sanitizar la entrada del usuario para nombres de archivo. FileInfo/DirectoryInfo almacenan en caché metadatos de archivos — úselos cuando necesita múltiples propiedades. SearchOption.AllDirectories recursa; para árboles grandes, use EnumerateFiles (streaming).

vb
Imports System.IO

' path manipulation (cross-platform)
Dim full = Path.Combine("C:", "Users", "alice", "data.txt")
' C:Usersalicedata.txt

Dim dir = Path.GetDirectoryName(full)      ' C:Usersalice
Dim file = Path.GetFileName(full)           ' data.txt
Dim name = Path.GetFileNameWithoutExtension(full)  ' data
Dim ext = Path.GetExtension(full)           ' .txt

' temp files
Dim tempFile = Path.GetTempFileName()       ' creates empty file
Dim tempDir = Path.GetTempPath()

' check extension
If Path.GetExtension(path).Equals(".csv", StringComparison.OrdinalIgnoreCase) Then
    ' process CSV
End If

' directory operations
Directory.CreateDirectory("path/to/dir")
If Directory.Exists("path") Then
    Dim files = Directory.GetFiles("path", "*.txt", SearchOption.AllDirectories)
    Dim dirs = Directory.GetDirectories("path")
End If

' file info
Dim fi As New FileInfo("data.txt")
Console.WriteLine($"Size: {fi.Length}, Modified: {fi.LastWriteTime}")

' safe file name
Dim invalid = Path.GetInvalidFileNameChars()
Dim safe = New String(name.Select(Function(c) If(invalid.Contains(c), "_"c, c)).ToArray())

Vigilancia de archivos

FileSystemWatcher vigila un directorio en busca de cambios. Establezca NotifyFilter para limitar qué cambios disparan eventos (rendimiento). El mayor problema: los editores a menudo disparan múltiples eventos Changed por guardado (escritura, flush, metadatos) — haga debounce rastreando el tiempo del último evento. Los errores (como desbordamiento de búfer por demasiados eventos) también necesitan manejo.

vb
Imports System.IO

' FileSystemWatcher monitors directory changes
Dim watcher As New FileSystemWatcher() With {
    .Path = "C:Watch",
    .Filter = "*.txt",
    .IncludeSubdirectories = True,
    .NotifyFilter = NotifyFilters.FileName Or NotifyFilters.LastWrite Or NotifyFilters.Size
}

' event handlers
AddHandler watcher.Created, Sub(s, e)
    Console.WriteLine($"Created: {e.FullPath}")
End Sub

AddHandler watcher.Changed, Sub(s, e)
    Console.WriteLine($"Changed: {e.FullPath}")
End Sub

AddHandler watcher.Deleted, Sub(s, e)
    Console.WriteLine($"Deleted: {e.FullPath}")
End Sub

AddHandler watcher.Renamed, Sub(s, e)
    Console.WriteLine($"Renamed: {e.OldFullPath} -> {e.FullPath}")
End Sub

AddHandler watcher.Error, Sub(s, e)
    Console.WriteLine($"Error: {e.GetException().Message}")
End Sub

watcher.EnableRaisingEvents = True

' common gotcha: multiple events fire for one save
' debounce by tracking last event time
Dim lastEvent As DateTime
AddHandler watcher.Changed, Sub(s, e)
    If (DateTime.Now - lastEvent).TotalMilliseconds > 500 Then
        lastEvent = DateTime.Now
        ' process
    End If
End Sub

Serialización

System.Text.Json es el serializador JSON moderno y de alto rendimiento (reemplaza Newtonsoft.Json para código nuevo). XmlSerializer maneja XML — note que requiere un constructor sin parámetros y propiedades públicas. Para colecciones, serialice List(Of T) directamente. JsonSerializerOptions con WriteIndented produce salida legible por humanos.

vb
Imports System.IO
Imports System.Xml.Serialization
Imports System.Text.Json

' class to serialize
Public Class Person
    Public Property Name As String
    Public Property Age As Integer
    Public Property Email As String
End Class

' JSON (System.Text.Json — modern, fast)
Dim p As New Person With {.Name = "Alice", .Age = 30, .Email = "[email protected]"}
Dim json = JsonSerializer.Serialize(p)
File.WriteAllText("p.json", json)

Dim jsonOpts As New JsonSerializerOptions With {.WriteIndented = True}
File.WriteAllText("p_pretty.json", JsonSerializer.Serialize(p, jsonOpts))

Dim loaded = JsonSerializer.Deserialize(Of Person)(File.ReadAllText("p.json"))

' XML
Dim xs As New XmlSerializer(GetType(Person))
Using fs As New FileStream("p.xml", FileMode.Create)
    xs.Serialize(fs, p)
End Using

Using fs As New FileStream("p.xml", FileMode.Open)
    Dim loadedXml = DirectCast(xs.Deserialize(fs), Person)
End Using

' collections
Dim people As New List(Of Person) From {p, New Person With {.Name = "Bob"}}
Dim jsonList = JsonSerializer.Serialize(people)
Dim loadedList = JsonSerializer.Deserialize(Of List(Of Person))(jsonList)
19

Manejo de Errores Profundo

Patrones Try/Catch

Capture excepciones específicas antes que las generales (el orden importa). Use Finally para limpieza — se ejecuta incluso si devuelve o lanza dentro de Try. Los filtros When (VB 14+) le permiten capturar basándose en propiedades de la excepción. Al envolver excepciones, pase la original como innerException para preservar el stack trace. Evite capturar Exception a menos que relance.

vb
Try
    Dim result = 10 / x
    File.WriteAllText("out.txt", result.ToString())
Catch ex As DivideByZeroException
    Console.WriteLine($"Cannot divide by zero: {ex.Message}")
Catch ex As IOException
    Console.WriteLine($"File error: {ex.Message}")
Catch ex As UnauthorizedAccessException
    Console.WriteLine($"Permission denied: {ex.Message}")
Catch ex As Exception
    ' catch-all (use sparingly)
    Console.WriteLine($"Unexpected: {ex.Message}")
    Throw                          ' re-throw
Finally
    ' always runs (even on exception or return)
    ' cleanup: close files, release resources
End Try

' exception filters (VB 14+)
Try
    ProcessData()
Catch ex As InvalidOperationException When ex.Message.Contains("collection")
    Console.WriteLine("Collection was modified")
Catch ex As InvalidOperationException
    Console.WriteLine("Other invalid op")
End Try

' nested try
Try
    Try
        RiskyOperation()
    Catch ex As Exception
        Throw New InvalidOperationException("Inner failed", ex)  ' wrap
    End Try
Catch ex As Exception
    Console.WriteLine(ex.InnerException?.Message)
End Try

Excepciones personalizadas

Las excepciones personalizadas deben heredar de Exception (o una base específica del dominio) y proporcionar múltiples constructores: solo mensaje, mensaje + inner, y específicos del dominio. Márguelas <Serializable> si pueden cruzar límites de AppDomain. Añada propiedades para el contexto que los llamadores necesiten. Sobrescriba ToString para registro claro.

vb
' inherit from Exception (or a more specific base)
<Serializable>
Public Class ValidationException
    Inherits Exception

    Public Property FieldName As String

    Public Sub New(message As String)
        MyBase.New(message)
    End Sub

    Public Sub New(message As String, inner As Exception)
        MyBase.New(message, inner)
    End Sub

    Public Sub New(fieldName As String, message As String)
        MyBase.New(message)
        Me.FieldName = fieldName
    End Sub

    Public Overrides Function ToString() As String
        Return $"Validation error in '{FieldName}': {Message}"
    End Function
End Class

' usage
Sub ValidateAge(age As Integer)
    If age < 0 Then
        Throw New ValidationException("age", "Age cannot be negative")
    ElseIf age > 150
        Throw New ValidationException("age", "Age seems unrealistic")
    End If
End Sub

' catch custom
Try
    ValidateAge(-5)
Catch ex As ValidationException
    Console.WriteLine($"Field '{ex.FieldName}': {ex.Message}")
End Try

Estrategias de manejo de excepciones

Nunca trague excepciones silenciosamente — como mínimo, regístrelas. Use Using para recursos IDisposable (más limpio que try/finally). Registre manejadores globales para AppDomain.UnhandledException (última oportunidad) y Application.ThreadException (hilo de UI de WinForms). AggregateException envuelve múltiples excepciones de código paralelo — aplane y maneje cada InnerException.

vb
' 1. Don't catch what you can't handle
' BAD: swallows errors silently
Try
    SaveData()
Catch ex As Exception
    ' nothing
End Try

' GOOD: log and rethrow, or handle meaningfully
Try
    SaveData()
Catch ex As IOException
    Logger.Warn(ex, "Save failed, will retry")
    RetrySave()
Catch ex As Exception
    Logger.Error(ex, "Unexpected error saving")
    Throw
End Try

' 2. Use Using instead of try/finally for IDisposable
Using fs As New FileStream("f.txt", FileMode.Open)
    ' use fs
End Using  ' automatically disposes

' 3. Global exception handlers
AddHandler AppDomain.CurrentDomain.UnhandledException, Sub(s, e)
    Logger.Fatal(DirectCast(e.ExceptionObject, Exception), "App crash")
End Sub

AddHandler Application.ThreadException, Sub(s, e)
    Logger.Error(e.Exception, "UI thread error")
End Sub

' 4. AggregateException (parallel)
Try
    Parallel.ForEach(items, Sub(item) ProcessItem(item))
Catch ex As AggregateException
    For Each inner In ex.InnerExceptions
        Logger.Error(inner, "Task failed")
    Next
End Try

Patrones de Result (funcional)

El patrón Result devuelve éxito/fallo como un valor en lugar de lanzar — útil para fallos esperados (parseo, validación) donde las excepciones serían costosas y ruidosas. Combínelo con métodos de extensión (OnSuccess, OnFailure) para encadenamiento. Use excepciones para casos verdaderamente excepcionales; Result para fallos predecibles.

vb
' Alternative to exceptions for expected failures
Public Class Result(Of T)
    Public ReadOnly Property Value As T
    Public ReadOnly Property [Error] As String
    Public ReadOnly Property IsSuccess As Boolean

    Private Sub New(value As T, [error] As String, isSuccess As Boolean)
        Me.Value = value
        Me.Error = [error]
        Me.IsSuccess = isSuccess
    End Sub

    Public Shared Function Ok(value As T) As Result(Of T)
        Return New Result(Of T)(value, Nothing, True)
    End Function

    Public Shared Function Fail([error] As String) As Result(Of T)
        Return New Result(Of T)(Nothing, [error], False)
    End Function
End Class

' usage
Function ParseInt(s As String) As Result(Of Integer)
    Dim n As Integer
    If Integer.TryParse(s, n) Then
        Return Result(Of Integer).Ok(n)
    Else
        Return Result(Of Integer).Fail($"Cannot parse '{s}'")
    End If
End Function

' caller
Dim result = ParseInt("42")
If result.IsSuccess Then
    Console.WriteLine($"Got: {result.Value}")
Else
    Console.WriteLine($"Error: {result.Error}")
End If

' chain (extension methods)
' result.OnSuccess(Function(x) x * 2).OnFailure(Function(e) 0)

Integración de registro

Use un framework de registro (NLog, Serilog, log4net) — no Console.WriteLine. Los niveles de registro (Trace/Debug/Info/Warn/Error/Fatal) le permiten filtrar en tiempo de ejecución vía configuración. El registro estructurado (Serilog) preserva los nombres de campos — mejor para búsqueda en herramientas como ELK o Seq. Siempre incluya contexto (IDs de usuario, nombres de operaciones) en los mensajes de log.

vb
Imports NLog  ' or log4net, Serilog

' configure (in app.config or code)
' NLog.config:
' <targets><target name="file" type="File" fileName="app.log" /></targets>
' <rules><logger name="*" minlevel="Info" writeTo="file" /></rules>

Public Class MyService
    Private Shared ReadOnly Logger As Logger = LogManager.GetCurrentClassLogger()

    Public Sub ProcessData(data As String)
        Logger.Info($"Processing: {data}")

        Try
            Logger.Debug("Starting parse")
            Dim result = Parse(data)
            Logger.Info($"Done: {result}")
        Catch ex As FormatException
            Logger.Warn(ex, "Parse failed")
            Throw
        Catch ex As Exception
            Logger.Error(ex, "Unexpected error")
            Throw
        End Try
    End Sub
End Class

' structured logging (Serilog)
' Log.Information("User {UserId} logged in from {IP}", userId, ip)

' log levels (in order):
'   Trace < Debug < Info < Warn < Error < Fatal
' set minimum level in config to filter

' exception with context
Try
    SaveUser(user)
Catch ex As Exception
    Logger.Error(ex, "Failed to save user {UserId}", user.Id)
    Throw
End Try
20

ADO.NET y Base de Datos

Conexión y comandos

Siempre use parámetros (nunca concatenación de cadenas) para prevenir inyección SQL. Los bloques Using aseguran que las conexiones se cierren incluso en excepciones. ExecuteReader para SELECT; ExecuteNonQuery para INSERT/UPDATE/DELETE; ExecuteScalar para un solo valor (como COUNT o SCOPE_IDENTITY). El pool de conexiones es automático — abra tarde, cierre temprano.

vb
Imports System.Data.SqlClient  ' or Microsoft.Data.SqlClient for .NET+
' or MySql.Data.MySqlClient, System.Data.SQLite, etc.

Dim connStr As String = "Server=localhost;Database=mydb;Integrated Security=True;"

' basic query
Using conn As New SqlConnection(connStr)
    conn.Open()
    Using cmd As New SqlCommand("SELECT Id, Name FROM Users WHERE Age > @age", conn)
        cmd.Parameters.AddWithValue("@age", 18)
        Using reader = cmd.ExecuteReader()
            While reader.Read()
                Console.WriteLine($"{reader("Id")}: {reader("Name")}")
            End While
        End Using
    End Using
End Using

' insert with parameters
Using conn As New SqlConnection(connStr), cmd As New SqlCommand(
    "INSERT INTO Users (Name, Age) VALUES (@name, @age); SELECT SCOPE_IDENTITY();", conn)
    cmd.Parameters.AddWithValue("@name", "Alice")
    cmd.Parameters.AddWithValue("@age", 30)
    conn.Open()
    Dim newId = CInt(cmd.ExecuteScalar())
End Using

' update/delete
Using cmd As New SqlCommand("UPDATE Users SET Age = @age WHERE Id = @id", conn)
    cmd.Parameters.AddWithValue("@age", 31)
    cmd.Parameters.AddWithValue("@id", newId)
    Dim rowsAffected = cmd.ExecuteNonQuery()
End Using

Transacciones

Las transacciones aseguran atomicidad — todas las operaciones tienen éxito o todas fallan. BeginTransaction en una sola conexión es para una base de datos. TransactionScope maneja transacciones distribuidas a través de múltiples conexiones (e incluso múltiples bases de datos) — llame Complete para confirmar. Elija el nivel de aislamiento cuidadosamente: Serializable es el más seguro pero el más lento; ReadCommitted es el predeterminado.

vb
Dim connStr = "Server=localhost;Database=mydb;Integrated Security=True;"

' basic transaction
Using conn As New SqlConnection(connStr)
    conn.Open()
    Using tran = conn.BeginTransaction()
        Try
            Using cmd As New SqlCommand("", conn, tran)
                cmd.CommandText = "UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1"
                cmd.ExecuteNonQuery()

                cmd.CommandText = "UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2"
                cmd.ExecuteNonQuery()

                tran.Commit()
            End Using
        Catch ex As Exception
            tran.Rollback()
            Console.WriteLine($"Transaction rolled back: {ex.Message}")
            Throw
        End Try
    End Using
End Using

' TransactionScope (distributed transactions)
Using scope As New TransactionScope()
    Using conn1 As New SqlConnection(connStr1)
        ' work on db1
    End Using
    Using conn2 As New SqlConnection(connStr2)
        ' work on db2
    End Using
    scope.Complete()  ' commit all
End Using  ' rollback if Complete not called

' isolation levels
Using tran = conn.BeginTransaction(IsolationLevel.Serializable)
    ' ...
End Using

DataTable y DataAdapter

DataTable es una tabla en memoria — desconectada de la base de datos. SqlDataAdapter la llena y empuja las actualizaciones de vuelta. SqlCommandBuilder auto-genera comandos INSERT/UPDATE/DELETE para escenarios simples de tabla única. DataView filtra/ordena sin re-consultar. Para código nuevo, prefiera ORMs (EF Core, Dapper) sobre DataTables crudos.

vb
Dim connStr = "Server=localhost;Database=mydb;Integrated Security=True;"

' fill DataTable
Dim dt As New DataTable()
Using adapter As New SqlDataAdapter("SELECT * FROM Users", connStr)
    adapter.Fill(dt)
End Using

' access rows
For Each row As DataRow In dt.Rows
    Console.WriteLine($"{row("Id")}: {row("Name")}")
Next

' filter and sort (in-memory)
Dim view As New DataView(dt) With {
    .Sort = "Name ASC",
    .RowFilter = "Age > 25"
}
For Each row As DataRowView In view
    Console.WriteLine(row("Name"))
Next

' modify and update
Dim newRow = dt.NewRow()
newRow("Name") = "Carol"
newRow("Age") = 28
dt.Rows.Add(newRow)

dt.Rows(0)("Age") = 31              ' modify
dt.Rows(1).Delete()                 ' mark for deletion

' push changes back to DB
Using adapter As New SqlDataAdapter("SELECT * FROM Users", connStr)
    Dim builder As New SqlCommandBuilder(adapter)
    adapter.Update(dt)              ' generates INSERT/UPDATE/DELETE
End Using

' LINQ on DataTable
Dim young = From row In dt.AsEnumerable()
            Where row.Field(Of Integer)("Age") < 30
            Select row.Field(Of String)("Name")

Dapper micro-ORM

Dapper es el punto óptimo entre ADO.NET crudo (rápido pero verboso) y EF Core (productivo pero más lento). Auto-mapea columnas a propiedades. Use objetos anónimos para parámetros. Query<T> para listas; QuerySingle<T> para uno; Execute para non-queries. Pase una colección a Execute para operaciones por lotes. Multi-mapping maneja joins.

vb
Imports Dapper  ' NuGet: Install-Package Dapper

' Dapper is a lightweight ORM that extends IDbConnection

Dim connStr = "Server=localhost;Database=mydb;Integrated Security=True;"
Using conn As New SqlConnection(connStr)
    ' query
    Dim users = conn.Query(Of User)("SELECT * FROM Users WHERE Age > @age",
                                     New With {Key .age = 18}).ToList()

    ' single
    Dim user = conn.QuerySingle(Of User)("SELECT * FROM Users WHERE Id = @id",
                                          New With {Key .id = 5})

    ' execute
    Dim rows = conn.Execute("INSERT INTO Users (Name, Age) VALUES (@name, @age)",
                            New With {Key .name = "Alice", Key .age = 30})

    ' batch insert
    Dim newUsers = {
        New With {Key .name = "Bob", Key .age = 25},
        New With {Key .name = "Carol", Key .age = 28}
    }
    conn.Execute("INSERT INTO Users (Name, Age) VALUES (@name, @age)", newUsers)

    ' multi-result (one-to-many)
    Dim sql = "SELECT * FROM Companies; SELECT * FROM Employees;"
    Using multi = conn.QueryMultiple(sql)
        Dim companies = multi.Read(Of Company)().ToList()
        Dim employees = multi.Read(Of Employee)().ToList()
    End Using

    ' stored procedure
    Dim result = conn.Query(Of User)("sp_GetUsers", New With {Key .role = "admin"},
                                       commandType:=CommandType.StoredProcedure)
End Using

Fundamentos de EF Core

EF Core es el ORM completo — maneja relaciones, migraciones, seguimiento de cambios y consultas LINQ. Las propiedades DbSet son los puntos de entrada. SaveChanges persiste todos los cambios rastreados. Include carga ansiosamente entidades relacionadas (evita consultas N+1). Para escenarios de solo lectura, use AsNoTracking() para mejor rendimiento. Las migraciones gestionan cambios de esquema.

vb
Imports Microsoft.EntityFrameworkCore

' DbContext
Public Class AppDbContext
    Inherits DbContext

    Public Property Users As DbSet(Of User)
    Public Property Orders As DbSet(Of Order)

    Protected Overrides Sub OnConfiguring(options As DbContextOptionsBuilder)
        options.UseSqlServer("Server=localhost;Database=mydb;Integrated Security=True;")
    End Sub

    Protected Overrides Sub OnModelCreating(modelBuilder As ModelBuilder)
        modelBuilder.Entity(Of User)().
            HasKey(Function(u) u.Id).
            HasMany(Function(u) u.Orders).
            WithOne(Function(o) o.User).
            HasForeignKey(Function(o) o.UserId)
    End Sub
End Class

' CRUD
Using db As New AppDbContext()
    ' create
    db.Users.Add(New User With {.Name = "Alice", .Age = 30})
    db.SaveChanges()

    ' read
    Dim alice = db.Users.First(Function(u) u.Name = "Alice")
    Dim adults = db.Users.Where(Function(u) u.Age >= 18).ToList()

    ' update
    alice.Age = 31
    db.SaveChanges()

    ' delete
    db.Users.Remove(alice)
    db.SaveChanges()

    ' eager loading
    Dim usersWithOrders = db.Users.Include(Function(u) u.Orders).ToList()

    ' raw SQL
    Dim raw = db.Users.FromSqlRaw("SELECT * FROM Users WHERE Age > {0}", 18).ToList()

    ' transactions
    Using tran = db.Database.BeginTransaction()
        Try
            ' work
            db.SaveChanges()
            tran.Commit()
        Catch
            tran.Rollback()
            Throw
        End Try
    End Using
End Using
21

LINQ en VB

Sintaxis de consulta

La sintaxis de consulta LINQ de VB es similar a SQL y a menudo más legible para consultas complejas. From ... Where ... Select es el patrón básico. Múltiples cláusulas From hacen cross joins (filtre con Where). Group By crea grupos accesibles vía la palabra clave Group. Join requiere la palabra clave Equals (no On ... Equals ... como SQL).

vb
Dim nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

' query syntax (SQL-like)
Dim evens = From n In nums
            Where n Mod 2 = 0
            Select n
            ' List: 2, 4, 6, 8, 10

Dim squares = From n In nums
              Select n, Square = n * n

Dim pairs = From n In nums
            Where n > 3
            Order By n Descending
            Select n

' with multiple sources
Dim names = {"Alice", "Bob", "Carol"}
Dim ages = {30, 25, 42}
Dim people = From name In names
             From age In ages
             Where age > 26
             Select name, age

' group by
Dim orders = GetOrders()
Dim byCustomer = From o In orders
                 Group o By o.CustomerId Into Group
                 Select CustomerId, Total = Group.Sum(Function(o) o.Amount)

' join
Dim result = From u In users
             Join o In orders On u.Id Equals o.UserId
             Select u.Name, o.Amount

Sintaxis de método

La sintaxis de método usa métodos de extensión y lambdas — más compacta y componible que la sintaxis de consulta. Function(x) ... es la sintaxis lambda en VB. First lanza si está vacío; FirstOrDefault devuelve default (Nothing para tipos de referencia, 0 para Integer). Skip/Take es el patrón estándar de paginación. Ambas sintaxis compilan al mismo IL.

vb
Dim nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

' method syntax (fluent)
Dim evens = nums.Where(Function(n) n Mod 2 = 0).ToList()
Dim squares = nums.Select(Function(n) n * n).ToList()
Dim sorted = nums.OrderBy(Function(n) n).ToList()
Dim desc = nums.OrderByDescending(Function(n) n).ToList()

' aggregation
Dim sum = nums.Sum()
Dim avg = nums.Average()
Dim min = nums.Min()
Dim max = nums.Max()
Dim count = nums.Count()
Dim countEven = nums.Count(Function(n) n Mod 2 = 0)

' element access
Dim first = nums.First()
Dim firstEven = nums.First(Function(n) n Mod 2 = 0)
Dim maybeFirst = nums.FirstOrDefault(Function(n) n > 100)  ' 0 if not found
Dim single = nums.Single(Function(n) n = 5)

' take/skip
Dim top3 = nums.Take(3).ToList()
Dim skip3 = nums.Skip(3).ToList()
Dim page = nums.Skip(10).Take(5).ToList()  ' pagination

' distinct
Dim unique = nums.Distinct().ToList()
Dim union = nums.Union({11, 12}).ToList()
Dim intersect = nums.Intersect({5, 6, 7}).ToList()
Dim except = nums.Except({5, 6, 7}).ToList()

Agrupamiento y agregación

GroupBy devuelve IGrouping(Of Key, Element) — cada grupo tiene una Key y es enumerable por sí mismo. ToLookup crea una tabla de búsqueda (como un diccionario de múltiples valores). Los tipos anónimos (New With {Key .X = ...}) son geniales para proyecciones — Key hace la propiedad inmutable y parte de la igualdad. Aggregate es el combinador más flexible (pero menos legible).

vb
Public Class Order
    Public Property Id As Integer
    Public Property CustomerId As Integer
    Public Property Amount As Decimal
    Public Property Date As DateTime
End Class

Dim orders = GetOrders()

' group by
Dim byCustomer = orders.GroupBy(Function(o) o.CustomerId)
For Each grp In byCustomer
    Console.WriteLine($"Customer {grp.Key}: {grp.Count()} orders, total {grp.Sum(Function(o) o.Amount)}")
Next

' group with projection
Dim summary = orders.GroupBy(Function(o) o.CustomerId).
    Select(Function(g) New With {
        Key .CustomerId = g.Key,
        Key .OrderCount = g.Count(),
        Key .TotalAmount = g.Sum(Function(o) o.Amount),
        Key .AvgAmount = g.Average(Function(o) o.Amount),
        Key .MaxAmount = g.Max(Function(o) o.Amount)
    }).ToList()

' multi-level grouping
Dim byMonth = orders.GroupBy(Function(o) New With {Key o.Date.Year, Key o.Date.Month})
For Each grp In byMonth
    Console.WriteLine($"{grp.Key.Year}-{grp.Key.Month}: {grp.Sum(Function(o) o.Amount)}")
Next

' lookup (like a dictionary of lists)
Dim lookup = orders.ToLookup(Function(o) o.CustomerId)
Dim customerOrders = lookup(5).ToList()  ' all orders for customer 5

' aggregate with seed
Dim total = orders.Aggregate(0D, Function(acc, o) acc + o.Amount)

Unión de datos

Join es inner join (solo coincidentes). Group Join con DefaultIfEmpty da left join. Múltiples cláusulas Join se encadenan naturalmente. El Join de sintaxis de método toma cuatro lambdas: clave externa, clave interna, selector de resultado. Zip empareja elementos por posición — útil pero raro. Para joins complejos, la sintaxis de consulta es usualmente más clara que la de método.

vb
Public Class User
    Public Property Id As Integer
    Public Property Name As String
    Public Property DepartmentId As Integer
End Class

Public Class Department
    Public Property Id As Integer
    Public Property Name As String
End Class

Dim users = GetUsers()
Dim departments = GetDepartments()

' inner join
Dim result = From u In users
             Join d In departments On u.DepartmentId Equals d.Id
             Select u.Name, Department = d.Name

' method syntax
Dim result2 = users.Join(departments,
                          Function(u) u.DepartmentId,
                          Function(d) d.Id,
                          Function(u, d) New With {Key .Name = u.Name, Key .Dept = d.Name}).ToList()

' group join (left join)
Dim leftJoin = From d In departments
               Group Join u In users On d.Id Equals u.DepartmentId Into Group
               From u In Group.DefaultIfEmpty()
               Select Department = d.Name, User = If(u Is Nothing, "(none)", u.Name)

' multiple joins
Dim detail = From u In users
             Join d In departments On u.DepartmentId Equals d.Id
             Join o In orders On u.Id Equals o.UserId
             Select u.Name, d.Name, o.Amount

' zip (pair two sequences element-by-element)
Dim nums = {1, 2, 3}
Dim words = {"one", "two", "three"}
Dim pairs = nums.Zip(words, Function(n, w) $"{n}={w}").ToList()
' {"1=one", "2=two", "3=three"}

Ejecución diferida

LINQ usa ejecución diferida — las consultas se ejecutan al iterar, no al definirlas. Esto significa que cada For Each re-ejecuta la consulta. Para cachear resultados, llame ToList/ToArray. Evite efectos secundarios en lambdas de consulta (se ejecutan de forma impredecible). Algunos operadores (Count, First, Any) fuerzan ejecución inmediata. Any/All cortocircuitan.

vb
Dim nums = New List(Of Integer) From {1, 2, 3, 4, 5}

' query is NOT executed yet
Dim query = nums.Where(Function(n) n > 2).Select(Function(n) n * 10)

' executes when iterated
For Each n In query
    Console.WriteLine(n)            ' 30, 40, 50
Next

' executes AGAIN on each iteration
For Each n In query
    Console.WriteLine(n)            ' 30, 40, 50 again
Next

' force immediate execution
Dim list = query.ToList()           ' executes once, caches
Dim arr = query.ToArray()
Dim count = query.Count()           ' executes
Dim first = query.First()           ' executes

' side effects in query — be careful!
Dim i = 0
Dim bad = nums.Where(Function(n)
                        i += 1
                        Return n > 2
                     End Function)
' i changes every time you iterate bad

' eager vs deferred operators
'   Deferred: Where, Select, OrderBy, Skip, Take, Distinct
'   Eager:    ToList, ToArray, Count, First, Sum, Max, Any, All

' check if any/all match
Dim hasEven = nums.Any(Function(n) n Mod 2 = 0)
Dim allPositive = nums.All(Function(n) n > 0)
22

Multihilo

Tasks y async

Task.Run programa trabajo en un hilo del pool de hilos. Async/Await hace que el código asíncrono parezca sincrónico — el método se pausa en Await sin bloquear el hilo. Task.WhenAll espera a todos; Task.WhenAny devuelve el primero en terminar. Siempre pase CancellationToken a tareas de larga duración y llame ThrowIfCancellationRequested periódicamente.

vb
Imports System.Threading.Tasks

' Task creation
Dim t1 = Task.Run(Function() DoWork())
Dim t2 = Task.Run(Function() 42)  ' returns Integer
Dim result = Await t2  ' 42

' async function
Async Function FetchDataAsync() As Task(Of String)
    Using client As New HttpClient()
        Return Await client.GetStringAsync("https://api.example.com")
    End Using
End Function

' call it
Dim data = Await FetchDataAsync()

' parallel execution
Dim tasks = {
    Task.Run(Function() ComputePart1()),
    Task.Run(Function() ComputePart2()),
    Task.Run(Function() ComputePart3())
}
Dim results = Await Task.WhenAll(tasks)

' when any (first to finish)
Dim allTasks = urls.Select(Function(u) FetchAsync(u))
Dim firstDone = Await Task.WhenAny(allTasks)
Dim firstResult = Await firstDone

' continuation
Dim t = Task.Run(Function() 42).
    ContinueWith(Function(prev) prev.Result * 2)
Console.WriteLine(t.Result)  ' 84

' cancellation
Dim cts As New CancellationTokenSource()
Dim token = cts.Token
Dim task = Task.Run(Function()
    For i = 1 To 100
        token.ThrowIfCancellationRequested()
        Thread.Sleep(100)
    Next
End Function, token)
cts.CancelAfter(500)  ' cancel after 500ms

Bucles paralelos

Parallel.For/ForEach particionan el trabajo a través de hilos del pool — geniales para bucles CPU-bound. La sobrecarga thread-local es para reducciones (suma, max) sin bloquear en cada iteración. state.Break detiene iteraciones superiores; state.Stop se detiene inmediatamente. PLINQ (AsParallel) paraleliza LINQ — pero solo ayuda para operaciones CPU-intensive.

vb
Imports System.Threading.Tasks

' Parallel.For
Parallel.For(0, 100, Sub(i)
    ProcessItem(i)
End Sub)

' with options
Dim opts As New ParallelOptions With {
    .MaxDegreeOfParallelism = Environment.ProcessorCount
}
Parallel.For(0, 100, opts, Sub(i)
    ProcessItem(i)
End Sub)

' Parallel.ForEach
Dim items = GetItems()
Parallel.ForEach(items, Sub(item)
    ProcessItem(item)
End Sub)

' with state (thread-local)
Dim total As Integer = 0
Dim lockObj As New Object()
Parallel.For(0, 1000,
    Function() 0,  ' local init
    Function(i, state, localTotal)
        localTotal += Compute(i)
        Return localTotal
    End Function,  ' body
    Sub(localTotal)
        SyncLock lockObj
            total += localTotal
        End SyncLock
    End Sub  ' local finally
)

' break vs stop
Parallel.For(0, 100, Sub(i, state)
    If i = 50 Then state.Break()  ' stop iterations > 50
    If i = 25 Then state.Stop()   ' stop all immediately
End Sub)

' PLINQ
Dim nums = Enumerable.Range(1, 1000)
Dim squares = nums.AsParallel().
    Where(Function(n) n Mod 2 = 0).
    Select(Function(n) n * n).
    ToList()

Sincronización

SyncLock es la sincronización más simple — exclusión mutua. Interlocked es para operaciones atómicas int/long (más rápido que locks). Mutex puede ser nombrado y compartido entre procesos (aplicaciones de instancia única). Semaphore limita la concurrencia (ej., máximo 3 conexiones). ReaderWriterLockSlim permite múltiples lectores O un escritor — genial para caches de lectura intensiva.

vb
Imports System.Threading

' SyncLock (Monitor.Enter/Exit)
Private ReadOnly lockObj As New Object()
Private counter As Integer = 0

Sub Increment()
    SyncLock lockObj
        counter += 1
    End SyncLock
End Sub

' Interlocked (atomic operations)
Interlocked.Increment(counter)
Interlocked.Decrement(counter)
Interlocked.Add(counter, 10)
Interlocked.Exchange(counter, 0)
Dim oldVal = Interlocked.CompareExchange(counter, 42, 0)  ' if 0, set to 42

' Mutex (cross-process)
Using mtx As New Mutex(False, "Global\MyAppMutex")
    If mtx.WaitOne(0) Then
        ' got the mutex — only one instance runs
    Else
        ' another instance is running
    End If
End Using

' Semaphore (limit concurrent access)
Dim sem As New Semaphore(3, 3)  ' 3 concurrent
sem.WaitOne()
Try
    ' work
Finally
    sem.Release()
End Try

' ReaderWriterLockSlim
Dim rwLock As New ReaderWriterLockSlim()
rwLock.EnterReadLock()
Try
    ' multiple readers
Finally
    rwLock.ExitReadLock()
End Try
rwLock.EnterWriteLock()
Try
    ' exclusive write
Finally
    rwLock.ExitWriteLock()
End Try

Channels y productor-consumidor

Channel<T> (moderno) es la forma recomendada de construir pipelines productor-consumidor — completamente asíncrono, soporta backpressure vía capacidad limitada. ForEachAsync (consumo paralelo) permite a múltiples consumidores leer concurrentemente. BlockingCollection es el equivalente sincrónico más antiguo. ConcurrentQueue/Stack son colecciones lock-free para uso directo.

vb
Imports System.Threading.Channels

' create a channel (bounded for backpressure)
Dim channel = Channel.CreateBounded(Of Integer)(100)

' producer
Async Function ProducerAsync() As Task
    For i = 1 To 1000
        Await channel.Writer.WriteAsync(i)
    Next
    channel.Writer.Complete()
End Function

' consumer
Async Function ConsumerAsync() As Task
    Await ForEachAsync(channel.Reader.ReadAllAsync(),
                       Environment.ProcessorCount,
                       Async Function(item)
                           Await ProcessItemAsync(item)
                       End Function)
End Function

' multiple producers/consumers
Dim producers = Enumerable.Range(0, 3).Select(Function(i) ProducerAsync())
Dim consumers = Enumerable.Range(0, 3).Select(Function(i) ConsumerAsync())
Await Task.WhenAll(producers.Concat(consumers))

' BlockingCollection (older API, still useful)
Dim bc As New BlockingCollection(Of Integer)(100)
' producer
Task.Run(Sub()
    For i = 1 To 100
        bc.Add(i)
    Next
    bc.CompleteAdding()
End Sub)
' consumer
Task.Run(Sub()
    For Each item In bc.GetConsumingEnumerable()
        ProcessItem(item)
    Next
End Sub)

' thread-safe queue
Dim queue As New ConcurrentQueue(Of Integer)()
queue.Enqueue(1)
Dim val As Integer
queue.TryDequeue(val)

Timers y programación

Tres tipos de timer: System.Threading.Timer (pool de hilos, más eficiente), System.Timers.Timer (escenarios de servidor, eventos), Windows.Forms.Timer (hilo de UI, para actualizaciones de UI). Para código asíncrono, Task.Delay es más limpio que los timers. Siempre disponga los timers para prevenir fugas. Para trabajo asíncrono periódico, un bucle While con Task.Delay y un CancellationToken es el patrón más limpio.

vb
Imports System.Threading

' System.Threading.Timer (background thread)
Dim timer As New Timer(Sub(state)
    Console.WriteLine($"Tick at {DateTime.Now}")
End Sub, Nothing, TimeSpan.Zero, TimeSpan.FromSeconds(5))

' change interval
timer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(10))

' dispose
timer.Dispose()

' System.Timers.Timer (events, can use SynchronizingObject)
Dim eventTimer As New Timers.Timer With {
    .Interval = 5000,
    .AutoReset = True
}
AddHandler eventTimer.Elapsed, Sub(s, e)
    Console.WriteLine("Elapsed")
End Sub
eventTimer.Start()

' UI timer (WinForms — runs on UI thread)
Dim uiTimer As New Windows.Forms.Timer With {.Interval = 1000}
AddHandler uiTimer.Tick, Sub(s, e)
    Label1.Text = DateTime.Now.ToString()
End Sub
uiTimer.Start()

' one-shot delay
Async Function DoLaterAsync() As Task
    Await Task.Delay(5000)  ' 5 seconds
    Console.WriteLine("Done waiting")
End Function

' periodic with cancellation
Async Function PeriodicAsync(token As CancellationToken) As Task
    While Not token.IsCancellationRequested
        DoWork()
        Await Task.Delay(1000, token)
    End While
End Function
23

Serialización

JSON con System.Text.Json

System.Text.Json es el serializador JSON moderno y rápido integrado en .NET. Los atributos controlan la serialización: JsonPropertyName renombra, JsonJsonIgnore excluye. JsonNamingPolicy.CamelCase coincide con las convenciones de JavaScript. Para JSON grande, serialice a un stream (no a una cadena) para evitar cargar todo en memoria. JsonDocument parsea sin un tipo destino.

vb
Imports System.Text.Json
Imports System.Text.Json.Serialization

Public Class Person
    Public Property Id As Integer
    Public Property Name As String
    <JsonPropertyName("email")>
    Public Property Email As String
    <JsonIgnore>
    Public Property Password As String
    Public Property CreatedAt As DateTime
End Class

' serialize
Dim p As New Person With {.Id = 1, .Name = "Alice", .Email = "[email protected]", .CreatedAt = Date.Now}
Dim json As String = JsonSerializer.Serialize(p)

' with options
Dim opts As New JsonSerializerOptions With {
    .WriteIndented = True,
    .PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    .DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}
json = JsonSerializer.Serialize(p, opts)

' deserialize
Dim loaded = JsonSerializer.Deserialize(Of Person)(json)

' collections
Dim people As New List(Of Person) From {p}
Dim jsonList = JsonSerializer.Serialize(people)
Dim loadedList = JsonSerializer.Deserialize(Of List(Of Person))(jsonList)

' dynamic (JsonDocument)
Using doc As JsonDocument = JsonDocument.Parse(json)
    Dim name = doc.RootElement.GetProperty("name").GetString()
    Console.WriteLine(name)
End Using

' stream (large files)
Using fs As New FileStream("data.json", FileMode.Create)
    JsonSerializer.Serialize(fs, p)
End Using

Serialización XML

XmlSerializer es verboso pero flexible. Los atributos controlan nombres de elementos/atributos y estructura. Requiere un constructor sin parámetros y propiedades públicas de lectura/escritura. XmlArray/XmlArrayItem controlan colecciones anidadas. Para leer archivos XML grandes, prefiera XmlReader (streaming) sobre XmlSerializer (carga todo en memoria).

vb
Imports System.Xml.Serialization
Imports System.IO

<XmlRoot("person")>
Public Class Person
    <XmlElement("id")>
    Public Property Id As Integer
    <XmlElement("name")>
    Public Property Name As String
    <XmlAttribute("role")>
    Public Property Role As String
    <XmlIgnore>
    Public Property Password As String
    <XmlArray("orders")>
    <XmlArrayItem("order")>
    Public Property Orders As List(Of Order)
End Class

' serialize
Dim xs As New XmlSerializer(GetType(Person))
Using fs As New FileStream("p.xml", FileMode.Create)
    xs.Serialize(fs, p)
End Using

' with settings
Dim settings As New XmlWriterSettings With {
    .Indent = True,
    .IndentChars = "  ",
    .Encoding = Encoding.UTF8
}
Using fs As New FileStream("p.xml", FileMode.Create),
      xw As XmlWriter = XmlWriter.Create(fs, settings)
    xs.Serialize(xw, p)
End Using

' deserialize
Using fs As New FileStream("p.xml", FileMode.Open)
    Dim loaded = DirectCast(xs.Deserialize(fs), Person)
End Using

' collections
Dim xsList As New XmlSerializer(GetType(List(Of Person)))
Using fs As New FileStream("people.xml", FileMode.Create)
    xsList.Serialize(fs, people)
End Using

CSV

TextFieldParser maneja campos CSV entre comillas correctamente. Para escritura, escape campos que contengan comas, comillas o nuevas líneas envolviéndolos en comillas y duplicando las comillas internas. Para producción, use CsvHelper (NuGet) — maneja todos los casos extremos, soporta mapeo personalizado y es mucho más rápido. Siempre use una librería para CSV; el formato tiene casos extremos sorprendentes.

vb
Imports Microsoft.VisualBasic.FileIO

' read CSV (handles quoted fields)
Using parser As New TextFieldParser("data.csv")
    parser.TextFieldType = FieldType.Delimited
    parser.SetDelimiters(",")
    parser.HasFieldsEnclosedInQuotes = True

    While Not parser.EndOfData
        Dim fields = parser.ReadFields()
        Console.WriteLine($"{fields(0)}, {fields(1)}")
    End While
End Using

' write CSV (manual, with proper escaping)
Function CsvEscape(s As String) As String
    If s.Contains(",") OrElse s.Contains("""") OrElse s.Contains(vbCrLf) Then
        Return $"""{s.Replace("""", """""")}"""
    End If
    Return s
End Function

Dim lines As New List(Of String)
lines.Add("name,age,email")
For Each p In people
    lines.Add($"{CsvEscape(p.Name)},{p.Age},{CsvEscape(p.Email)}")
Next
File.WriteAllLines("out.csv", lines)

' CsvHelper (NuGet) — production-grade
' Install-Package CsvHelper
Imports CsvHelper
Using reader As New StreamReader("data.csv"),
      csv As New CsvReader(reader, CultureInfo.InvariantCulture)
    Dim records = csv.GetRecords(Of Person)().ToList()
End Using

Using writer As New StreamWriter("out.csv"),
      csv As New CsvWriter(writer, CultureInfo.InvariantCulture)
    csv.WriteRecords(people)
End Using

Binario y DataContract

DataContractSerializer es más tolerante a versiones que XmlSerializer (maneja campos añadidos/eliminados vía Order). Para serialización binaria de alto rendimiento, use Protocol Buffers (protobuf-net) o MessagePack — ambos son 10-100x más pequeños y rápidos que XML/JSON. Elija según necesidades: legible por humanos (JSON/XML) vs compacto/rápido (protobuf/messagepack).

vb
Imports System.Runtime.Serialization
Imports System.IO

' DataContract (version-tolerant)
<DataContract>
Public Class Person
    <DataMember(Order:=1)>
    Public Property Id As Integer
    <DataMember(Order:=2)>
    Public Property Name As String
    <DataMember(Order:=3, IsRequired:=False)>
    Public Property Email As String
End Class

' serialize to XML
Dim ser As New DataContractSerializer(GetType(Person))
Using fs As New FileStream("p.xml", FileMode.Create)
    ser.WriteObject(fs, p)
End Using

' deserialize
Using fs As New FileStream("p.xml", FileMode.Open)
    Dim loaded = DirectCast(ser.ReadObject(fs), Person)
End Using

' Protocol Buffers (protobuf-net, NuGet)
' Install-Package protobuf-net
<ProtoContract>
Public Class Person
    <ProtoMember(1)>
    Public Property Id As Integer
    <ProtoMember(2)>
    Public Property Name As String
End Class

' serialize
Using fs As New FileStream("p.bin", FileMode.Create)
    ProtoBuf.Serializer.Serialize(fs, p)
End Using

' deserialize
Using fs As New FileStream("p.bin", FileMode.Open)
    Dim loaded = ProtoBuf.Serializer.Deserialize(Of Person)(fs)
End Using

' MessagePack (MessagePack-CSharp, NuGet)
' compact, fast, schema-less
Dim bytes = MessagePack.MessagePackSerializer.Serialize(p)
Dim loaded = MessagePack.MessagePackSerializer.Deserialize(Of Person)(bytes)

Convertidores personalizados

Los convertidores personalizados manejan tipos que System.Text.Json no soporta nativamente (DateOnly, TimeSpan, tipos personalizados). JsonDerivedType (NET 7+) maneja polimorfismo — serializa el tipo de runtime con un discriminador. Los métodos ShouldSerialize* incluyen condicionalmente propiedades (como el patrón de XmlSerializer). Siempre registre convertidores en JsonSerializerOptions.

vb
Imports System.Text.Json
Imports System.Text.Json.Serialization

' custom converter for a type
Public Class DateOnlyConverter
    Inherits JsonConverter(Of DateOnly)

    Public Overrides Function Read(reader As ByRef Utf8JsonReader,
                                    type As Type, options As JsonSerializerOptions) As DateOnly
        Return DateOnly.Parse(reader.GetString())
    End Function

    Public Overrides Sub Write(writer As Utf8JsonWriter, value As DateOnly, options As JsonSerializerOptions)
        writer.WriteStringValue(value.ToString("yyyy-MM-dd"))
    End Sub
End Class

' register globally
Dim opts As New JsonSerializerOptions()
opts.Converters.Add(New DateOnlyConverter())

' or per-property
Public Class Event
    <JsonConverter(GetType(DateOnlyConverter))>
    Public Property Date As DateOnly
End Class

' polymorphic serialization
<JsonDerivedType(GetType(Dog), "dog")>
<JsonDerivedType(GetType(Cat), "cat")>
Public MustInherit Class Animal
    Public Property Name As String
End Class

Public Class Dog
    Inherits Animal
    Public Property Breed As String
End Class

Dim a As Animal = New Dog With {.Name = "Rex", .Breed = "Lab"}
Dim json = JsonSerializer.Serialize(a, GetType(Animal))
' {"$type":"dog","name":"Rex","breed":"Lab"}

' conditional serialization
Public Class User
    Public Property Name As String
    Public Property Password As String

    Public Function ShouldSerializePassword() As Boolean
        Return False  ' never serialize password
    End Function
End Class
24

Reflection

Inspección de tipos

Reflection le permite inspeccionar tipos en tiempo de ejecución. GetType(Type) para tipos en tiempo de compilación; obj.GetType() para tipos en runtime. BindingFlags filtran miembros (Public/NonPublic, Instance/Static). GetCustomAttributes devuelve atributos aplicados a un tipo. Reflection es lento — almacene en caché objetos Type y MemberInfo para uso repetido.

vb
Imports System.Reflection

Dim t As Type = GetType(String)
' or: Dim t = obj.GetType()

' basic info
Console.WriteLine($"Name: {t.Name}")
Console.WriteLine($"Full: {t.FullName}")
Console.WriteLine($"Base: {t.BaseType}")
Console.WriteLine($"IsClass: {t.IsClass}")
Console.WriteLine($"IsEnum: {t.IsEnum}")
Console.WriteLine($"IsGenericType: {t.IsGenericType}")

' interfaces
For Each i In t.GetInterfaces()
    Console.WriteLine($"Implements: {i.Name}")
Next

' properties
For Each prop In t.GetProperties()
    Console.WriteLine($"Property: {prop.Name} ({prop.PropertyType.Name})")
Next

' methods
For Each m In t.GetMethods(BindingFlags.Public Or BindingFlags.Instance)
    Console.WriteLine($"Method: {m.Name}({String.Join(", ", m.GetParameters().Select(Function(p) p.ParameterType.Name))})")
Next

' fields, constructors, events
t.GetFields()
t.GetConstructors()
t.GetEvents()

' attributes
For Each attr In t.GetCustomAttributes(False)
    Console.WriteLine($"Attribute: {attr.GetType().Name}")
Next

' generic type args
If t.IsGenericType Then
    For Each arg In t.GetGenericArguments()
        Console.WriteLine($"Type arg: {arg.Name}")
    Next
End If

Creación de instancias e invocación

Activator.CreateInstance crea objetos por tipo. MethodInfo.Invoke llama métodos — pase args como Object(). PropertyInfo.GetValue/SetValue acceden a propiedades. BindingFlags.NonPublic le permite alcanzar miembros privados (use con moderación — rompe encapsulación). MakeGenericMethod construye un método genérico en runtime. Esta es la base de los sistemas de plugins.

vb
Imports System.Reflection

' create instance
Dim t As Type = GetType(List(Of String))
Dim obj As Object = Activator.CreateInstance(t)

' with constructor args
Dim dt As DateTime = Activator.CreateInstance(GetType(DateTime), {2024, 12, 31})

' invoke method
Dim mi As MethodInfo = t.GetMethod("Add")
mi.Invoke(obj, New Object() {"hello"})

' property get/set
Dim pi As PropertyInfo = t.GetProperty("Count")
Dim count As Integer = CInt(pi.GetValue(obj))
pi.SetValue(obj, 10)  ' may throw if read-only

' field get/set
Dim fi As FieldInfo = GetType(MyClass).GetField("_private", BindingFlags.NonPublic Or BindingFlags.Instance)
Dim val = fi.GetValue(instance)
fi.SetValue(instance, newValue)

' generic method
Dim method As MethodInfo = GetType(Enumerable).GetMethod("Where")
Dim generic = method.MakeGenericMethod(GetType(Integer))
Dim filtered = generic.Invoke(Nothing, New Object() {nums, predicate})

' create delegate from method
Dim del As [Delegate] = [Delegate].CreateDelegate(GetType(Func(Of String, Boolean)), mi)

' load assembly
Dim asm As Assembly = Assembly.LoadFrom("plugin.dll")
Dim pluginType As Type = asm.GetType("MyPlugin")
Dim plugin As Object = Activator.CreateInstance(pluginType)

Atributos

Los atributos personalizados añaden metadatos a tipos y miembros. AttributeUsage controla dónde pueden aplicarse y si se permiten múltiples. GetCustomAttribute/GetCustomAttributes los leen en runtime vía reflection. Atributos integrados: Obsolete (advertencia/error del compilador), Conditional (inclusión en tiempo de compilación), Serializable. Los atributos potencian validación, mapeo ORM, serialización y más.

vb
Imports System

' define custom attribute
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Method, AllowMultiple:=True)>
Public Class DescriptionAttribute
    Inherits Attribute

    Public ReadOnly Property Text As String
    Public Sub New(text As String)
        Me.Text = text
    End Sub
End Class

' apply
<Description("User service")>
Public Class UserService
    <Description("Get user by ID")>
    Public Function GetUser(id As Integer) As User
        ' ...
    End Function
End Class

' read attributes
Dim t As Type = GetType(UserService)
Dim classAttr = CType(Attribute.GetCustomAttribute(t, GetType(DescriptionAttribute)), DescriptionAttribute)
Console.WriteLine(classAttr?.Text)

For Each mi In t.GetMethods()
    Dim methodAttr = CType(Attribute.GetCustomAttribute(mi, GetType(DescriptionAttribute)), DescriptionAttribute)
    If methodAttr IsNot Nothing Then
        Console.WriteLine($"{mi.Name}: {methodAttr.Text}")
    End If
Next

' built-in attributes
<Obsolete("Use NewMethod instead", True)>  ' True = error, False = warning
Sub OldMethod()
End Sub

<Conditional("DEBUG")>
Sub LogDebug(msg As String)
    Console.WriteLine(msg)
End Sub

<Serializable>
Public Class MyData
End Class

Emit y código dinámico

Reflection.Emit genera IL en runtime — potente pero de bajo nivel. DefineDynamicAssembly -> Module -> Type -> Method -> ILGenerator. Emit escribe opcodes IL. Los árboles de expresión son una alternativa de más alto nivel — construya expresiones como datos, luego Compile a un delegate. Use Emit para rendimiento extremo (serializadores personalizados); árboles de expresión para consultas dinámicas (EF Core).

vb
Imports System.Reflection
Imports System.Reflection.Emit

' build a dynamic assembly/module/type/method
Dim asmName As New AssemblyName("DynamicAsm")
Dim asm As AssemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run)
Dim mod As ModuleBuilder = asm.DefineDynamicModule("MainModule")
Dim type As TypeBuilder = mod.DefineType("Calculator", TypeAttributes.Public)

' add a method
Dim mb As MethodBuilder = type.DefineMethod("Add",
    MethodAttributes.Public Or MethodAttributes.Static,
    GetType(Integer), {GetType(Integer), GetType(Integer)})

Dim il As ILGenerator = mb.GetILGenerator()
il.Emit(OpCodes.Ldarg_0)            ' load first arg
il.Emit(OpCodes.Ldarg_1)            ' load second arg
il.Emit(OpCodes.Add)                ' add
il.Emit(OpCodes.Ret)                ' return

Dim createdType As Type = type.CreateType()
Dim result As Integer = CInt(createdType.GetMethod("Add").Invoke(Nothing, {5, 3}))
Console.WriteLine(result)  ' 8

' expression trees (higher-level)
Imports System.Linq.Expressions

Dim xParam As ParameterExpression = Expression.Parameter(GetType(Integer), "x")
Dim yParam As ParameterExpression = Expression.Parameter(GetType(Integer), "y")
Dim body As BinaryExpression = Expression.Add(xParam, yParam)
Dim addFunc As Func(Of Integer, Integer, Integer) =
    Expression.Lambda(Of Func(Of Integer, Integer, Integer))(body, {xParam, yParam}).Compile()
Console.WriteLine(addFunc(5, 3))  ' 8

Consideraciones de rendimiento

Reflection es 100-1000x más lento que llamadas directas. Siempre almacene en caché objetos MemberInfo. Para rutas críticas, compile a delegates vía árboles de expresión — casi tan rápido como llamadas directas. Los source generators (.NET moderno) pueden hacer reflection en tiempo de compilación, eliminando el coste de runtime completamente. Use reflection para frameworks (serializadores, ORMs, DI); evítelo en lógica de negocio.

vb
Imports System.Reflection

' reflection is slow — cache and optimize

' BAD: re-fetch on every call
For Each item In items
    Dim prop = item.GetType().GetProperty("Name")
    Dim name = CStr(prop.GetValue(item))
Next

' GOOD: cache PropertyInfo
Dim propInfo As PropertyInfo = GetType(Item).GetProperty("Name")
For Each item In items
    Dim name = CStr(propInfo.GetValue(item))
Next

' BEST: compile to delegate (10-100x faster)
Dim getter = CreateGetter(Of Item, String)("Name")
For Each item In items
    Dim name = getter(item)
Next

Function CreateGetter(Of T, TResult)(propName As String) As Func(Of T, TResult)
    Dim param = Expression.Parameter(GetType(T), "obj")
    Dim body = Expression.Property(param, propName)
    Return Expression.Lambda(Of Func(Of T, TResult))(body, param).Compile()
End Function

' source generators (modern alternative)
' compile-time reflection — no runtime cost
' Partial Class MyService
'     <GenerateReflection>
'     Public Shared Sub PrintMethods()
'         ' generated code knows the types at compile time
'     End Sub
' End Class

' when to use reflection:
'   - serializers (JSON, XML)
'   - ORMs (mapping columns to properties)
'   - DI containers (constructor injection)
'   - plugin systems
'   - testing frameworks
' avoid for hot paths — cache or compile
25

Interop

P/Invoke (llamando a DLLs de Win32)

P/Invoke le permite llamar funciones de DLLs nativas. DllImport especifica la librería y opciones. CharSet.Unicode maneja el marshaling de cadenas. ByRef para parámetros de salida. StructLayout(Sequential) asegura que los structs coincidan con el layout de C. SetLastError=True le permite consultar el último error de Win32 vía Marshal.GetLastWin32Error(). Siempre envuelva llamadas nativas en una clase NativeMethods.

vb
Imports System.Runtime.InteropServices

Public Class NativeMethods
    ' MessageBox from user32.dll
    <DllImport("user32.dll", CharSet:=CharSet.Unicode, SetLastError:=True)>
    Public Shared Function MessageBox(hWnd As IntPtr, text As String, caption As String, type As UInteger) As Integer
    End Function

    ' Beep from kernel32
    <DllImport("kernel32.dll")>
    Public Shared Function Beep(freq As UInteger, duration As UInteger) As Boolean
    End Function

    ' GetSystemMetrics
    <DllImport("user32.dll")>
    Public Shared Function GetSystemMetrics(nIndex As Integer) As Integer
    End Function
End Class

' usage
NativeMethods.MessageBox(IntPtr.Zero, "Hello from native!", "Test", 0)
NativeMethods.Beep(440, 500)  ' 440 Hz for 500ms

' constants
Public Const SM_CXSCREEN As Integer = 0
Public Const SM_CYSCREEN As Integer = 1
Dim screenWidth = NativeMethods.GetSystemMetrics(SM_CXSCREEN)

' structures
<StructLayout(LayoutKind.Sequential)>
Public Structure POINT
    Public X As Integer
    Public Y As Integer
End Structure

<DllImport("user32.dll")>
Public Shared Function GetCursorPos(ByRef lpPoint As POINT) As Boolean
End Function

Dim p As POINT
GetCursorPos(p)
Console.WriteLine($"Cursor at ({p.X}, {p.Y})")

Interop con COM

El interop COM le permite llamar a Office y otras librerías COM. Añada una referencia al assembly de interop (o use NuGet). SIEMPRE libere objetos COM con Marshal.ReleaseComObject — no se recolectan como basura prontamente, llevando a procesos Excel "fantasma". El late binding (CreateObject) evita referencias en tiempo de compilación pero pierde IntelliSense y seguridad de tipos.

vb
Imports System.Runtime.InteropServices
Imports Excel = Microsoft.Office.Interop.Excel

' add reference: Microsoft.Office.Interop.Excel (or via NuGet)

Sub ExportToExcel(data As DataTable)
    Dim app As Excel.Application = Nothing
    Dim wb As Excel.Workbook = Nothing
    Dim ws As Excel.Worksheet = Nothing

    Try
        app = New Excel.Application()
        app.Visible = False
        wb = app.Workbooks.Add()
        ws = CType(wb.Worksheets(1), Excel.Worksheet)

        ' write headers
        For c = 0 To data.Columns.Count - 1
            ws.Cells(1, c + 1) = data.Columns(c).ColumnName
        Next

        ' write data
        For r = 0 To data.Rows.Count - 1
            For c = 0 To data.Columns.Count - 1
                ws.Cells(r + 2, c + 1) = data.Rows(r)(c).ToString()
            Next
        Next

        ws.SaveAs("C:	empexport.xlsx")
    Finally
        ' ALWAYS release COM objects
        If ws IsNot Nothing Then Marshal.ReleaseComObject(ws)
        If wb IsNot Nothing Then
            wb.Close(False)
            Marshal.ReleaseComObject(wb)
        End If
        If app IsNot Nothing Then
            app.Quit()
            Marshal.ReleaseComObject(app)
        End If
    End Try
End Sub

' late binding (no reference needed)
Dim lateApp As Object = CreateObject("Excel.Application")
lateApp.Visible = True
lateApp.Quit()

Interop con C#

VB y C# pueden llamarse mutuamente libremente — compilan al mismo IL. Añada una referencia de proyecto o librería compartida. Vigile la sensibilidad a mayúsculas (VB es insensible a mayúsculas), la sintaxis de indexación y ref/out (VB usa ByRef). Las características de C# como código unsafe, ciertos genéricos y pattern matching no tienen equivalente en VB — use C# para esas partes.

vb
' VB and C# interoperate seamlessly in the same project (multi-targeting)
' Add a C# project reference, or use a shared library

' C# code (in CSharpLib.dll):
'   namespace Utils
'   {
'       public class Calculator
'       {
'           public int Add(int a, int b) => a + b;
'           public static int Multiply(int a, int b) => a * b;
'       }
'   }

' VB usage:
Imports Utils

Dim calc As New Calculator()
Dim sum = calc.Add(2, 3)              ' 5
Dim product = Calculator.Multiply(4, 5)  ' 20

' differences to watch:
'   - VB is case-insensitive; C# is case-sensitive
'   - VB uses () for indexing; C# uses []
'   - VB And/Or are bitwise (AndAlso/OrElse for short-circuit)
'   - C# uses camelCase often; VB PascalCase by convention
'   - C# has unsafe code and pointers; VB doesn't
'   - C# has yield return; VB has Yield (similar)

' consuming async C# methods
Dim result = Await csharpObj.GetDataAsync()

' ref/out parameters
' C#: void TryParse(string s, out int result)
' VB: Integer.TryParse(s, result)  ' result is ByRef

' using C# extension methods
Imports CSharpExtensions
Dim s = "hello".Capitalize()  ' extension method from C# lib

Llamando a Python y otros lenguajes

Para interop con Python, Python.NET embebe CPython en .NET — acceso directo a objetos. Más simple: ejecute python.exe con Process.Start. R.NET hace lo mismo para R. ClearScript embebe V8 para JavaScript. Elija según la profundidad de integración: ejecute scripts únicos; embeba para integración estrecha. Vigile el rendimiento (llamadas cross-runtime son lentas).

vb
' Python via Python.NET (NuGet: Python.Runtime)
' Install-Package Python.NET

' After setup:
' PythonEngine.Initialize()
' Using Py.GIL()
'     Dim np As PyObject = Py.Import("numpy")
'     Dim arr = np.array(New Integer() {1, 2, 3, 4, 5})
'     Dim mean = np.mean(arr)
'     Console.WriteLine(mean)
' End Using

' Or run Python script via process
Dim psi As New ProcessStartInfo With {
    .FileName = "python.exe",
    .Arguments = "script.py arg1 arg2",
    .UseShellExecute = False,
    .RedirectStandardOutput = True,
    .CreateNoWindow = True
}
Using p As Process = Process.Start(psi)
    Dim output = p.StandardOutput.ReadToEnd()
    p.WaitForExit()
    Console.WriteLine(output)
End Using

' R via R.NET (NuGet)
' Install-Package R.NET
' Dim engine = REngine.GetInstance()
' engine.Evaluate("x <- c(1, 2, 3, 4, 5)")
' Dim mean = engine.Evaluate("mean(x)").AsNumeric()[0]

' JavaScript via ClearScript (V8)
' Install-Package Microsoft.ClearScript.V8
' Dim engine As New V8ScriptEngine()
' engine.Execute("function add(a, b) { return a + b; }")
' Dim result = engine.Script.add(2, 3)

' command-line tools
Dim result = Process.Start("ffmpeg", "-i input.mp4 output.wav")

Memoria y punteros

Marshal.AllocHGlobal/FreeHGlobal asignan/liberan memoria no administrada — siempre empárelos en Try/Finally. Marshal.Copy mueve arrays entre memoria administrada y no administrada. VB no soporta código unsafe (use C# para eso, o use métodos Marshal). Span(Of T) (.NET moderno) da rendimiento similar a punteros con seguridad de memoria — preferido sobre punteros crudos.

vb
Imports System.Runtime.InteropServices

' allocate native memory
Dim ptr As IntPtr = Marshal.AllocHGlobal(1024)  ' 1KB
Try
    ' copy data
    Marshal.WriteByte(ptr, 0, 42)
    Marshal.WriteInt32(ptr, 4, 1234)

    ' copy array
    Dim arr As Integer() = {1, 2, 3, 4}
    Marshal.Copy(arr, 0, ptr, arr.Length)

    ' read back
    Dim b = Marshal.ReadByte(ptr, 0)
    Dim i = Marshal.ReadInt32(ptr, 4)

    Dim arr2(3) As Integer
    Marshal.Copy(ptr, arr2, 0, 4)
Finally
    Marshal.FreeHGlobal(ptr)
End Try

' string marshaling
Dim strPtr As IntPtr = Marshal.StringToHGlobalUni("hello")
Try
    ' pass to native function
Finally
    Marshal.FreeHGlobal(strPtr)
End Try

' pointer-sized fields
<StructLayout(LayoutKind.Sequential)>
Public Structure HandleInfo
    Public Handle As IntPtr
    Public Size As IntPtr
End Structure

' unsafe code (C# only — VB doesn't support unsafe)
' in C#: unsafe { int* p = &x; *p = 42; }
' in VB: use Marshal class or write a C# helper

' Span(Of T) — modern memory-safe pointer
Dim span As Span(Of Byte) = New Byte(1023) {}
span(0) = 42
Dim intSpan = MemoryMarshal.Cast(Of Byte, Integer)(span)

Was this helpful?