Skip to content

Visual Basic Шпаргалка

Простой, событийно-ориентированный язык для Windows-приложений.

01

Переменные, типы и операторы

Объявление переменных и встроенные типы

VB.NET статически типизирован с объявлениями Dim. Частые типы: String, Integer (32-бит), Long (64-бит), Double (64-бит float), Decimal (высокая точность для финансовых), Boolean, Date, Char. Используйте суффикс типа (D для Decimal, F для Single, L для Long) для принудительных литеральных типов. Option Explicit On форсирует объявление переменных (предотвращает опечатки). Option Infer On включает type inference с Dim без явных типов. Nullable-типы (Integer?) оборачивают value-типы, чтобы они могли хранить Nothing — полезно для БД и API.

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

Константы, Enum и Structures

Const объявляет константы времени компиляции (неявно Shared, нельзя изменить). Enum определяет именованные целочисленные константы — используйте [Enum].Parse для конвертации строк и CInt для числового значения. Скобочная нотация [Error] экранирует зарезервированные ключевые слова. Атрибут <Flags> помечает enum как bit field, позволяя комбинацию через Or/And — распространено для разрешений и опций. Всегда указывайте underlying type (As Integer) для flags, чтобы контролировать ширину битов.

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

Конвертация типов и кастинг

Расширяющие конвертации (Integer в Double) неявные и безопасные. Сужающие (Double в Integer) требуют явного кастинга. CInt/CStr/CDate/CDec — функции конвертации VB (CType — generic-версия). DirectCast строжайший — работает только при точном совпадении runtime-типа. TryCast возвращает Nothing при сбое вместо выброса (только ссылочные типы). Всегда предпочитайте TryParse вместо Parse для пользовательского ввода, чтобы избежать исключений. IsNumeric/IsDate — удобные валидаторы. CInt округляет (banker's rounding), а Int() пола.

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

Операторы и выражения

VB использует \ для целочисленного деления и / для вещественного (в отличие от C#, где / и %). Mod — оператор остатка. VB использует = и для присваивания, и для сравнения равенства (контекст определяет). And/Or вычисляют оба операнда; AndAlso/OrElse короткозамкнутые (предпочтительны для производительности и избегания null references). & — оператор конкатенации строк (не +, который может делать числовое сложение). Используйте AndAlso/OrElse по умолчанию для короткого замыкания и избегания ошибок вроде проверки 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

Ввод, вывод и форматирование

Console.WriteLine/Write — базовые методы вывода. Строковая интерполяция ($"...") — современный, читаемый способ встраивать выражения — поддерживает спецификаторы формата вроде :F2 (2 знака), :C (валюта), :P (процент), :X (hex). String.Format использует позиционные плейсхолдеры {0}, {1} с опциональным выравниванием ({0,-10} = по левому краю, ширина 10). Для циклов, строящих большие строки, используйте StringBuilder (не &), чтобы избежать создания многих промежуточных строк. Integer.Parse выбрасывает на невалидном вводе; используйте TryParse для безопасности.

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

Управляющий поток

If...Then...Else и тернарный

If...Then...ElseIf...Else — условие VB. AndAlso/OrElse короткозамкнутые (не вычисляют правую сторону, если не нужно) — всегда предпочитайте их And/Or для условий. Функция If() (VB 14+) — тернарный оператор: If(condition, trueVal, falseVal). If() с двумя аргументами — оператор null-coalescing: If(maybeNull, defaultValue). Избегайте старой функции IIf() — она всегда вычисляет обе ветви и возвращает Object (boxing). Однострочный If не требует 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 — switch VB — чище, чем цепочки If...ElseIf. Case поддерживает несколько значений (через запятую), диапазоны (To) и операторы сравнения (Is >= 90). Case Else — ветвь по умолчанию. В отличие от switch C#, VB не проваливается — каждый Case работает независимо и Break не нужен. Select Case работает со строками, числами и даже объектами (через Is). Идеально для диспетчеризации по дискретным значениям.

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 и итераторы

For...To...Next итерирует диапазон с опциональным Step (отрицательный для обратного отсчёта). For Each итерирует любой IEnumerable — чище для коллекций. Exit For ломает цикл; Continue For пропускает к следующей итерации. Iterator-функции (с Yield) производят последовательности лениво — значения генерируются по требованию, что эффективно по памяти для больших или бесконечных последовательностей. Yield возвращает одно значение, затем возобновляет с того же места на следующей итерации. Это эквивалент yield return в 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 и While

Do While выполняется, пока условие True; Do Until — пока условие False (пока не станет True). Размещение условия внизу (Loop While/Until) гарантирует, что тело выполнится хотя бы один раз — полезно для меню и валидации ввода. While...End While — старая, более простая форма. Exit Do ломает; Continue Do пропускает к следующей итерации. Выбирайте Do While, когда можете не войти в цикл; Do...Loop While, когда нужно выполниться хотя бы раз.

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 и GoTo

With...End With — синтаксический сахар для доступа к нескольким членам одного объекта без повторения имени переменной — полезно для инициализации и билдеров. GoTo прыгает к метке; современный VB не рекомендует его, кроме выхода из глубоко вложенных циклов или в устаревшей обработке On Error GoTo. On Error GoTo — обработка ошибок в стиле VB6 (Resume Next пропускает строку с ошибкой) — предпочитайте структурированный Try/Catch в новом коде. Объект Err хранит описание и номер последней ошибки.

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

Процедуры, функции и события

Основы Sub и Function

Sub выполняет действие (без возвращаемого значения); Function возвращает значение. Return выходит из функции со значением. Старый стиль «присвоить имени функции» (Multiply = a * b) всё ещё работает, но Return яснее. При вызове Sub скобки опциональны (Greet "Alice" или Greet("Alice")). Функции, вызываемые ради возвращаемого значения, требуют скобок. По умолчанию VB позволяет вызывать функции без присвоения возвращаемого значения, но это может путать — будьте явны.

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)

Параметры: ByVal, ByRef, Optional, ParamArray

ByVal (по умолчанию) передаёт копию — изменения внутри не влияют на вызывающего. ByRef передаёт ссылку — изменения ВЛИЯЮТ на вызывающего (как ref в C#). Используйте ByRef, когда процедура должна модифицировать переменную вызывающего или возвращать несколько значений. Optional-параметры имеют значения по умолчанию и должны идти после обязательных. ParamArray принимает переменное число аргументов (как params в C#) и должен быть последним. Именованные аргументы (name:=value) улучшают читаемость и позволяют пропускать optional.

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)

Лямбда-выражения и делегаты

Лямбды — inline-функции: Function(...) для возвращающих значение, Sub(...) для не возвращающих. Func(Of T, TResult) — встроенный тип делегата для функций; Action(Of T) — для sub (без возврата). AddressOf создаёт делегат из именованного метода. Лямбды необходимы для LINQ (Where, Select, OrderBy принимают функции как аргументы). Многострочные лямбды используют Function...End Function (или Sub...End Sub). Делегаты — типобезопасные указатели на функции — полезны для колбэков, событий и паттернов стратегии.

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")

События и обработчики событий

События включают паттерн наблюдателя: класс вызывает события, а подписчики обрабатывают их. Объявляйте через Event, запускайте через RaiseEvent, обрабатывайте через Handles (WithEvents, время компиляции) или AddHandler (runtime, динамически). WithEvents + Handles декларативны, но переменная должна быть на уровне модуля/класса. AddHandler/RemoveHandler позволяют подписываться/отписываться во время выполнения — полезно для динамического связывания. Обработчики событий должны соответствовать сигнатуре события. События — основа Windows Forms и WPF (клики кнопок, загрузка формы и т.д.).

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

Перегрузка, рекурсия и Shared-члены

Перегрузка позволяет нескольким методам иметь одно имя с разными списками параметров — компилятор выбирает лучшее совпадение. Ключевое слово Overloads требуется при перегрузке через наследование. Рекурсия — когда функция вызывает себя — нужен base case для завершения. Остерегайтесь stack overflow при глубокой рекурсии. Shared (C# static) члены принадлежат типу, а не экземплярам — вызывайте через ClassName.Member без создания объекта. Shared-конструкторы запускаются один раз на тип, идеальны для инициализации статических данных.

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

Строки и обработка текста

Основы строк и неизменяемость

Строки в VB.NET неизменяемы — каждый метод (ToUpper, Replace и т.д.) возвращает НОВУЮ строку; оригинал не меняется. Используйте = для сравнения равенства (VB-специфично). IndexOf возвращает первый индекс подстроки (-1 если не найдена). Split разбивает строку в массив; Join объединяет массив в строку. Trim удаляет пробелы (или указанные символы) с обоих концов. Для сравнения без учёта регистра используйте StringComparison.OrdinalIgnoreCase. Для тяжёлых строковых манипуляций в циклах используйте 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 и производительность

String неизменяема — каждый & или += создаёт новую строку, копируя всё предыдущее содержимое. В циклах это O(n²). StringBuilder изменяема — Append/Insert/Remove модифицируют тот же буфер, делая её O(n). Всегда используйте StringBuilder для циклов, строящих большие строки. StringBuilder цепочка (Append возвращает тот же экземпляр). Устанавливайте начальный Capacity, если знаете примерный размер, чтобы избежать перераспределений. Для разовых конкатенаций (не в циклах) & нормален и читаемее.

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

Строковая интерполяция и форматирование

Строковая интерполяция ($"...") — современный способ встраивать переменные и выражения — читаемая и поддерживает спецификаторы формата после двоеточия (:C валюта, :F2 фиксированные знаки, :N разделители тысяч, :X hex, :D5 с нулями, :P процент). Форматы дат: yyyy-MM-dd, HH:mm:ss, dddd (полное имя дня). Используйте String.Format для позиционных плейсхолдеров или когда строка формата строится динамически. Для culture-aware приложений (интернационализация) передавайте CultureInfo в ToString/Format для управления разделителями, символами валюты и форматами дат.

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 €

Регулярные выражения

Regex.IsMatch проверяет совпадение; Regex.Matches находит все; Regex.Match находит первое. Группы захватывают части совпадения скобками — доступ через Groups(1), Groups(2). Regex.Replace подставляет совпадения (используйте $1, $2 для ссылок на группы). RegexOptions.Compiled компилирует паттерн для более быстрого повторного сопоставления. Частые паттерны: \d (цифра), \w (словесный символ), \s (пробел), + (один или более), * (ноль или более), {n} (точно n), ^/$ (начало/конец). Всегда валидируйте пользовательский ввод regex для email, телефонов и т.д.

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

Операции с Char и кодирование

Char — один Unicode-символ (2 байта). Используйте суффикс c для char-литералов ("A"c). Методы Char (IsDigit, IsLetter, IsUpper) полезны для валидации. Asc/Chr конвертируют между char и ASCII-кодами. ToCharArray конвертирует строку в изменяемый char-массив (сами строки неизменяемы). Encoding.UTF8.GetBytes конвертирует строки в byte-массивы (необходимо для файлового I/O и сетей). Base64 (Convert.ToBase64String) кодирует бинарные данные как текст — используется в data URI, email-вложениях и API.

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

Массивы и коллекции

Массивы: объявление, индексация и методы

Массивы VB 0-индексированы и фиксированного размера. Dim arr(n) создаёт n+1 элементов (0 to n). Литералы массивов используют { }. Многомерные массивы (,) прямоугольные; jagged-массивы ()() — массивы массивов (каждая строка может иметь разную длину). ReDim изменяет размер массива — Preserve сохраняет существующие значения (без него значения очищаются). Array.Sort/Reverse/Clear/IndexOf/Copy — статические помощники. Используйте List(Of T) вместо массивов, когда размер часто меняется.

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): динамические массивы

List(Of T) — основная динамическая коллекция — растёт автоматически при Add. Count даёт число элементов (не capacity). Add/Insert/Remove/RemoveAt/Contains/IndexOf — основные методы. Find/FindAll/Exists принимают предикат (лямбду) для кастомного поиска. Sort может принимать делегат Comparison для кастомного упорядочивания. List(Of T) оборачивает массив внутри и изменяет размер при заполнении (амортизированный O(1) Add). Конвертируйте в массив через ToArray(), когда нужна коллекция фиксированного размера.

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 и Queue/Stack

Dictionary(Of TKey, TValue) отображает ключи в значения — O(1) поиск по ключу. Используйте TryGetValue, чтобы избежать исключений и двойных lookup (ContainsKey + indexer). Итерируйте через KeyValuePair. HashSet(Of T) хранит уникальные элементы с O(1) add/contains — используйте для дедупликации и set-операций (UnionWith, IntersectWith, ExceptWith). Queue — FIFO (Enqueue/Dequeue); Stack — LIFO (Push/Pop). Все эти коллекции generic (типобезопасные, без boxing). Выбирайте по паттерну доступа: lookup → Dictionary, уникальность → HashSet, порядок → 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: Query и Method-синтаксис

LINQ (Language Integrated Query) трансформирует коллекции декларативно. Query-синтаксис (From...Where...Select) SQL-подобный и читаем для сложных запросов. Method-синтаксис (.Where().Select()) fluent и хорошо связывается. Оба компилируются в один код. Ключевые операторы: Where (фильтр), Select (трансформация), OrderBy (сортировка), GroupBy (группировка), Take/Skip (пагинация), First/FirstOrDefault (поиск), Any/All (проверка), Sum/Average/Max/Min (агрегация). FirstOrDefault возвращает default (0, Nothing), если нет совпадения — избегает исключений. LINQ работает на любом IEnumerable (массивы, списки, словари).

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

Инициализация коллекций и кортежи

Инициализаторы коллекций (From { }) создают и наполняют коллекции одним оператором. Кортежи (VB 2017+) группируют несколько значений — именованные поля (X, Y) яснее, чем Item1, Item2. Кортежи идеальны для возврата нескольких значений из функции без определения класса. Деконструкция (Dim (a, b) = tuple) разбивает кортеж в переменные. ToDictionary конвертирует список в словарь (селектор ключа + селектор значения). Кортежи — value-типы (struct), поэтому эффективны для небольших, временных группировок.

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

Объектно-ориентированное программирование

Класс, поля и свойства

Классы связывают данные (поля/свойства) и поведение (методы). Auto-implemented properties (Property X As Type) авто-генерируют скрытое backing-field — лаконично для простых данных. Полные свойства (блоки Get/Set) позволяют валидацию, вычисления или побочные эффекты. ReadOnly-свойства имеют только Get. Me ссылается на текущий экземпляр (как this в C#). Конструкторы (Sub New) инициализируют объекты. Свойства выглядят как поля для вызывающих (p.Name), но выполняют код — эта инкапсуляция — ключевая выгода ООП.

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)

Наследование и MyBase

Inherits устанавливает отношение IS-A (Dog IS Animal). MyBase.New() вызывает базовый конструктор (должен быть первым оператором). Overridable (база) + Overrides (производный) включают полиморфизм — производный метод запускается даже при доступе через базовую ссылку. Shadows (или Overloads) скрывает член базы вместо переопределения (без полиморфизма). Используйте Overridable/Overrides для истинного полиморфизма; Shadows только когда нужно скрыть член, который нельзя изменить. VB поддерживает одиночное наследование (один базовый класс), но множественные интерфейсы.

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)

