Programming in Visual Basic

Chapter 5
Delegates, Events, Lambdas, Collections, and LINQ

Post Graduate Program in Computer Science Engineering
Programming in Visual Basic   •   Post Graduate

Delegates, Events, Lambdas, Collections, and LINQ

Introduction

Modern Visual Basic applications are built from components that communicate, transform data, and respond to changing conditions. Five language features are especially important in this design: delegates, events, lambda expressions, collections, and Language Integrated Query (LINQ). Together they support callback-based programming, event-driven systems, functional-style transformations, type-safe data management, and expressive querying.

These features are not isolated conveniences. A delegate can refer to a method; an event can publish notifications through delegates; a lambda can provide an unnamed method; collections can store and organize objects; and LINQ can query those collections using a common programming model. Their combination is central to Windows applications, web services, enterprise systems, data-processing tools, and asynchronous software.

Learning Objectives

After completing this chapter, the learner should be able to:

  1. Explain delegate types, multicast delegates, and type safety in Visual Basic.
  2. Design and consume custom events using the .NET event model.
  3. Construct lambda expressions and distinguish expression lambdas from statement lambdas.
  4. Select appropriate generic collections for different computational requirements.
  5. Use LINQ for filtering, projection, ordering, grouping, joining, aggregation, and deferred execution.
  6. Analyse performance, mutability, type inference, and correctness issues in integrated examples.
  7. Apply these features in a maintainable, secure, and professionally designed application.

1. Delegates as Type-Safe Method References

A delegate is a type that describes the signature of a method. It stores a reference to one or more methods whose parameter list and return type match the delegate declaration. Delegates provide an abstraction between the code that invokes an operation and the code that implements it.

Public Delegate Function BinaryOperation(x As Integer, y As Integer) As Integer

Public Function Add(x As Integer, y As Integer) As Integer
    Return x + y
End Function

Dim operation As BinaryOperation = AddressOf Add
Dim result As Integer = operation(4, 5)   '9

The delegate specifies a contract: two Integer parameters and an Integer result. A method with a different signature cannot be assigned when strict type checking is enabled. This compile-time checking is safer than storing method names as strings and invoking them through reflection.

1.1 Invocation and composition

A delegate may be invoked using normal call syntax or its Invoke method. A delegate can also refer to several methods. Such a delegate is called a multicast delegate.

Dim operations As BinaryOperation = AddressOf Add
operations = [Delegate].Combine(operations,
                                CType(AddressOf Multiply, BinaryOperation))

For a multicast delegate, methods execute in invocation-list order. If the delegate returns a value, the return value from the final method is normally the value observed by the caller. Consequently, multicast delegates are most appropriate for Sub procedures, especially notifications, rather than for computations that return values.

The invocation list can be inspected with GetInvocationList(). A robust component should consider exception behaviour: if one method throws an exception, ordinary invocation may prevent later methods from running. Explicit iteration can provide more controlled error handling, although swallowing exceptions without logging is poor practice.

1.2 Generic framework delegates

The .NET platform provides common delegate types:

DelegateTypical signatureCommon use
ActionNo return valueCommands and callbacks
Action(Of T)One parameter, no return valueProcessing an item
Func(Of T, TResult)One parameter and a resultTransformation or calculation
Predicate(Of T)One parameter and Boolean resultTesting a condition
Comparison(Of T)Two parameters and Integer resultSorting

For example:

Dim isLarge As Predicate(Of Integer) = Function(n) n > 100
Dim convert As Func(Of Integer, String) = Function(n) $"Value={n}"

A comparison delegate returns a negative integer when the first value precedes the second, zero when they are equivalent for ordering, and a positive integer when the first follows the second. It is generally preferable to use Comparer(Of T).Default or OrderBy unless custom ordering is required.

1.3 Delegates and inversion of control

A method that accepts a delegate does not need to know which algorithm will be applied:

Public Function Transform(values As IEnumerable(Of Integer),
                          operation As Func(Of Integer, Integer)) _
                          As List(Of Integer)
    Return values.Select(operation).ToList()
End Function

The caller supplies the transformation. This is an example of inversion of control: the reusable method controls iteration, while the caller controls the operation. Delegates therefore support strategy patterns, dependency injection, test doubles, and asynchronous completion callbacks.

2. Events and Publisher–Subscriber Design

An event represents a notification that something has happened. The object that raises the event is the publisher; objects that respond are subscribers. The publisher does not need direct knowledge of subscribers, which reduces coupling.

Visual Basic declares events with the Event keyword and raises them with RaiseEvent:

Public Class TemperatureSensor
    Public Event TemperatureChanged As EventHandler(Of TemperatureChangedEventArgs)

    Private _temperature As Double

    Public Sub SetTemperature(value As Double)
        If value <> _temperature Then
            Dim oldValue = _temperature
            _temperature = value
            RaiseEvent TemperatureChanged(
                Me,
                New TemperatureChangedEventArgs(oldValue, value))
        End If
    End Sub
End Class

Public Class TemperatureChangedEventArgs
    Inherits EventArgs

    Public ReadOnly Property OldValue As Double
    Public ReadOnly Property NewValue As Double

    Public Sub New(oldValue As Double, newValue As Double)
        Me.OldValue = oldValue
        Me.NewValue = newValue
    End Sub
End Class

A subscriber attaches a handler using AddHandler or the Handles clause:

Dim sensor As New TemperatureSensor()
AddHandler sensor.TemperatureChanged,
    Sub(sender, e)
        Console.WriteLine($"Changed from {e.OldValue} to {e.NewValue}")
    End Sub

The conventional event signature is (sender As Object, e As EventArgs) or a derived event-argument type. sender identifies the publisher, while e carries notification data. The standard pattern improves interoperability with .NET libraries and tools.

2.1 Subscription lifetime and memory

Events create references. When an object subscribes to a long-lived publisher, the publisher may retain the subscriber and prevent garbage collection. This is a common source of memory leaks in long-running applications. A subscriber should detach when its participation ends:

AddHandler sensor.TemperatureChanged, AddressOf OnTemperatureChanged
'Later:
RemoveHandler sensor.TemperatureChanged, AddressOf OnTemperatureChanged

Anonymous lambdas are convenient but can be difficult to remove unless the delegate instance is stored:

Dim handler As EventHandler(Of TemperatureChangedEventArgs) =
    Sub(sender, e) Console.WriteLine(e.NewValue)
AddHandler sensor.TemperatureChanged, handler
RemoveHandler sensor.TemperatureChanged, handler

Event handlers should be short, non-blocking where possible, and safe against repeated or out-of-order notifications. A UI event handler must normally update controls on the UI thread; background publishers should use an appropriate synchronization mechanism.

3. Lambda Expressions

A lambda expression is an inline function. It may be assigned to a delegate or passed directly to a method such as Where, Select, or AddHandler.

Dim square As Func(Of Integer, Integer) = Function(x) x * x
Dim numbers = {1, 2, 3, 4}
Dim evenNumbers = numbers.Where(Function(n) n Mod 2 = 0)

A lambda with one expression is an expression lambda. A statement lambda contains a block:

Dim classify As Func(Of Integer, String) =
    Function(n)
        If n < 0 Then Return "negative"
        If n = 0 Then Return "zero"
        Return "positive"
    End Function

Parameter types are frequently inferred, but explicit types can improve clarity in public or complex code:

Dim scale As Func(Of Double, Double) = Function(x As Double) x * 1.15

3.1 Closures

A lambda can capture variables from its surrounding scope. The resulting object is called a closure.

Dim threshold As Integer = 50
Dim accepted = numbers.Where(Function(n) n >= threshold)
threshold = 3

With deferred LINQ execution, the lambda may observe the later value of threshold, depending on when the query is enumerated. This behaviour is powerful but can surprise developers. A captured variable is not necessarily copied as an immutable value. Use local copies or immediate materialization when stable semantics are required.

Closures may also extend the lifetime of captured objects. Capturing a large object graph or a UI control in a long-lived callback can cause memory retention. Advanced design therefore considers both lexical convenience and object lifetime.

4. Collections and Generic Data Structures