Интерфейсы и полиморфизм

Интерфейсы определяют контракт (методы, свойства) без реализации — классы их Implement. В отличие от наследования, класс может реализовать несколько интерфейсов. Ключевое слово Implements связывает член с его интерфейсным объявлением (VB-специфичный синтаксис). Интерфейсы включают полиморфизм: код может работать с любым IDrawable, не зная, Circle это или Square. TypeOf x Is T проверяет runtime-тип; DirectCast кастит (выбрасывает, если невалидно). Используйте интерфейсы для развязки кода: зависите от IDrawable, не Circle. Это принцип инверсии зависимостей.

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

Дженерики и ограничения

Дженерики (Of T) позволяют писать типобезопасный, переиспользуемый код для любого типа — без boxing, без кастов, проверка типов во время компиляции. List(Of T), Dictionary(Of K,V), Stack(Of T) — generic-коллекции. Ограничения ограничивают параметр типа: Class (ссылочный тип), Structure (value-тип), New (имеет конструктор без параметров — позволяет New T()), конкретный базовый класс или интерфейс. Дженерики избегают потери производительности на boxing (value-типы) и хрупкости кастов Object. Всегда предпочитайте generic-коллекции (List(Of T)) не-generic (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

Partial-классы и пространства имён

Partial-классы разделяют класс по нескольким файлам — компилятор объединяет их. Полезно для отделения сгенерированного кода (designer-файлы) от рукописного или разделения больших классов. Пространства имён организуют типы и предотвращают коллизии имён (MyApp.Models.User vs MyApp.Services.User). Imports вводит имена пространств имён в scope (без полной квалификации). RootNamespace (настройка проекта) оборачивает все типы — установите в имя компании/продукта. Соглашения: пространства имён соответствуют структуре папок; одно пространство на логическую область.

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

Обработка ошибок и исключения

Try...Catch...Finally

Try/Catch/Finally — структурированная обработка исключений. Блоки Catch проверяются по порядку — специфичные исключения первыми, Exception (база) последним. Finally выполняется всегда (даже при Return или неперехваченных исключениях) — используйте для очистки (закрытие файлов, освобождение ресурсов). Throw (без аргументов) перебрасывает текущее исключение, сохраняя stack trace. When фильтрует условно (Catch...When condition). Свойства Exception: Message (описание), StackTrace (цепочка вызовов), Source (assembly). Никогда не ловите Exception молча без логирования — это скрывает баги.

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

Генерация и кастомные исключения

Throw New ExceptionType(...) выбрасывает исключение. Всегда передавайте осмысленное сообщение и используйте NameOf() для имён параметров (устойчиво к рефакторингу). При оборачивании исключений передавайте оригинал как innerException для сохранения причины. Кастомные исключения должны наследоваться от Exception (или более специфичной базы), быть помечены <Serializable> и предоставлять конструкторы (message, message+inner). Переопределяйте ToString() для кастомного отображения. Кастомные исключения позволяют вызывающим ловить специфичные типы ошибок и обрабатывать их соответственно — используйте для доменно-специфичных ошибок (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

Using-операторы и IDisposable

Using гарантирует вызов Dispose() при выходе из блока — критично для ресурсов (файлы, соединения с БД, сетевые потоки), которые не garbage-collected своевременно. Несколько ресурсов можно объявить в одном Using (через запятую). Реализуйте IDisposable, когда ваш класс держит unmanaged-ресурсы или другие IDisposable-объекты. Паттерн Dispose: Dispose(disposing As Boolean) освобождает managed (при disposing=True) и unmanaged ресурсы; GC.SuppressFinalize предотвращает запуск финализатора. Всегда оборачивайте IDisposable-объекты в Using для предотвращения утечек ресурсов.

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

Лучшие практики исключений

Лучшие практики исключений: (1) Не ловите исключения, которые не можете обработать — позвольте им распространяться. (2) Никогда не глотайте исключения молча (пустой Catch) — как минимум логируйте. (3) Используйте TryParse вместо Parse для пользовательского ввода (исключения дороги и для действительно исключительных случаев). (4) Выбрасывайте специфичные типы исключений (ArgumentOutOfRangeException, не Exception). (5) Используйте фильтры When для логирования без ловли (верните False, чтобы не ловить). (6) Перебрасывайте через Throw (без аргументов) для сохранения stack trace — не Throw ex, который его сбрасывает. Исключения для исключительных условий, не для нормального потока управления.

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

Отладка и диагностика

Debug.Assert/WriteLine работают только в Debug-сборках (вырезаются в Release) — используйте для инвариантов и диагностики. Trace работает в обеих сборках — для production-логирования. #If DEBUG...#End If включает условную компиляцию. Debugger.Break() действует как программная точка останова. Stopwatch точно измеряет прошедшее время (для бенчмаркинга). StackTrace захватывает цепочку вызовов (полезно для логирования). EventLog пишет в Windows Event Log (требует admin для создания источника). Используйте эти инструменты для диагностики без модификации production-поведения.

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

Файловый I/O и потоки

Чтение и запись текстовых файлов

File.WriteAllText/ReadAllText простейшие для малых файлов. File.ReadAllLines возвращает String-массив (загружает весь файл в память). Для больших файлов используйте StreamReader/StreamWriter с Using — они читают/пишут построчно, держа память низкой. Using гарантирует закрытие потока даже при исключениях. AppendAllText/AppendAllLines добавляют к существующим файлам. Всегда проверяйте File.Exists перед чтением для дружелюбной ошибки. Для async I/O (неблокирующий UI) используйте ReadAllTextAsync/WriteAllTextAsync с 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

Бинарные файлы и сериализация

BinaryWriter/Reader читают/пишут примитивные типы в компактном бинарном формате — порядок чтения должен совпадать с записью. Для структурированных данных предпочитайте JSON (человекочитаемый, языконезависимый). System.Text.Json (встроенный, быстрый) или Newtonsoft.Json (популярный, богатый функционалом) сериализуют объекты в/из JSON. JsonSerializer.Serialize/Deserialize(Of T) — основные методы. Используйте JsonSerializerOptions для форматирования (indented, camelCase). JSON идеален для конфигов, API и обмена данными. Избегайте BinaryFormatter (риск безопасности — удалён в .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)

Операции с каталогами и путями

Directory.CreateDirectory/GetFiles/GetDirectories/Exists/Delete управляют папками. GetFiles поддерживает search patterns (*.csv) и SearchOption.AllDirectories для рекурсии. Класс Path безопасно обрабатывает пути кросс-платформенно — всегда используйте Path.Combine (не &) для объединения путей (обрабатывает разделители). Path.GetTempFileName создаёт уникальный temp-файл. FileInfo/DirectoryInfo — объектно-ориентированные обёртки над файловыми/каталожными операциями со свойствами (Length, CreationTime) и методами (CopyTo, MoveTo, Delete). Используйте их вместо строковых манипуляций для путей.

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

Парсинг CSV и файлы данных

Для простого CSV Split(","c) работает — но ломается на quoted-полях с запятыми ("Smith, John"). TextFieldParser (в Microsoft.VisualBasic.FileIO) корректно обрабатывает кавычки, экранированные кавычки и разделители — используйте для реального CSV. Также парсит файлы фиксированной ширины (FieldWidths). Для записи CSV вручную заключайте в кавычки поля, содержащие запятые/кавычки. My.Computer.FileSystem — VB-специфичная удобная обёртка (проще API, меньше контроля). Для больших CSV рассмотрите CsvHelper (NuGet) — робастная, стриминговая CSV-библиотека. Всегда обрабатывайте строку заголовка отдельно.

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})

Async файловые операции

Async файловый I/O использует ReadToEndAsync/ReadLineAsync/WriteAsync с Await — поток не блокируется во время I/O, улучшая отзывчивость (особенно в UI-приложениях). Async-методы возвращают Task или Task(Of T); Await разворачивает результат. Для обработки многих файлов конкурентно запустите все задачи и Await Task.WhenAll (параллельный I/O). Async Sub — для обработчиков событий; Async Function — для всего остального. Async I/O блестит на web-серверах (много запросов) и desktop-приложениях (удержание UI отзывчивым). Накладные расходы малы, поэтому используйте async для любого I/O, которое может быть медленным.

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 и UI

Основы форм и события

Windows Forms (WinForms) строит desktop-UI добавлением Controls на Form. Каждый контрол имеет свойства (Text, Location, Size) и события (Click, Load, FormClosing). AddHandler связывает события во время выполнения; Handles (с WithEvents) — во время компиляции. MessageBox.Show отображает диалоги. OnLoad/OnFormClosing — переопределяемые protected-методы для жизненного цикла формы. Application.Run запускает цикл сообщений. WinForms использует дизайнер (drag-and-drop в Visual Studio) — сгенерированный код идёт в .Designer.vb partial-класс. Для современных приложений рассмотрите WPF или 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

Распространённые контролы

Частые WinForms-контролы: TextBox (текстовый ввод, Multiline для области), CheckBox (булев), RadioButton (взаимоисключающие — группируйте в GroupBox), ComboBox (dropdown), NumericUpDown (числовой ввод со спиннером), ListBox (выбираемый список). Устанавливайте Location (Point) и Size. Используйте object initializers (With {.Prop = value}) для лаконичной настройки. Controls.AddRange добавляет несколько контролов сразу. У каждого контрола есть событие по умолчанию (Button.Click, TextBox.TextChanged, ComboBox.SelectedIndexChanged) — двойной клик в дизайнере генерирует обработчик.

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

Layout: панели, docking и anchoring

Layout WinForms: Dock (Top/Bottom/Left/Right/Fill) заставляет контрол заполнять край — добавляйте Fill последним, чтобы он занял оставшееся пространство. Anchor привязывает контрол к краям родителя (он меняет размер при ресайзе формы). TableLayoutPanel упорядочивает контролы в сетке (строки/столбцы с процентными/абсолютными размерами) — лучше для форм. FlowLayoutPanel складывает контролы в поток (оборачивается автоматически). Panel — простой контейнер для группировки. Для адаптивных layout предпочитайте TableLayoutPanel ручному позиционированию. Порядок Controls.Add важен для docking (поздние добавления перекрывают ранние).

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

Диалоги: OpenFileDialog, SaveFileDialog, ColorDialog