A collection stores multiple values and provides operations for access, insertion, removal, and traversal. Generic collections preserve type safety and avoid unnecessary conversions. The principal interfaces are IEnumerable(Of T), ICollection(Of T), IList(Of T), ISet(Of T), and IDictionary(Of TKey, TValue).

CollectionStrengthTypical use
List(Of T)Indexed storage and flexible sizeOrdered sequences
LinkedList(Of T)Node insertion/removal with a known nodeSpecialized linked structures
HashSet(Of T)Uniqueness and set operationsMembership and deduplication
Dictionary(Of K, V)Key-based lookupIndexes and maps
Queue(Of T)First-in, first-out processingWork queues
Stack(Of T)Last-in, first-out processingUndo and traversal
SortedSet(Of T)Unique sorted valuesOrdered sets
SortedDictionary(Of K, V)Key-sorted mappingOrdered key-value data

4.1 Choosing a collection

A List(Of T) is a strong default when order matters and access is primarily sequential or index-based. A dictionary is more appropriate when an application repeatedly locates an item by a unique key. A hash set expresses uniqueness directly and supplies set operations such as UnionWith and IntersectWith.

The abstract performance model is also relevant. A list index lookup is generally constant-time, written O(1), while searching an unsorted list is generally linear, O(n), where n is the number of elements. Hash-based lookup is generally expected O(1), but can degrade in pathological collision cases. These are asymptotic expectations, not guarantees for every implementation or workload. Memory overhead, equality semantics, hash quality, and ordering requirements must also be considered.

4.2 Equality and mutability

Dictionaries and sets depend on equality and hashing. If an object is used as a key and its fields participating in equality or hashing are changed afterward, the object may become difficult or impossible to retrieve. Immutable key types are therefore safer. For custom classes, implement equality and hashing consistently, or use a comparer explicitly.

Collections are often mutable. Exposing a private List(Of T) directly allows callers to violate invariants. Public APIs should expose read-only views, copies, or interfaces with only the required capabilities. Thread safety should not be assumed: ordinary collections generally require external synchronization when accessed concurrently.

5. LINQ: Querying Objects and Data Sources

LINQ provides a uniform syntax for querying objects, arrays, collections, XML, and compatible data providers. Importantly, LINQ is based on extension methods and delegates. Where accepts a predicate, while Select accepts a projection function.

Dim scores = {72, 48, 91, 67, 85}
Dim result = scores.Where(Function(s) s >= 70) _
                   .OrderByDescending(Function(s) s) _
                   .ToList()

The query returns 91, 85, 72. Where retains elements; OrderByDescending orders them; ToList executes the pipeline and materializes the result.

5.1 Query and method syntax

Visual Basic supports query syntax:

Dim result = From s In scores
             Where s >= 70
             Order By s Descending
             Select s

Query syntax is translated into method calls conceptually similar to Where, OrderByDescending, and Select. Method syntax is often more expressive for complex pipelines, while query syntax can be readable for joins and grouped queries.

5.2 Core operators

  • Where: filters elements.
  • Select: projects each element into a new form.
  • SelectMany: flattens nested sequences.
  • OrderBy, ThenBy: performs primary and secondary ordering.
  • GroupBy: creates groups by a key.
  • Join and GroupJoin: combine related sequences.
  • Distinct: removes duplicate values according to equality rules.
  • Take, Skip, First, and Single: control selection and cardinality.
  • Any and All: test conditions.
  • Count, Sum, Average, Min, and Max: aggregate values.
  • ToList, ToArray, and ToDictionary: materialize results.

A projection changes the shape of data:

Dim summaries = employees.Select(Function(e) New With {
    .Name = e.Name,
    .AnnualPay = e.MonthlyPay * 12D
})

The suffix D identifies a Decimal literal, which is appropriate for many financial calculations. Currency calculations still require explicit policy concerning rounding, tax rules, and storage precision; LINQ does not solve those domain issues.

5.3 Deferred and immediate execution

Many LINQ operators use deferred execution. The query describes a computation that runs when enumerated:

Dim query = numbers.Where(Function(n) n > 5)
'The source may change before query.ToList() is called.