Частые диалоги: OpenFileDialog (выбрать файл для открытия), SaveFileDialog (выбрать, куда сохранить), ColorDialog (выбрать цвет), FontDialog (выбрать шрифт), FolderBrowserDialog (выбрать каталог). Filter использует формат "Description|pattern|Description|pattern". ShowDialog возвращает DialogResult (OK/Cancel) — всегда проверяйте перед использованием результата. Оборачивайте диалоги в Using (они IDisposable). OverwritePrompt предотвращает случайные перезаписи. InitialDirectory запускает диалог в полезной локации. Эти диалоги предоставляют нативный Windows UI без кастомного кода.

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 и DataGridView

Data binding автоматически связывает UI-контролы с data-объектами. Простая привязка: txt.DataBindings.Add("Text", obj, "PropName"). Для списков установите DataGridView.DataSource в BindingList(Of T) или DataTable. INotifyPropertyChanged включает two-way binding — UI обновляется при изменении свойства (вызывайте PropertyChanged в setter). BindingList(Of T) — как ObservableCollection — уведомляет сетку при добавлении/удалении элементов. BindingSource добавляет навигацию и фильтрацию. AutoGenerateColumns создаёт столбцы из свойств. Data binding устраняет ручной код синхронизации между UI и данными.

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

Файловый I/O углублённо

StreamReader и StreamWriter

StreamReader/StreamWriter обрабатывают текстовые файлы с правильным кодированием и dispose. Блок Using гарантирует закрытие файлов даже при исключениях — всегда используйте. File.ReadAllLines/ReadAllText удобны для малых файлов; ReadLine в цикле эффективен по памяти для больших. Async-методы (ReadAllTextAsync) предотвращают зависание UI во время I/O. StreamWriter с append:=True добавляет к существующим файлам. Кодировка по умолчанию UTF-8; укажите явно через New StreamWriter(path, append, Encoding.UTF8) для legacy-форматов.

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")

Бинарные и CSV-файлы

BinaryWriter/Reader хранят примитивные типы в компактном бинарном формате — намного меньше текста и быстрее парсится. Всегда читайте в том же порядке и типах, что писали. Для CSV TextFieldParser корректно обрабатывает quoted-поля с встроенными запятыми (в отличие от наивного Split). Для production CSV используйте библиотеку вроде CsvHelper (NuGet), обрабатывающую edge-кейсы, маппинг типов и стриминг. Бинарные файлы платформо-зависимы (endianness); используйте BinaryWriter с little-endian для кросс-платформенной совместимости.

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

Файловые системные операции

File и FileInfo предоставляют файловые операции; Directory и DirectoryInfo обрабатывают папки. FileInfo/DirectoryInfo эффективнее, когда нужно несколько атрибутов (один системный вызов). EnumerateFiles ленивый (возвращает по одному) vs GetFiles, загружающий все пути в память — используйте Enumerate для больших каталогов. Path.Combine строит пути с правильными разделителями кросс-платформенно; никогда не конкатенируйте строки вручную. SearchOption.AllDirectories может выбросить на access-denied; ловите UnauthorizedAccessException или используйте рекурсивный помощник.

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

Сериализация (JSON и XML)

System.Text.Json (современный, быстрый) предпочтительнее устаревшего DataContractJsonSerializer. JsonSerializer.Serialize/Deserialize обрабатывают большинство типов; используйте JsonSerializerOptions для форматирования и политик именования. Для XML XmlSerializer простой, но ограничен (нет словарей, требует конструктор без параметров). Для сложных сценариев (полиморфизм, циклические ссылки) рассмотрите Newtonsoft.Json (Json.NET) через NuGet — больше функций, но медленнее. Всегда обрабатывайте ошибки десериализации (JsonException) для непроверенного ввода.

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

Async файловые операции и потоки

Async файловый I/O удерживает UI отзывчивым во время больших операций. ReadAsync/WriteAsync работают с буферами для тонкой отчётности о прогрессе. IProgress(Of T) сообщает прогресс в UI-поток безопасно. MemoryStream — для данных в памяти (без диска). GZipStream сжимает/разжимает потоки на лету. Для сетевого I/O используйте HttpClient (async). Всегда используйте блоки Using для гарантии закрытия потоков. Размер буфера 81920 (80 КБ) — хороший default — балансирует память и накладные расходы 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

Доступ к БД с ADO.NET

Соединения и команды

Всегда используйте параметризованные запросы (Parameters.AddWithValue) для предотвращения SQL-инъекций — никогда не конкатенируйте пользовательский ввод в SQL-строки. SqlConnection, SqlCommand и SqlDataReader — все IDisposable; оборачивайте их в Using. ExecuteReader возвращает строки; ExecuteNonQuery возвращает затронутые строки (INSERT/UPDATE/DELETE); ExecuteScalar возвращает одно значение (COUNT, SUM). Для других БД используйте соответствующий провайдер (OracleConnection, OleDbConnection, OdbcConnection) или generic DbProviderFactory.

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

DataSet и DataTable

DataTable/DataSet — отключённые контейнеры данных — загрузите один раз, работайте offline, обновляйте позже. SqlDataAdapter.Fill загружает данные; SqlCommandBuilder авто-генерирует команды INSERT/UPDATE/DELETE для простых single-table-сценариев. Typed DataSets (через .xsd-дизайнер) предоставляют проверку типов во время компиляции и IntelliSense. DataView фильтрует и сортирует без модификации underlying DataTable. Для современных приложений предпочитайте Entity Framework или Dapper вместо сырых DataSet — они более поддерживаемы и тестируемы. DataSet остаются полезны для отчётности и legacy-интеропа.

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

Транзакции и пулинг соединений

Транзакции обеспечивают атомарность — все операции успешно или все неудачно. SqlTransaction оборачивает несколько команд на одном соединении. TransactionScope включает распределённые транзакции между несколькими соединениями/ресурсами (использует MS DTC при необходимости) — вызовите scope.Complete() для commit. Пулинг соединений автоматический в ADO.NET: соединения переиспользуются, а не пересоздаются, что резко повышает производительность. Всегда Close/Dispose соединения (блоки Using) для возврата в пул. Настраивайте размер пула и время жизни в строке подключения для high-throughput-приложений.

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

Async операции с БД

Async операции с БД (OpenAsync, ExecuteReaderAsync, ReadAsync) предотвращают зависание UI во время долгих запросов. Паттерн зеркалирует sync ADO.NET, но с Await. Всегда используйте Async в UI-приложениях для удержания отзывчивости. Для high-throughput-серверов async DB-вызовы освобождают потоки для обработки других запросов. MARS (Multiple Active Result Sets) позволяет несколько readers на одном соединении — включите через 'MultipleActiveResultSets=True' в строке подключения. Обрабатывайте SqlException для доменно-специфичных ошибок (deadlocks, нарушения ограничений).

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 — micro-ORM, расширяющий IDbConnection методами-расширениями — быстрый (почти raw ADO.NET) и простой. Query<T> мапит строки на объекты автоматически по совпадению имён столбцов со свойствами. Анонимные объекты предоставляют параметры (защита от SQL-инъекций). Bulk insert передаёт список, и Dapper выполняет по разу на элемент. Multi-mapping обрабатывает joins разделением строк по столбцу. Dapper идеален, когда нужен контроль SQL с меньшим boilerplate, чем raw ADO.NET. Для сложных графов объектов и отслеживания изменений используйте 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

Query и Method-синтаксис

LINQ предоставляет два синтаксиса: query (SQL-подобный, From...Where...Select) и method (fluent, .Where().Select()). Они компилируются в один IL — выбирайте по читаемости. Query-синтаксис поддерживает меньше операторов (нет Sum, Count напрямую); используйте method для них. LINQ использует отложенное выполнение — запрос не запускается, пока вы не итерируете (For Each) или не вызовете материализующий оператор (ToList, Count). Это значит, что модификация источника после определения запроса влияет на результаты. Используйте .ToList() для немедленного выполнения и снапшота данных.

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

Агрегация и группировка

Операторы агрегации LINQ (Sum, Average, Min, Max, Count, Any, All) сводят последовательности к одиночным значениям. GroupBy разделяет элементы по ключу; каждая группа имеет .Key и сама enumerable. Query-синтаксис 'Group By...Into Group' VB-специфичный. Несколько агрегатов на группу распространены для отчётности. Any/All короткозамкнутые (ранняя остановка) — полезны для проверки условий без полной итерации. Агрегатные функции выбрасывают на пустых последовательностях; используйте nullable-варианты (Sum возвращает 0, Average выбрасывает) или DefaultIfEmpty.

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 и set-операции

Join выполняет inner joins (совпадающие ключи); Group Join с DefaultIfEmpty создаёт left joins. Zip парит элементы позиция-в-позицию. Set-операции (Union, Intersect, Except, Distinct) используют компараторы равенства по умолчанию — переопределяйте Equals/GetHashCode или передавайте кастомный IEqualityComparer для сложных типов. Concat добавляет без удаления дубликатов (в отличие от Union). Все ленивые, кроме материализованных. Для больших датасетов рассмотрите HashSet или Dictionary для O(1) lookup вместо O(n*m) вложенных циклов 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

Сортировка, пагинация и элементные операции

OrderBy/ThenBy сортируют (стабильно); OrderByDescending/ThenByDescending сортируют по убыванию. Несколько ключей сортировки используют ThenBy после OrderBy. Пагинация использует Skip (offset) и Take (limit) — необходима для больших датасетов в UI. First/Last выбрасывают на пустых; FirstOrDefault возвращает default (Nothing для ссылочных, 0 для чисел). Single форсирует ровно одно совпадение (иначе выброс) — полезно для валидации. TakeWhile/SkipWhile разделяют по предикату до его неудачи. Все ленивые, кроме First/Last/Single, форсирующих итерацию.

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 и деревья выражений

Методы-расширения (с <Extension()>) добавляют LINQ-подобные операторы к любому типу. Кастомные агрегаты (Median, Mode, StandardDeviation) заполняют пробелы в встроенных операторах. IEnumerable(Of T) использует делегаты (in-memory); IQueryable(Of T) использует деревья выражений, транслируемые в SQL (EF/LINQ to SQL) — смешивание может вызвать client-side evaluation (медленно). ToList/ToArray/ToDictionary форсируют выполнение и снапшот результатов. PLINQ (.AsParallel) параллелит запросы — используйте для CPU-bound операций на больших коллекциях, но остерегайтесь упорядочивания и потокобезопасности.

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

Многопоточность и async

Task и Async/Await

Async/Await — современный способ писать асинхронный код — выглядит синхронно, но не блокирует потоки. Помечайте методы Async и используйте Await на вызовах, возвращающих Task. UI-обработчики событий могут быть Async Sub (единственное место, где Async Sub приемлем). Task.Run выгружает CPU-bound-работу в thread pool. Task.WhenAll ждёт все задачи (параллельно); Task.WhenAny ждёт первую завершённую. Компилятор генерирует state machine, обрабатывающую continuations, распространение исключений и захват контекста. Всегда предпочитайте Async/Await ручному threading или колбэкам.

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)

Потокобезопасность и синхронизация

SyncLock (VB) / lock (C#) обеспечивает взаимное исключение — только один поток входит в блок за раз. Лочьте на private Object, никогда на Me или string. Interlocked предоставляет атомарные операции без локов (быстрее для простых случаев). Mutex работает между процессами (single-instance-приложения). SemaphoreSlim ограничивает конкурентный доступ (например, максимум 3 API-вызова одновременно). Вссегда освобождайте локи/семафоры в блоках Finally. Deadlocks возникают, когда два потока ждут друг друга — захватывайте локи в согласованном порядке. Используйте ConcurrentDictionary, ConcurrentQueue для lock-free потокобезопасных коллекций.

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 и BackgroundWorker

Parallel.For/ForEach распределяют работу по потокам thread pool — идеально для CPU-bound циклов. PLINQ (.AsParallel) параллелит LINQ-запросы. Используйте ParallelOptions.MaxDegreeOfParallelism для ограничения потоков. CancellationToken включает кооперативную отмену. BackgroundWorker — legacy, но удобен для WinForms — он маршалит ProgressChanged и RunWorkerCompleted в UI-поток автоматически. Для нового кода предпочитайте Task.Run + IProgress(Of T) + Async/Await. Параллельные циклы блокирующие (в отличие от Async); не используйте их на 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()

Таймеры и маршалинг в UI-поток

В .NET три Timer: Windows.Forms.Timer (UI-поток, простейший для форм), System.Threading.Timer (thread pool, лёгкий) и System.Timers.Timer (компонент, серверные сценарии). Только Forms.Timer может обновлять UI напрямую; остальные требуют Invoke. Control.Invoke маршалит делегат в UI-поток — проверяйте InvokeRequired сначала. ConfigureAwait(False) улучшает производительность в library-коде (без захвата контекста), но предотвращает UI-доступ после. SynchronizationContext.Post — более общий способ маршалинга в конкретный контекст (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)

Concurrent-коллекции и Channels

Concurrent-коллекции (ConcurrentDictionary, Queue, Stack, Bag) — потокобезопасные альтернативы обычным коллекциям — используйте их вместо локов. AddOrUpdate/GetOrAdd на ConcurrentDictionary — атомарные составные операции. BlockingCollection реализует паттерн producer-consumer с блокирующими Add/Take — отлично для worker-очередей. Channels (System.Threading.Channels) — современная, async-friendly альтернатива с backpressure. Для потокобезопасности UI используйте Invoke/BeginInvoke вместо concurrent-коллекций. Всегда предпочитайте эти встроенные примитивы ручному лочению — они протестированы и оптимизированы.

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

Реестр и Windows API

Чтение и запись реестра

Windows Registry хранит настройки приложений и системную конфигурацию. Используйте Microsoft.Win32.Registry для доступа. HKEY_CURRENT_USER (HKCU) per-user (без admin); HKEY_LOCAL_MACHINE (HKLM) системно (требует admin). Всегда предоставляйте default value при чтении (возвращает Nothing, если отсутствует). Используйте RegistryValueKind для указания типа (String, DWord, Binary). Ключ Run авто-запускает приложения при входе. Оборачивайте RegistryKey в Using для закрытия дескрипторов. Будьте осторожны при редактировании реестра — ошибки могут сломать 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: вызов Windows API

P/Invoke (Platform Invoke) позволяет VB вызывать неуправляемые функции Windows API через DllImport. Объявляйте сигнатуру функции, соответствующую C API; runtime маршалит типы автоматически (String ↔ LPSTR, Integer ↔ DWORD). Используйте ByRef для выходных параметров и StructLayout для структур, передаваемых по ссылке. CharSet.Auto выбирает ANSI или Unicode по ОС. Частые библиотеки: user32 (окна, сообщения), kernel32 (система, файлы), gdi32 (графика). Всегда оборачивайте P/Invoke в класс NativeMethods. Pinvoke.net — отличный ресурс для сигнатур.

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

Process и Shell-операции

Process.Start запускает внешние программы. UseShellExecute=False с RedirectStandardOutput позволяет программно захватывать вывод — необходимо для CLI-инструментов. Всегда WaitForExit и читайте вывод, чтобы избежать deadlocks (буфер вывода может заполниться и заблокировать процесс). GetProcesses перечисляет запущенные процессы; GetProcessesByName находит конкретные. Verb='runas' повышает до admin (вызывает UAC). Для долгоживущих процессов подписывайтесь на событие Exited или используйте async-паттерны. Будьте осторожны с пользовательскими путями к файлам — валидируйте для предотвращения command injection.

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)

Environment и системная информация

Environment предоставляет системную и пользовательскую информацию. GetFolderPath с SpecialFolder — правильный способ найти стандартные каталоги (AppData, MyDocuments, Temp) — никогда не хардкодьте пути. Переменные окружения настраивают поведение между машинами. GetCommandLineArgs включает исполняемый как первый элемент. Для детальной информации о железе используйте WMI (System.Management) — запросы классов вроде Win32_Processor, Win32_LogicalDisk. Screen.AllScreens (Windows Forms) даёт информацию о мониторах для multi-display-настроек. Всегда используйте эти API вместо угадывания путей или настроек.

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

INI-файлы и конфигурация

INI-файлы устарели, но всё ещё используются для простых конфигураций. Функции Windows API (WritePrivateProfileString, GetPrivateProfileString) обрабатывают парсинг. Для современного .NET используйте app.config (XML) или appsettings.json (JSON) с ConfigurationManager или Microsoft.Extensions.Configuration. JSON-конфиг поддерживает вложенные структуры, массивы и environment-специфичные переопределения (appsettings.Production.json). Configuration builders могут комбинировать несколько источников (файлы, переменные окружения, командная строка). Всегда отделяйте конфигурацию от кода для гибкости развёртывания.

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

Коллекции и дженерики углублённо

List, Dictionary и HashSet