Operators such as ToList, ToArray, ToDictionary, Count, and First execute immediately. Deferred execution can improve composability and avoid unnecessary work, but it can also cause repeated computation, observe changed source data, or repeat database operations. Materialize deliberately when a stable snapshot is needed.

5.4 IEnumerable and IQueryable

IEnumerable(Of T) normally performs operations in the application process using delegates. IQueryable(Of T) represents an expression tree that a provider may translate, for example into SQL. Not every Visual Basic expression can be translated by every provider. A query may compile but fail at runtime or execute inefficiently if a provider cannot translate part of it.

A professional developer should filter and project on the data source when appropriate, retrieve only required columns, avoid accidental client-side evaluation, and inspect generated queries in data-access systems. Do not assume that a LINQ query has the same performance characteristics across in-memory and remote providers.

6. Integrated Worked Example: Course Performance Analysis

Consider the following model:

Public Class Student
    Public Property Id As Integer
    Public Property Name As String
    Public Property Programme As String
    Public Property Marks As List(Of Integer)
End Class

The average mark for a student with k assessments is:

average = (m₁ + m₂ + ⋯ + mₖ) ÷ k

Here, mᵢ is an assessment mark and k is the number of assessments. The formula assumes that all assessments have equal weight and that k > 0. If assessments have different weights, a weighted average is required instead.

Dim students = New List(Of Student) From {
    New Student With {.Id = 1, .Name = "Asha", .Programme = "CSE",
                      .Marks = New List(Of Integer) From {80, 75, 90}},
    New Student With {.Id = 2, .Name = "Ben", .Programme = "CSE",
                      .Marks = New List(Of Integer) From {55, 62, 58}},
    New Student With {.Id = 3, .Name = "Chen", .Programme = "IT",
                      .Marks = New List(Of Integer) From {88, 91, 84}}
}

Dim report = students.
    Select(Function(s) New With {
        .s = s,
        .Average = If(s.Marks.Count = 0, 0.0,
                      s.Marks.Average()),
        .Passed = s.Marks.Count > 0 AndAlso s.Marks.Average() >= 50
    }).
    Where(Function(x) x.Passed).
    OrderByDescending(Function(x) x.Average).
    ToList()

Asha’s average is (80 + 75 + 90) ÷ 3 = 245 ÷ 3 ≈ 81.67. Ben’s average is 175 ÷ 3 ≈ 58.33, and Chen’s average is 263 ÷ 3 ≈ 87.67. The resulting order is Chen, Asha, and Ben.

The example illustrates a subtle design issue: the average is computed twice in the original projection. For a small in-memory list this is harmless, but repeated expensive calculations should be avoided. A clearer approach is to project marks and average once, then filter:

Dim report = students.
    Select(Function(s) New With {
        .Student = s,
        .Average = If(s.Marks.Any(), s.Marks.Average(), 0.0)
    }).
    Where(Function(x) x.Average >= 50).
    OrderByDescending(Function(x) x.Average).
    ToList()

The event model can notify another component when a report is generated:

Public Class ReportService
    Public Event ReportGenerated As EventHandler(Of ReportEventArgs)

    Public Sub Publish(report As Object)
        RaiseEvent ReportGenerated(Me, New ReportEventArgs(report))
    End Sub
End Class

A delegate-based formatter can then be injected, allowing CSV, HTML, or JSON output without changing the report-generation algorithm. This combination demonstrates separation of concerns: collections hold data, LINQ transforms it, delegates customize behaviour, and events communicate completion.

7. Case Study: Illustrative Smart Inventory Monitor

Background

A warehouse application receives stock updates from scanners. Managers need alerts when inventory falls below a reorder level, while an audit component records every accepted update.

Problem

A tightly coupled scanner class that directly calls email, database, and dashboard code becomes difficult to test and change. It may also notify users repeatedly for the same condition.

Approach

The inventory service stores items in a Dictionary(Of String, InventoryItem) for key-based lookup. It exposes an InventoryLow event. Subscribers attach handlers for email, dashboard, and audit functions. LINQ produces a management view of all items below their reorder level:

Dim lowStock = items.Values.
    Where(Function(i) i.Quantity < i.ReorderLevel).
    OrderBy(Function(i) i.Quantity).
    Select(Function(i) New With {
        .Code = i.Code,
        .Quantity = i.Quantity,
        .ReorderLevel = i.ReorderLevel
    }).ToList()

A predicate supplied as a delegate determines whether an item is critical, and a lambda formats the alert message. The service raises an event only after validating the item code, quantity, and timestamp. Subscribers are removed when a dashboard session closes.

Outcome

The service becomes independently testable. A test can supply an in-memory dictionary, invoke an update, and verify that the event was raised with correct event arguments. New notification channels can subscribe without modifying inventory logic. Materializing lowStock gives the dashboard a stable snapshot rather than a query whose results may change during rendering.

Learning points

  1. A dictionary expresses the key-based access requirement.
  2. Events reduce coupling between a publisher and subscribers.
  3. Delegates and lambdas inject policies and formatting behaviour.
  4. LINQ makes filtering and projection readable, but materialization must be intentional.
  5. Validation and authorization must occur before publishing operational events.

8. Design, Security, and Governance Considerations

These features are technically powerful but do not automatically produce safe software. Event handlers may expose personal or operational data to unauthorized subscribers. LINQ queries may leak sensitive fields if projections return entire objects rather than required properties. Query providers must use parameterized translation; constructing raw SQL through string concatenation creates injection risk.

Applications should validate event inputs, apply access control before publishing data, minimize retained references, and log significant failures without recording unnecessary personal information. Deterministic ordering should be specified when users depend on presentation order. Public APIs should document whether returned sequences are mutable, lazy, or snapshots.

In concurrent systems, event publication may race with subscription changes or object disposal. UI applications must respect thread affinity. Asynchronous handlers require careful exception observation and cancellation design. A lambda that launches background work without awaiting it can create unobserved failures and premature object disposal.

9. Contemporary Practice and Best Practices

Current .NET development increasingly combines LINQ with asynchronous streams, immutable records, nullable-reference analysis, dependency injection, and source-generated tooling. These developments reinforce several principles:

  • Prefer generic, strongly typed APIs and enable Option Strict On.
  • Use the narrowest collection interface required by a method.
  • Prefer immutable data for keys, event arguments, and shared state.
  • Keep event handlers small and detach subscriptions when lifetimes end.
  • Materialize LINQ queries at deliberate boundaries.
  • Avoid multiple enumeration of expensive or remote sequences.
  • Inspect provider-generated queries and measure rather than guessing.
  • Use Any() for existence tests instead of counting an entire sequence.
  • Guard First, Single, and dictionary access according to expected cardinality.
  • Treat captured variables and closures as objects with lifetime implications.
  • Separate domain rules from formatting, persistence, and notification.
  • Profile allocation and query performance for production workloads.

Chapter Summary

Delegates provide type-safe references to methods and enable callbacks, strategies, and dependency injection. Events use delegates to implement a publisher–subscriber model, but subscriptions create lifetime and concurrency responsibilities. Lambda expressions offer concise anonymous functions and may capture surrounding variables through closures. Generic collections provide different trade-offs for ordering, indexing, uniqueness, and key-based lookup. LINQ combines these ideas into composable pipelines for filtering, projection, ordering, grouping, joining, and aggregation. Deferred execution improves flexibility but requires deliberate materialization when stable results or controlled performance are required. Professional use demands attention to equality, mutability, provider translation, security, thread affinity, and resource lifetime.

Key Terms

Delegate: A type-safe reference to one or more methods.

Multicast delegate: A delegate containing an invocation list of multiple methods.

Callback: A method supplied to another method for later invocation.

Event: A notification mechanism based on delegate handlers.

Publisher: The object that raises an event.

Subscriber: An object that handles an event.

Event arguments: Data carried with an event notification.

Lambda expression: An inline anonymous function.

Closure: A lambda together with captured variables from its surrounding scope.

Generic collection: A type-safe collection parameterized by one or more types.

Dictionary: A key-value collection optimized for key-based access.

Hash set: A collection of unique values based on equality and hashing.

LINQ: Language Integrated Query for querying data sources.

Projection: Transformation of each source element into a new form.