List(Of T) — рабочая лошадка — динамический массив с O(1) append, O(n) insert/remove. Dictionary(Of K,V) предоставляет O(1) поиск по ключу (хеш-таблица). HashSet(Of T) хранит уникальные элементы с быстрыми set-операциями (Union, Intersect, Except). SortedDictionary держит ключи отсортированными (binary search tree). LinkedList — двусвязный (быстрый insert/remove где угодно, но нет индексированного доступа). Выбирайте по паттернам доступа: List для индексированного, Dictionary для ключевого, HashSet для принадлежности, SortedDictionary для упорядоченной итерации. Все generic (типобезопасные, без 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 и ObservableCollection

Queue (FIFO) и Stack (LIFO) специализированы для упорядоченных паттернов доступа. ObservableCollection вызывает события CollectionChanged при добавлении/удалении/замене элементов — необходима для MVVM data binding. BindingList расширяет это возможностями редактирования (AddingNew, AllowEdit, AllowRemove). ReadOnlyCollection оборачивает список для предотвращения модификации (возвращает оригинал, не копию). KeyedCollection комбинирует семантику списка и словаря (доступ по индексу или ключу). Для потокобезопасных версий используйте ConcurrentQueue, ConcurrentStack, ConcurrentDictionary из 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

Generic-методы и ограничения

Дженерики предоставляют типобезопасность без накладных расходов boxing/unboxing. Ограничения (Of T As ...) ограничивают параметры типа: Class (ссылочные типы), Structure (value-типы), New (конструктор без параметров), IComparable (интерфейс) или базовый класс. Несколько ограничений используют фигурные скобки: Of T As {Class, New, IComparable(Of T)}. Generic-методы выводят тип из аргументов. Дженерики reified в .NET (информация о типе доступна во время выполнения, в отличие от type erasure в Java). Используйте дженерики для коллекций, алгоритмов и utility-классов, чтобы избежать дублирования кода, сохраняя типобезопасность.

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

Кастомные коллекции и IEnumerable

Реализация IEnumerable(Of T) включает итерацию For Each. Шаблон Iterator/Yield (VB 14+) генерирует конечные автоматы для отложенного вычисления — значения создаются по требованию. Это эффективно использует память для больших или бесконечных последовательностей. CollectionBase — это устаревший базовый класс; для нового кода предпочтительнее реализовать ICollection(Of T) или унаследовать от List(Of T). Пользовательские коллекции имеют смысл, когда нужно особое поведение (проверка, уведомления, отложенная загрузка). В большинстве случаев объединяйте существующие коллекции, а не создавайте их с нуля.

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

Кортежи и ValueTuple

ValueTuple (VB 15+) — современный способ возвращать несколько значений — легковесный (тип-значение), с именованными полями для читаемости. Tuple (System.Tuple) — ссылочный тип только с именами Item1/Item2. ValueTuple поддерживает деконструкцию (Dim (a, b) = ...) и поэлементное равенство. Используйте ValueTuple для внутренних возвратов из методов и промежуточных результатов LINQ. Для публичных API предпочитайте записи или классы со значимыми именами. Шаблон пустышки (_) игнорирует нежелательные значения. ValueTuple особенно полезен в LINQ для анонимных проекций, которые нужно вернуть из методов.

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

Отладка и диагностика

Debug и Trace

Класс Debug компилируется в сборках Release (использует ConditionalAttribute); Trace работает во всех сборках. TraceListeners направляют вывод в файлы, консоль или журнал событий. TraceSwitch (настраиваемый через app.config) управляет уровнем детализации во время выполнения без перекомпиляции. TraceSource обеспечивает именованную гранулярную трассировку с типами событий (Error, Warning, Info, Verbose). Всегда используйте Trace для производственной диагностики и Debug для утверждений разработки. Атрибут Conditional полностью удаляет вызовы методов в сборках Release — нулевые накладные расходы. Настраивайте слушатели в app.config для гибкости развёртывания.

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

Отладчик и точки останова

Атрибуты отладчика настраивают отображение объектов в отладчике. DebuggerDisplay управляет строкой сводки; DebuggerBrowsable управляет видимостью членов (RootHidden разворачивает коллекции). DebuggerStepThrough/Hidden управляют поведением при пошаговом выполнении. Условные точки останова приостанавливают выполнение только когда условие истинно — необходимы для поиска ошибок в больших циклах. Точки трассировки записывают сообщения без изменения кода. Окно Immediate вычисляет выражения во время выполнения. Edit and Continue позволяет исправлять код без перезапуска. Освойте эти функции отладчика, чтобы значительно ускорить отладку.

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

Стратегии обработки исключений

Перехватывайте специфичные исключения раньше общих (сначала самые специфичные). Используйте Throw (не Throw ex), чтобы сохранить исходный стек вызовов. Фильтры исключений (предложение When) добавляют условия без перехвата — исключение распространяется дальше, если фильтр ложен. Пользовательские исключения должны наследоваться от Exception (не от ApplicationException, который устарел), быть Serializable и реализовывать три конструктора. Оборачивайте низкоуровневые исключения в доменно-специфичные, чтобы абстрагировать детали реализации. Никогда не перехватывайте и не подавляйте исключения молча — как минимум регистрируйте их в журнале. Используйте глобальные обработчики исключений (AppDomain.UnhandledException, Application.ThreadException) как страховку.

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

Ведение журналов и Журнал событий

Windows Event Log идеально подходит для системных событий (запуск/остановка службы, критические ошибки) — требуются права администратора для создания источника. Для журналирования приложений используйте структурированную платформу журналирования: Serilog (современная, дружественная к JSON), NLog или log4net. Структурированное журналирование (logger.Information("User {UserId}...", id)) сохраняет типы данных для запросов, в отличие от конкатенации строк. Уровни журнала (Debug, Info, Warning, Error, Fatal) позволяют фильтровать во время выполнения. Всегда включайте контекст (ID пользователя, ID заказа) в записи журнала для отладки. Счётчики производительности отслеживают CPU, память и пользовательские метрики в продакшене.

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)

Модульное тестирование и TDD

Модульные тесты проверяют отдельные методы изолированно. MSTest (встроенный), xUnit и NUnit — основные фреймворки. TestInitialize/TestCleanup выполняются до/после каждого теста. Методы Assert проверяют результаты (AreEqual, IsTrue, ThrowsException). DataTestMethod/DataRow параметризуют тесты с несколькими входными данными. ExpectedException (или Try/Catch/Assert.Fail) проверяет случаи исключений. Используйте Moq для создания заглушек зависимостей (интерфейсов) — настройте ожидаемое поведение и проверяйте вызовы. TDD (разработка через тестирование) сначала пишет тесты, затем код. Стремитесь к высокому покрытию бизнес-логики; пропускайте тривиальные геттеры/сеттеры свойств. Запускайте тесты в CI/CD для выявления регрессий.

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

Коллекции и словари

ArrayList и List(Of T)

List(Of T) — современная, типобезопасная замена ArrayList. Всегда предпочитайте его — вы получаете проверку типов во время компиляции и лучшую производительность (нет упаковки для типов-значений). Предложение From инициализирует список встроенным образом. FindAll/Find используют предикаты (лямбды). AddRange добавляет несколько элементов сразу.

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) — стандартная хеш-таблица. Используйте TryGetValue для безопасного получения значения (возвращает False, если отсутствует, без исключения). Add выбрасывает исключение, если ключ существует; индексатор (dict(key)) молча перезаписывает. Коллекции Keys/Values позволяют перебирать или извлекать. LINQ работает со словарями — они 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 и SortedSet

HashSet предназначен для быстрой проверки принадлежности и операций над множествами (объединение, пересечение, разность). SortedSet хранит элементы в отсортированном порядке (двоичное дерево поиска). Оба имеют операции O(1) или O(log n) — намного быстрее, чем List.Contains (O(n)). Используйте HashSet, когда порядок не важен; SortedSet, когда нужна упорядоченная итерация.

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 и Stack

Queue — это FIFO (первым пришёл — первым ушёл) — используйте для планирования задач, BFS. Stack — LIFO (последним пришёл — первым ушёл) — используйте для отмены, вычисления выражений, DFS. Оба имеют O(1) для enqueue/dequeue и push/pop. Создайте Stack из массива, чтобы обратить его одной строкой. Peek просматривает следующий элемент без удаления.

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}

Параллельные коллекции

Параллельные коллекции потокобезопасны — используйте их вместо блокировки обычных коллекций. AddOrUpdate и GetOrAdd у ConcurrentDictionary атомарны. ConcurrentQueue/Stack без блокировок. ConcurrentBag работает быстрее всего, когда каждый поток в основном производит и потребляет свои собственные элементы. BlockingCollection — стандартный шаблон для очередей типа производитель-потребитель.

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

Файловый ввод-вывод продвинутый

Чтение и запись потоков