Deferred execution: Evaluation postponed until a sequence is enumerated.

Materialization: Conversion of a sequence into a concrete collection.

IEnumerable: An interface representing an enumerable sequence.

IQueryable: An interface representing provider-translatable query expressions.

Extension method: A method callable as though it were an instance method of an existing type.

Invocation list: The ordered set of methods held by a multicast delegate.

Review Questions

Short Answer Questions

  1. What signature requirements must a method satisfy to be assigned to a delegate?
  2. Why is the conventional .NET event pattern based on sender and EventArgs?
  3. Distinguish an expression lambda from a statement lambda.
  4. When is a HashSet(Of T) more suitable than a List(Of T)?
  5. What is deferred execution in LINQ?

Long Answer Questions

  1. Explain delegates, multicast delegates, and generic framework delegates with suitable Visual Basic examples.
  2. Describe the publisher–subscriber event model and analyse subscription lifetime, memory retention, and exception handling.
  3. Compare List, Dictionary, HashSet, Queue, and Stack in terms of semantics and typical use.
  4. Explain the major LINQ operators and compare method syntax with query syntax.
  5. Discuss the differences between IEnumerable(Of T) and IQueryable(Of T) and their performance implications.

Analytical / Application Questions

  1. Design an event-driven order-processing component that uses a delegate for discount policy, a collection for order storage, and LINQ for reporting. Justify each design choice.
  2. A query is enumerated three times against a remote data source and repeatedly recomputes an expensive projection. Diagnose the problem and propose an improved design.
  3. An application retains closed dashboard objects after they subscribe to a long-lived inventory event. Explain the cause and propose at least two corrective strategies.

Multiple Choice Questions with Answers

Question 1

Which statement best describes a delegate in Visual Basic?

A. A type-safe reference to one or more methods

B. A collection that stores only strings

C. A database table definition

D. A mechanism that prevents all exceptions

Correct Answer: A. A type-safe reference to one or more methods

Question 2

Which delegate type is most appropriate for a method that accepts an Integer and returns a Boolean?

A. Action(Of Integer)

B. Func(Of Integer, Boolean)

C. Action

D. Comparison(Of Integer)

Correct Answer: B. Func(Of Integer, Boolean)

Question 3

What is the conventional purpose of the sender parameter in an event handler?

A. It contains the event result

B. It identifies the object that raised the event

C. It stores the subscriber list

D. It forces synchronous execution

Correct Answer: B. It identifies the object that raised the event

Question 4

Which collection is most suitable for enforcing uniqueness of its elements?

A. List(Of T)

B. Queue(Of T)

C. HashSet(Of T)

D. Stack(Of T)

Correct Answer: C. HashSet(Of T)

Question 5

What does the LINQ Where operator do?

A. Transforms every element into another type

B. Filters elements according to a predicate

C. Sorts elements alphabetically

D. Materializes a sequence as an array

Correct Answer: B. Filters elements according to a predicate

Question 6

Which operation normally causes immediate LINQ execution and materialization?

A. Where

B. Select

C. OrderBy

D. ToList

Correct Answer: D. ToList

Question 7

A closure is best described as:

A. A collection sorted by hash code

B. A lambda together with variables captured from its surrounding scope

C. An event that has no subscribers

D. A query that cannot be executed

Correct Answer: B. A lambda together with variables captured from its surrounding scope

Question 8

If a student has marks 70, 80, and 90 with equal weighting, what is the average?

A. 75

B. 80

C. 85

D. 240

Correct Answer: B. 80

Question 9

Which structure is generally most appropriate for lookup by a unique key?

A. Dictionary(Of TKey, TValue)

B. Queue(Of T)

C. Stack(Of T)

D. LinkedList(Of T)

Correct Answer: A. Dictionary(Of TKey, TValue)

Question 10

Why should a subscriber often remove a handler from a long-lived publisher?

A. To change the delegate return type

B. To prevent the publisher from retaining the subscriber unnecessarily

C. To force LINQ queries to execute

D. To convert an event into a collection

Correct Answer: B. To prevent the publisher from retaining the subscriber unnecessarily

Programming in Visual Basic
Chapter 5 of 5