Всегда оборачивайте потоки в блоки Using для гарантированного освобождения (закрывает файл даже при исключениях). File.ReadAllText/ReadAllLines удобны для небольших файлов. Для больших файлов используйте StreamReader построчно. Асинхронный ввод-вывод (ReadToEndAsync) поддерживает отзывчивость UI — никогда не вызывайте синхронный файловый ввод-вывод в потоке 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

Бинарные данные и CSV

BinaryWriter/Reader обрабатывают типизированные бинарные данные — ReadInt32/ReadDouble должны точно совпадать с порядком записи. TextFieldParser (в Microsoft.VisualBasic.FileIO) — надёжный способ разбора CSV — обрабатывает поля в кавычках со встроенными запятыми и переносами строк. Не создавайте собственный CSV-парсер с Split(","). Для сложных CSV рассмотрите пакет CsvHelper из NuGet.

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)

Операции с путями

Всегда используйте Path.Combine (не конкатенацию строк) для объединения путей — он правильно обрабатывает разделители. Path.GetInvalidFileNameChars помогает очистить пользовательский ввод для имён файлов. FileInfo/DirectoryInfo кэшируют метаданные файла — используйте их, когда нужно несколько свойств. SearchOption.AllDirectories рекурсивен; для огромных деревьев используйте EnumerateFiles (потоковая обработка).

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())

Наблюдение за файлами

FileSystemWatcher отслеживает изменения в каталоге. Установите NotifyFilter, чтобы ограничить, какие изменения вызывают события (производительность). Главная ловушка: редакторы часто вызывают несколько событий Changed при каждом сохранении (запись, сброс, метаданные) — устраняйте дребезг, отслеживая время последнего события. Ошибки (например, переполнение буфера из-за слишком большого количества событий) также требуют обработки.

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

Сериализация

System.Text.Json — современный высокопроизводительный сериализатор JSON (заменяет Newtonsoft.Json для нового кода). XmlSerializer обрабатывает XML — обратите внимание, что требует конструктор без параметров и публичные свойства. Для коллекций сериализуйте List(Of T) напрямую. JsonSerializerOptions с WriteIndented создаёт читаемый человеком вывод.

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

Обработка ошибок углублённо

Шаблоны Try/Catch

Перехватывайте специфичные исключения раньше общих (порядок важен). Используйте Finally для очистки — выполняется, даже если вы возвращаете значение или выбрасываете исключение внутри Try. Фильтры When (VB 14+) позволяют перехватывать на основе свойств исключения. При оборачивании исключений передавайте оригинал как innerException, чтобы сохранить стек вызовов. Избегайте перехвата Exception, если только не выбрасываете повторно.

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

Пользовательские исключения

Пользовательские исключения должны наследоваться от Exception (или доменно-специфичного базового класса) и предоставлять несколько конструкторов: только сообщение, сообщение + внутреннее, и доменно-специфичный. Пометьте их <Serializable>, если они могут пересекать границы AppDomain. Добавьте свойства для контекста, который нужен вызывающим сторонам. Переопределите ToString для ясного журналирования.

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

Стратегии обработки исключений

Никогда не подавляйте исключения молча — как минимум регистрируйте их в журнале. Используйте Using для ресурсов IDisposable (чище, чем try/finally). Регистрируйте глобальные обработчики для AppDomain.UnhandledException (последний шанс) и Application.ThreadException (поток UI WinForms). AggregateException оборачивает несколько исключений из параллельного кода — выровняйте и обработайте каждое 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

Шаблоны Result (функциональные)

Шаблон Result возвращает успех/неудачу как значение, а не выбрасывая исключение — полезно для ожидаемых неудач (разбор, проверка), где исключения были бы дорогими и шумными. Комбинируйте с методами расширения (OnSuccess, OnFailure) для цепочек. Используйте исключения для действительно исключительных случаев; Result — для предсказуемых неудач.

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)

Интеграция журналирования

Используйте платформу журналирования (NLog, Serilog, log4net) — не Console.WriteLine. Уровни журнала (Trace/Debug/Info/Warn/Error/Fatal) позволяют фильтровать во время выполнения через конфигурацию. Структурированное журналирование (Serilog) сохраняет имена полей — лучше для поиска в таких инструментах, как ELK или Seq. Всегда включайте контекст (ID пользователей, имена операций) в сообщения журнала.

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

Подключения и команды

Всегда используйте параметры (никогда конкатенацию строк) для предотвращения SQL-инъекций. Блоки Using гарантируют закрытие подключений даже при исключениях. ExecuteReader для SELECT; ExecuteNonQuery для INSERT/UPDATE/DELETE; ExecuteScalar для одиночного значения (например, COUNT или SCOPE_IDENTITY). Пул подключений автоматический — открывайте поздно, закрывайте рано.

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

Транзакции

Транзакции обеспечивают атомарность — все операции либо выполняются, либо все терпят неудачу. BeginTransaction для одного подключения — для одной базы данных. TransactionScope обрабатывает распределённые транзакции между несколькими подключениями (и даже несколькими базами данных) — вызовите Complete для фиксации. Выбирайте уровень изоляции тщательно: Serializable самый безопасный, но медленный; ReadCommitted — по умолчанию.

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 и DataAdapter

DataTable — таблица в памяти, отключённая от базы данных. SqlDataAdapter заполняет её и отправляет обновления обратно. SqlCommandBuilder автоматически генерирует команды INSERT/UPDATE/DELETE для простых сценариев с одной таблицей. DataView фильтрует/сортирует без повторного запроса. Для нового кода предпочитайте ORM (EF Core, Dapper) вместо сырых DataTable.

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 микро-ORM

Dapper — золотая середина между сырым ADO.NET (быстрый, но многословный) и EF Core (продуктивный, но медленный). Он автоматически отображает столбцы на свойства. Используйте анонимные объекты для параметров. Query<T> для списков; QuerySingle<T> для одного; Execute для не-запросов. Передайте коллекцию в Execute для пакетных операций. Multi-mapping обрабатывает соединения.

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

Основы EF Core

EF Core — полнофункциональная ORM — обрабатывает связи, миграции, отслеживание изменений и запросы LINQ. Свойства DbSet — точки входа. SaveChanges сохраняет все отслеживаемые изменения. Include жадно загружает связанные сущности (избегает запросов N+1). Для сценариев только для чтения используйте AsNoTracking() для лучшей производительности. Миграции управляют изменениями схемы.

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 в VB

Синтаксис запросов

Синтаксис запросов LINQ в VB похож на SQL и часто более читаем для сложных запросов. From ... Where ... Select — базовый шаблон. Несколько предложений From делают перекрёстные соединения (фильтруйте через Where). Group By создаёт группы, доступные через ключевое слово Group. Join требует ключевое слово Equals (не On ... Equals ... как в 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

Синтаксис методов

Синтаксис методов использует методы расширения и лямбды — более компактный и композируемый, чем синтаксис запросов. Function(x) ... — синтаксис лямбды в VB. First выбрасывает исключение, если пусто; FirstOrDefault возвращает значение по умолчанию (Nothing для ссылочных типов, 0 для Integer). Skip/Take — стандартный шаблон разбиения на страницы. Оба синтаксиса компилируются в один и тот же 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()

Группировка и агрегация

GroupBy возвращает IGrouping(Of Key, Element) — каждая группа имеет Key и сама является перечисляемой. ToLookup создаёт таблицу поиска (как многозначный словарь). Анонимные типы (New With {Key .X = ...}) отлично подходят для проекций — Key делает свойство неизменяемым и частью равенства. Aggregate — самый гибкий (но наименее читаемый) комбинер.

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)

Соединение данных

Join — внутреннее соединение (только совпадающие). Group Join с DefaultIfEmpty даёт левое соединение. Несколько предложений Join объединяются естественным образом. Join в синтаксисе методов принимает четыре лямбды: внешний ключ, внутренний ключ, селектор результата. Zip объединяет элементы по позиции — полезно, но редко. Для сложных соединений синтаксис запросов обычно яснее, чем синтаксис методов.

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"}

Отложенное выполнение

LINQ использует отложенное выполнение — запросы выполняются при итерации, а не при определении. Это значит, что каждый For Each повторно выполняет запрос. Чтобы кэшировать результаты, вызовите ToList/ToArray. Избегайте побочных эффектов в лямбдах запроса (они выполняются непредсказуемо). Некоторые операторы (Count, First, Any) форсируют немедленное выполнение. Any/All замыкают цепочку.

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

Многопоточность

Задачи и асинхронность

Task.Run планирует работу на потоке пула потоков. Async/Await делает асинхронный код похожим на синхронный — метод приостанавливается на Await, не блокируя поток. Task.WhenAll ждёт все; Task.WhenAny возвращает первую завершившуюся. Всегда передавайте CancellationToken в долго выполняющиеся задачи и периодически вызывайте ThrowIfCancellationRequested.

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

Параллельные циклы

Parallel.For/ForEach распределяют работу между потоками пула — отлично для циклов, ограниченных CPU. Перегрузка с локальными для потока переменными используется для редукций (сумма, максимум) без блокировок на каждой итерации. state.Break останавливает более высокие итерации; state.Stop немедленно останавливает. PLINQ (AsParallel) параллелит LINQ — но помогает только для операций, тяжёлых для CPU.

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()

Синхронизация

SyncLock — простейшая синхронизация — взаимное исключение. Interlocked — для атомарных операций с int/long (быстрее блокировок). Mutex может быть именованным и общим для процессов (приложения с одним экземпляром). Semaphore ограничивает параллелизм (например, максимум 3 подключения). ReaderWriterLockSlim разрешает несколько читателей ИЛИ одного писателя — отлично для кэшей с большим количеством чтений.

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

Каналы и производитель-потребитель

Channel<T> (современный) — рекомендуемый способ создания конвейеров производитель-потребитель — полностью асинхронный, поддерживает противодавление через ограниченную ёмкость. ForEachAsync (параллельное потребление) позволяет нескольким потребителям читать одновременно. BlockingCollection — более старый синхронный эквивалент. ConcurrentQueue/Stack — коллекции без блокировок для прямого использования.

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)

Таймеры и планирование

Три типа таймеров: System.Threading.Timer (пул потоков, наиболее эффективный), System.Timers.Timer (серверные сценарии, события), Windows.Forms.Timer (поток UI, для обновлений UI). Для асинхронного кода Task.Delay чище, чем таймеры. Всегда освобождайте таймеры для предотвращения утечек. Для периодической асинхронной работы цикл While с Task.Delay и CancellationToken — самый чистый шаблон.

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

Сериализация

JSON с System.Text.Json

System.Text.Json — современный быстрый сериализатор JSON, встроенный в .NET. Атрибуты управляют сериализацией: JsonPropertyName переименовывает, JsonIgnore исключает. JsonNamingPolicy.CamelCase соответствует соглашениям JavaScript. Для огромных JSON сериализуйте в поток (не в строку), чтобы избежать загрузки всего в память. JsonDocument анализирует без целевого типа.

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

XML-сериализация

XmlSerializer многословен, но гибок. Атрибуты управляют именами элементов/атрибутов и структурой. Требует конструктор без параметров и публичные свойства чтения/записи. XmlArray/XmlArrayItem управляют вложенными коллекциями. Для чтения больших XML-файлов предпочитайте XmlReader (потоковая) вместо XmlSerializer (загружает всё в память).

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 правильно обрабатывает поля CSV в кавычках. Для записи экранируйте поля, содержащие запятые, кавычки или переносы строк, оборачивая в кавычки и удваивая внутренние кавычки. Для продакшена используйте CsvHelper (NuGet) — обрабатывает все краевые случаи, поддерживает пользовательское отображение и намного быстрее. Всегда используйте библиотеку для CSV; формат имеет удивительные краевые случаи.

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

Бинарные данные и DataContract

DataContractSerializer более устойчив к версиям, чем XmlSerializer (обрабатывает добавленные/удалённые поля через Order). Для высокопроизводительной бинарной сериализации используйте Protocol Buffers (protobuf-net) или MessagePack — оба в 10-100 раз меньше и быстрее, чем XML/JSON. Выбирайте на основе потребностей: человекочитаемый (JSON/XML) или компактный/быстрый (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)

Пользовательские конвертеры

Пользовательские конвертеры обрабатывают типы, которые System.Text.Json не поддерживает нативно (DateOnly, TimeSpan, пользовательские типы). JsonDerivedType (NET 7+) обрабатывает полиморфизм — сериализует тип времени выполнения с дискриминатором. Методы ShouldSerialize* условно включают свойства (как шаблон XmlSerializer). Всегда регистрируйте конвертеры в 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

Рефлексия

Инспекция типов

Рефлексия позволяет проверять типы во время выполнения. GetType(Type) для типов времени компиляции; obj.GetType() для типов времени выполнения. BindingFlags фильтруют члены (Public/NonPublic, Instance/Static). GetCustomAttributes возвращает атрибуты, применённые к типу. Рефлексия медленная — кэшируйте объекты Type и MemberInfo для повторного использования.

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

Создание экземпляров и вызов

Activator.CreateInstance создаёт объекты по типу. MethodInfo.Invoke вызывает методы — передавайте аргументы как Object(). PropertyInfo.GetValue/SetValue обращаются к свойствам. BindingFlags.NonPublic позволяет достичь приватных членов (используйте экономно — нарушает инкапсуляцию). MakeGenericMethod создаёт универсальный метод во время выполнения. Это основа плагинных систем.

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)

Атрибуты

Пользовательские атрибуты добавляют метаданные к типам и членам. AttributeUsage управляет тем, где они могут применяться и разрешены ли несколько. GetCustomAttribute/GetCustomAttributes считывают их во время выполнения через рефлексию. Встроенные атрибуты: Obsolete (предупреждение/ошибка компилятора), Conditional (включение во время компиляции), Serializable. Атрибуты обеспечивают проверку, отображение ORM, сериализацию и многое другое.

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 и динамический код

Reflection.Emit генерирует IL во время выполнения — мощно, но низкоуровнево. DefineDynamicAssembly -> Module -> Type -> Method -> ILGenerator. Emit записывает опкоды IL. Деревья выражений — альтернатива более высокого уровня — создавайте выражения как данные, затем Compile в делегат. Используйте Emit для экстремальной производительности (пользовательские сериализаторы); деревья выражений — для динамических запросов (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

Вопросы производительности

Рефлексия в 100-1000 раз медленнее прямых вызовов. Всегда кэшируйте объекты MemberInfo. Для горячих путей компилируйте в делегаты через деревья выражений — почти так же быстро, как прямые вызовы. Генераторы источников (современный .NET) могут выполнять рефлексию во время компиляции, полностью устраняя накладные расходы во время выполнения. Используйте рефлексию для фреймворков (сериализаторы, ORM, DI); избегайте её в бизнес-логике.

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

Взаимодействие

P/Invoke (вызов Win32 DLL)

P/Invoke позволяет вызывать функции нативных DLL. DllImport указывает библиотеку и параметры. CharSet.Unicode обрабатывает маршалинг строк. ByRef для выходных параметров. StructLayout(Sequential) гарантирует соответствие структур макету C. SetLastError=True позволяет запросить последнюю ошибку Win32 через Marshal.GetLastWin32Error(). Всегда оборачивайте нативные вызовы в класс 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})")

COM-взаимодействие

COM-взаимодействие позволяет вызывать Office и другие COM-библиотеки. Добавьте ссылку на сборку взаимодействия (или используйте NuGet). ВСЕГДА освобождайте COM-объекты через Marshal.ReleaseComObject — они не очищаются сборщиком мусора своевременно, что приводит к «призрачным» процессам Excel. Позднее связывание (CreateObject) избегает ссылок времени компиляции, но теряет IntelliSense и типобезопасность.

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()

Взаимодействие с C#

VB и C# могут свободно вызывать друг друга — они компилируются в один и тот же IL. Добавьте ссылку на проект или общую библиотеку. Следите за чувствительностью к регистру (VB нечувствителен к регистру), синтаксисом индексации и ref/out (VB использует ByRef). Возможности C# вроде unsafe-кода, некоторых дженериков и сопоставления шаблонов не имеют эквивалента в VB — используйте C# для этих частей.

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

Вызов Python и других языков

Для взаимодействия с Python, Python.NET встраивает CPython в .NET — прямой доступ к объектам. Проще: запускайте python.exe с Process.Start. R.NET делает то же самое для R. ClearScript встраивает V8 для JavaScript. Выбирайте на основе глубины интеграции: запуск процесса для одноразовых скриптов; встраивание для тесной интеграции. Следите за производительностью (вызовы между средами выполнения медленные).

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")

Память и указатели

Marshal.AllocHGlobal/FreeHGlobal выделяют/освобождают неуправляемую память — всегда парите их в Try/Finally. Marshal.Copy перемещает массивы между управляемой и неуправляемой памятью. VB не поддерживает unsafe-код (используйте C# для этого или методы Marshal). Span(Of T) (современный .NET) даёт производительность, близкую к указателям, с безопасностью памяти — предпочтительнее сырых указателей.

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?