Interfaces

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete console program that starts with Option Strict On, was built with warnings treated as errors and was run in the .NET 10 SDK container (Linux); the output shown is real output. The examples that are meant to fail say so, and show the compiler messages that the SDK printed. The examples use the root namespace ShelfDocs, which is why a few runtime messages show it. The project-level imports that the SDK adds (System, System.Collections.Generic, System.Linq and others) are why Console, List(Of T) and Enumerable methods need no Imports.

An interface is a contract: a named list of members (methods, properties, events) that a class or structure promises to provide, with no implementation and no state. Code that depends on the interface does not know, or care, which type is behind it. That makes interfaces the unit of substitution in Visual Basic: a unit test replaces a database store with an in-memory fake, the For Each statement walks any IEnumerable(Of T), List(Of T).Sort orders any type that implements IComparable(Of T), and a WPF or Windows Forms binding listens to anything that implements INotifyPropertyChanged. The running example is the shelf library catalogue, whose Catalog class talks to an ICatalogStore and never to a concrete store.

Visual Basic interfaces are the .NET interfaces, so what you know from C# carries over. The differences are in the syntax and they are worth learning precisely, because Visual Basic implements an interface explicitly by name, member by member, with the Implements clause. There is no implicit matching by name and signature as in C#, and no separate "explicit interface implementation" form: every implementation is explicit in that it names the member it implements, and a Private method can implement a member the way a C# explicit implementation does. The topics of this page are collected in the table below.

Topic Visual Basic Where it is explained

Declare a contract

Interface ... End Interface

Declaring an interface

Promise a contract

Implements statement in the class or structure

Implementing an interface

Say which member implements which

Implements clause on the member

The Implements clause

Combine contracts

Inherits inside an interface; several Implements entries

Interface inheritance and name clashes

Inheritance and interfaces together

re-implementation, Overridable

Dispatch and re-implementation

Interfaces and value types

boxing

Structures that implement interfaces

Type parameters

(Of T), Out, In

Generic interfaces and variance

The framework’s contracts

IComparable(Of T), IEnumerable(Of T), IDisposable, INotifyPropertyChanged

The standard .NET interfaces

Events and indexers in a contract

Event, Default property

Events and default properties in an interface

C# features VB can only consume

default interface members, static abstract

Interfaces from C#

Declaring an interface

[attributes] [accessmodifier] [Shadows] Interface Name [(Of typeparameters)]
    [Inherits InterfaceA, InterfaceB]
    member declarations
End Interface
  • An interface can be declared at namespace level, or inside a class, structure, module or another interface. Without an access modifier a top-level interface is Friend (visible inside the assembly), so a contract that other assemblies must see is written Public Interface.

  • The members are Sub, Function, Property, Event and nested type declarations. A nested Enum and a nested Class compiled on 17.13 and are used as IShelf.Zone; use nested types sparingly, because they hide in the contract. Fields, constants, constructors and members with a body are not allowed in an interface. The fields and constants are rejected with BC30602, a body with BC30603 and a Sub New with BC30363 (see Errors the compiler reports).

  • Interface members are implicitly Public, and you must not write a modifier on them. Public, Shared, Overridable and MustOverride are each rejected with BC30270: the interface says what, the implementing type says how.

  • A property is declared with its shape only: Property Name As String (read/write), ReadOnly Property Name As String or WriteOnly Property Name As String. There is no Get/Set block. The implementing property may offer more than the interface asks for: a ReadOnly interface property can be implemented by a read/write property (the extra setter is just not reachable through the interface).

  • A parameter in an interface member is written exactly as in a class: ByVal/ByRef, Optional with a default, ParamArray. The implementation must have the same parameter types, modifiers (ByVal versus ByRef), return type and, for an Optional parameter, the same default value: an implementation that wrote Optional text As String = "b" for an interface member that says = "a", or that dropped Optional, is BC30401. The parameter names may differ (count in the interface, n in the class).

  • By convention the name starts with I (ICatalogStore, IComparable); the .NET design guidelines use that prefix for every interface, and Visual Basic code follows it.

A new interface with a few members, two implementations and a client:

Option Strict On

Public MustInherit Class CatalogItem
    Public Property Id As Integer
    Public Property Title As String = ""
    Public MustOverride ReadOnly Property Kind As String
End Class

Public Class Book
    Inherits CatalogItem

    Public Overrides ReadOnly Property Kind As String
        Get
            Return "Book"
        End Get
    End Property
End Class

Public Class Magazine
    Inherits CatalogItem

    Public Overrides ReadOnly Property Kind As String
        Get
            Return "Magazine"
        End Get
    End Property
End Class

Public Interface ICatalogStore
    Function GetAll() As IReadOnlyList(Of CatalogItem)
    Sub Add(item As CatalogItem)
    Function Find(id As Integer) As CatalogItem
End Interface

Public Class InMemoryCatalogStore
    Implements ICatalogStore

    Private ReadOnly _items As New List(Of CatalogItem)

    Public Function GetAll() As IReadOnlyList(Of CatalogItem) Implements ICatalogStore.GetAll
        Return _items
    End Function

    Public Sub Add(item As CatalogItem) Implements ICatalogStore.Add
        _items.Add(item)
    End Sub

    Public Function Find(id As Integer) As CatalogItem Implements ICatalogStore.Find
        Return _items.FirstOrDefault(Function(item) item.Id = id)
    End Function
End Class

' A decorator: it is an ICatalogStore and it has one.
Public Class CountingCatalogStore
    Implements ICatalogStore

    Private ReadOnly _inner As ICatalogStore

    Public ReadOnly Property Calls As Integer

    Public Sub New(inner As ICatalogStore)
        _inner = inner
    End Sub

    Public Function GetAll() As IReadOnlyList(Of CatalogItem) Implements ICatalogStore.GetAll
        _Calls += 1
        Return _inner.GetAll()
    End Function

    Public Sub Add(item As CatalogItem) Implements ICatalogStore.Add
        _Calls += 1
        _inner.Add(item)
    End Sub

    Public Function Find(id As Integer) As CatalogItem Implements ICatalogStore.Find
        _Calls += 1
        Return _inner.Find(id)
    End Function
End Class

Public Class Catalog
    Private ReadOnly _store As ICatalogStore

    Public Sub New(store As ICatalogStore)
        _store = store
    End Sub

    Public Sub Add(item As CatalogItem)
        _store.Add(item)
    End Sub

    Public Function Count() As Integer
        Return _store.GetAll().Count
    End Function
End Class

Module Program
    Sub Main()
        Dim counting As New CountingCatalogStore(New InMemoryCatalogStore())
        Dim catalog As New Catalog(counting)

        catalog.Add(New Book With {.Id = 1, .Title = "Domain Modelling"})
        catalog.Add(New Magazine With {.Id = 2, .Title = "Shelf Monthly"})
        Console.WriteLine("items: " & catalog.Count())
        Console.WriteLine("calls on the decorator: " & counting.Calls)

        Dim store As ICatalogStore = counting
        Console.WriteLine("Find(2): " & store.Find(2).Title)
        Console.WriteLine("Find(9) is Nothing: " & (store.Find(9) Is Nothing))
        Console.WriteLine("calls now: " & counting.Calls)

        ' An interface variable lets the compiler check what may be called, nothing more.
        Console.WriteLine("variable type: " & store.GetType().Name)
        Console.WriteLine("is ICatalogStore: " & (TypeOf counting Is ICatalogStore))
        Dim asDisposable = TryCast(store, IDisposable)
        Console.WriteLine("is IDisposable: " & (asDisposable IsNot Nothing))
    End Sub
End Module
items: 2
calls on the decorator: 3
Find(2): Shelf Monthly
Find(9) is Nothing: True
calls now: 5
variable type: CountingCatalogStore
is ICatalogStore: True
is IDisposable: False

What the example shows:

  1. Catalog depends on ICatalogStore. Constructing it with a CountingCatalogStore that wraps an InMemoryCatalogStore required no change to Catalog. That is the decorator pattern, and it is possible because the decorator is also an ICatalogStore (see Design Patterns in Visual Basic).

  2. A variable declared As ICatalogStore exposes only the interface’s members plus the members of Object. The run-time type (store.GetType().Name) is still CountingCatalogStore, and TypeOf ... Is ICatalogStore and TryCast(store, IDisposable) test and convert at run time. TryCast returns Nothing when the object does not implement the interface; DirectCast throws InvalidCastException.

  3. Find(9) Is Nothing is True: the contract of Find is "returns Nothing when there is no such item". An interface cannot express that rule in the language, so write it in the XML documentation of the member (see XML Documentation, Analyzers and Code Style).

  4. _Calls += 1 increments the hidden backing field of the auto-property Calls. The field is named _Calls, with a leading underscore and the property’s spelling; it is accessible inside the class only. A ReadOnly auto-property needs no Set, and code inside the class can still assign to the backing field.

The structure of the example is shown below. The dashed arrows with a hollow head mean "implements".

classDiagram direction LR class ICatalogStore { <<interface>> +GetAll() IReadOnlyList +Add(item) +Find(id) CatalogItem } class InMemoryCatalogStore class CountingCatalogStore { +Calls Integer } class Catalog { +Add(item) +Count() Integer } ICatalogStore <|.. InMemoryCatalogStore ICatalogStore <|.. CountingCatalogStore CountingCatalogStore o-- ICatalogStore : wraps Catalog --> ICatalogStore : uses

Implementing an interface

A class or structure promises a contract with the Implements statement, which is placed first in the type, after Inherits (if there is one) and before any member declaration. A type can name several interfaces, separated by commas or on several Implements lines.

Public Class Report
    Inherits ReportBase                    ' at most one base class, first
    Implements INamed, IExporter           ' any number of interfaces
    Implements IPrinter                    ' more can follow on another line
    ...
End Class

The Implements statement alone is not enough. The compiler then demands that the type contains, for every member of every interface named, a member that says it implements that interface member.

Program.vb(9,16): error BC30149: Class 'MemoryStore' must implement 'Function Count() As Integer' for interface 'ICatalogStore'.
Option Strict On

Public Interface ICatalogStore
    Sub Add(title As String)
    Function Count() As Integer
End Interface

Public Class MemoryStore
    Implements ICatalogStore

    Public Sub Add(title As String) Implements ICatalogStore.Add
    End Sub
End Class

Module Program
    Sub Main()
    End Sub
End Module

The message names the missing member with its full signature, which is exactly what you need to paste into the class. Visual Studio offers "Implement Interface" on the light bulb of the Implements line (or on the error) and writes every missing member with the clauses filled in.

The Implements clause

The member that implements an interface member says so with an Implements clause after its signature:

Public Sub Save() Implements IExporter.Export
Public Function Compare(x As Book, y As Book) As Integer Implements IComparer(Of Book).Compare
Public Property Name As String Implements INamed.Name
Public Event Changed As EventHandler Implements IShelfLabels.Changed

The rules, all of which the examples below demonstrate:

  • The member kind must match (Sub for a Sub, Function for a Function, Property for a Property, Event for an Event) and so must the parameters and the return type. A mismatch is BC30401.

  • The member name does not have to match. Save() above implements Export().

  • The accessibility does not matter to the compiler. A Public, Friend or Private member can implement an interface member. If it is Private, nobody can call it through the class, but a variable of the interface type reaches it. This is the Visual Basic counterpart of a C# explicit interface implementation.

  • One member can implement several interface members: Implements IPrinter.Print, IArchiver.Archive. The signatures of all of them must be compatible with the member.

  • One interface member can be implemented by only one member of a type. A second member that names the same interface member is BC30583.

  • A member that implements nothing is an ordinary member of the class.

  • For a member inherited from a base interface, the clause may name the interface that declares it (Implements IReadable.Read) or the derived interface that the type implements (Implements IStore.Read); both compile and map the same member.

  • Members that implement an interface member cannot be Shared (BC30505), and a Module cannot implement an interface at all (BC30231 and BC31083); an interface is implemented by instances.

  • A ReadOnly interface property can be implemented by a read/write property. The reverse does not compile: a read/write interface property implemented by a ReadOnly property is BC31444.

Option Strict On

Public Interface INamed
    Property Name As String
End Interface

Public Interface IExporter
    Sub Export()
    ReadOnly Property Format As String
End Interface

Public Interface IPrinter
    Sub Print()
End Interface

Public Interface IArchiver
    Sub Archive()
End Interface

Public Interface IAuditable
    Sub Audit()
End Interface

Public Class Report
    Implements INamed, IExporter, IPrinter, IArchiver, IAuditable

    ' An auto-property can implement an interface property.
    Public Property Name As String = "Quarterly loans" Implements INamed.Name

    ' A ReadOnly interface property can be implemented by a read/write property.
    Public Property Format As String = "csv" Implements IExporter.Format

    ' The method name does not have to match the interface member name.
    Public Sub Save() Implements IExporter.Export
        Console.WriteLine("exporting '" & Name & "' as " & Format)
    End Sub

    ' One method can implement members of several interfaces.
    Public Sub Output() Implements IPrinter.Print, IArchiver.Archive
        Console.WriteLine("Output called")
    End Sub

    ' A Private method can implement a member: only the interface can reach it.
    Private Sub AuditEntry() Implements IAuditable.Audit
        Console.WriteLine("audited " & Name)
    End Sub
End Class

Module Program
    Sub Main()
        Dim report As New Report()
        Dim exporter As IExporter = report
        Dim printer As IPrinter = report
        Dim archiver As IArchiver = report
        Dim named As INamed = report
        Dim auditable As IAuditable = report

        named.Name = "Overdue loans"
        exporter.Export()
        printer.Print()
        archiver.Archive()
        auditable.Audit()

        report.Save()
        report.Output()
        Console.WriteLine(named.Name & " / " & exporter.Format)
    End Sub
End Module
exporting 'Overdue loans' as csv
Output called
Output called
audited Overdue loans
exporting 'Overdue loans' as csv
Output called
Overdue loans / csv

Read the output in three parts. The first four lines are the calls made through the interface variables (exporter.Export() reaches Save, printer.Print() and archiver.Archive() both reach Output, auditable.Audit() reaches the Private AuditEntry; named.Name = ... wrote through INamed, which is why Save prints the new name). Lines five and six are the calls made on the class variable, report.Save() and report.Output(). The last line reads the property through the two interfaces. AuditEntry cannot be called through report: the compiler reports BC30390 (the member is Private), and report.Audit() is BC30456 because the class has no member of that name.

A mapping you can inspect

The mapping from the interface member to the implementing member is recorded in the compiled type and is available to reflection through Type.GetInterfaceMap. A program that prints the map for the Private AuditEntry above printed:

interface: Audit
target   : AuditEntry private=True virtual=True final=True

The implementing method keeps its own name (AuditEntry), unlike a C# explicit implementation, whose method name includes the interface name. It is Private, virtual and final (sealed): not reachable by name, still dispatched through the interface, and not overridable by a derived class unless that class re-implements the interface (see Dispatch and re-implementation).

Name clashes and several interfaces

Two interfaces can declare the same member name with the same signature. With C#, that is the textbook reason to use an explicit implementation. With Visual Basic the clause already names the interface, so the clash resolves itself: give each interface member its own implementing method, or let one method serve both.

Option Strict On

Public Interface ILender
    Sub [Return]()
End Interface

Public Interface IReturnable
    Sub [Return]()
End Interface

' Two interfaces declare a member with the same signature. The class
' picks, member by member, what each interface member runs.
Public Class Loan
    Implements ILender, IReturnable

    Public Sub LenderReturn() Implements ILender.Return
        Console.WriteLine("lender side: book is back on the shelf")
    End Sub

    Public Sub BorrowerReturn() Implements IReturnable.Return
        Console.WriteLine("borrower side: receipt printed")
    End Sub

    ' Not an implementation of anything: it is just a method called Return.
    Public Sub [Return]()
        Console.WriteLine("Loan.Return called through the class")
    End Sub
End Class

Public Class SimpleLoan
    Implements ILender, IReturnable

    ' One method can serve both members when the behaviour is the same.
    Public Sub [Return]() Implements ILender.Return, IReturnable.Return
        Console.WriteLine("SimpleLoan.Return")
    End Sub
End Class

Module Program
    Sub Main()
        Dim loan As New Loan()
        DirectCast(loan, ILender).Return()
        DirectCast(loan, IReturnable).Return()
        loan.Return()

        Dim simple As New SimpleLoan()
        Dim lender As ILender = simple
        Dim returnable As IReturnable = simple
        lender.Return()
        returnable.Return()
    End Sub
End Module
lender side: book is back on the shelf
borrower side: receipt printed
Loan.Return called through the class
SimpleLoan.Return
SimpleLoan.Return

Three things are visible here. Return is a reserved word, so a member with that name is written [Return] in the declaration (and Return after a dot, where the context makes it a name). Loan has three different methods, and which one runs depends on the type of the expression, not on the object. And SimpleLoan shows the one-method-for-two-members form, which is correct when the two interfaces mean the same thing by the member.

Interface inheritance and name clashes

An interface can inherit one or more interfaces with the Inherits statement inside the interface body. (A class has at most one base class, an interface has any number of base interfaces; that is the one place where Visual Basic has multiple inheritance.) A type that implements the derived interface implements every base interface too, and the Implements clause of a member that comes from a base interface names the interface that declares it.

Option Strict On

Public Interface IReadable
    Function Read(id As Integer) As String
End Interface

Public Interface IWritable
    Sub Write(id As Integer, text As String)
End Interface

' An interface can inherit several interfaces. Implementing it means implementing all members.
Public Interface IReadWriteStore
    Inherits IReadable, IWritable

    ReadOnly Property Count As Integer
End Interface

Public Class DictionaryStore
    Implements IReadWriteStore

    Private ReadOnly _data As New Dictionary(Of Integer, String)

    Public ReadOnly Property Count As Integer Implements IReadWriteStore.Count
        Get
            Return _data.Count
        End Get
    End Property

    ' The inherited members are named after the interface that declares them.
    Public Function Read(id As Integer) As String Implements IReadable.Read
        Dim text As String = Nothing
        Return If(_data.TryGetValue(id, text), text, "(missing)")
    End Function

    Public Sub Write(id As Integer, text As String) Implements IWritable.Write
        _data(id) = text
    End Sub
End Class

Module Program
    Sub Show(source As IReadable, id As Integer)
        Console.WriteLine(id & " -> " & source.Read(id))
    End Sub

    Sub Fill(target As IWritable)
        target.Write(1, "Domain Modelling")
        target.Write(2, "Shelf Monthly")
    End Sub

    Sub Main()
        Dim store As IReadWriteStore = New DictionaryStore()
        Fill(store)                 ' IReadWriteStore widens to IWritable
        Show(store, 1)              ' ... and to IReadable
        Show(store, 3)
        Console.WriteLine("count: " & store.Count)

        ' The inherited members are available through the derived interface.
        Console.WriteLine(store.Read(2))

        ' Narrowing needs a cast, and the cast can fail.
        Dim readOnlyView As IReadable = store
        Dim writable = TryCast(readOnlyView, IWritable)
        Console.WriteLine("back to IWritable: " & (writable IsNot Nothing))
    End Sub
End Module
1 -> Domain Modelling
3 -> (missing)
count: 2
Shelf Monthly
back to IWritable: True
  • A derived interface converts implicitly to each of its base interfaces (IReadWriteStore to IReadable and IWritable). The opposite direction needs a cast: TryCast(readOnlyView, IWritable) returns Nothing when the object does not implement it.

  • Inherits must be the first statement in the body. A type that implements the derived interface does not have to repeat the base interfaces in its own Implements statement.

  • An interface cannot inherit from itself, directly or indirectly (the compiler reports a cycle).

When two base interfaces declare a member with the same name, a call through the derived interface is ambiguous. The compiler reports BC30521 on the call and the program does not build. The remedies are a cast to the interface that declares the member you mean, or Shadows on a new member in the derived interface that hides both:

Option Strict On

Public Interface IReadable
    ReadOnly Property Name As String
End Interface

Public Interface IWritable
    ReadOnly Property Name As String
End Interface

' Inheriting two interfaces with a member of the same name makes a call on the derived interface ambiguous.
Public Interface IReadWrite
    Inherits IReadable, IWritable
End Interface

' Shadows declares a new member that hides both inherited ones.
Public Interface IShadowedReadWrite
    Inherits IReadable, IWritable

    Shadows ReadOnly Property Name As String
End Interface

Public Class Store
    Implements IReadWrite, IShadowedReadWrite

    Private ReadOnly Property ReadName As String Implements IReadable.Name
        Get
            Return "read side"
        End Get
    End Property

    Private ReadOnly Property WriteName As String Implements IWritable.Name
        Get
            Return "write side"
        End Get
    End Property

    Private ReadOnly Property BothName As String Implements IShadowedReadWrite.Name
        Get
            Return "shadowing member"
        End Get
    End Property
End Class

Module Program
    Sub Main()
        Dim store As New Store()

        Dim ambiguous As IReadWrite = store
        ' ambiguous.Name is BC30521. Convert to the interface that declares the member you mean.
        Console.WriteLine(DirectCast(ambiguous, IReadable).Name)
        Console.WriteLine(DirectCast(ambiguous, IWritable).Name)

        Dim shadowed As IShadowedReadWrite = store
        Console.WriteLine(shadowed.Name)
        Console.WriteLine(DirectCast(shadowed, IReadable).Name)
    End Sub
End Module
read side
write side
shadowing member
read side

Source: Shadows

The same program with Dim ambiguous As IReadWrite = store followed by ambiguous.Name was compiled for this page:

Program.vb(17,33): error BC30521: Overload resolution failed because no accessible 'Name' is most specific for these arguments:

The error text continues with the two candidate declarations, one per line. The first line is enough to recognise it.

Dispatch and re-implementation

An interface call is virtual: the runtime finds the method that the object’s type mapped to the interface member. If the implementing member is Overridable, a derived class can override it and the interface call reaches the override, exactly as with an ordinary virtual method. But a derived class can also hide the implementing member with Shadows, and then two different things can happen, depending on whether the derived class re-implements the interface by repeating Implements.

Option Strict On

Public Interface IDescribable
    Function Describe() As String
End Interface

Public Class CatalogItem
    Implements IDescribable

    ' Overridable: the interface call is dispatched to the most derived override.
    Public Overridable Function Describe() As String Implements IDescribable.Describe
        Return "CatalogItem.Describe"
    End Function
End Class

Public Class Book
    Inherits CatalogItem

    Public Overrides Function Describe() As String
        Return "Book.Describe"
    End Function
End Class

Public Class ShadowingItem
    Inherits CatalogItem

    ' Shadows hides the base method for class callers, but the base class still implements the interface.
    Public Shadows Function Describe() As String
        Return "ShadowingItem.Describe (shadows)"
    End Function
End Class

Public Class Reimplemented
    Inherits CatalogItem
    Implements IDescribable        ' re-implementation: the interface is mapped again

    Public Shadows Function Describe() As String Implements IDescribable.Describe
        Return "Reimplemented.Describe"
    End Function
End Class

Module Program
    Sub Show(label As String, item As CatalogItem)
        Dim viaClass = item.Describe()
        Dim viaInterface = DirectCast(item, IDescribable).Describe()
        Console.WriteLine(label.PadRight(14) & viaClass.PadRight(28) & viaInterface)
    End Sub

    Sub Main()
        Console.WriteLine("variable".PadRight(14) & "via class".PadRight(28) & "via interface")
        Show("Book", New Book())
        Show("ShadowingItem", New ShadowingItem())
        Show("Reimplemented", New Reimplemented())

        ' With the exact derived type the shadowing methods are the ones seen.
        Dim shadowing As New ShadowingItem()
        Dim reimplemented As New Reimplemented()
        Console.WriteLine(shadowing.Describe())
        Console.WriteLine(reimplemented.Describe())
    End Sub
End Module
variable      via class                   via interface
Book          Book.Describe               Book.Describe
ShadowingItem CatalogItem.Describe        CatalogItem.Describe
Reimplemented CatalogItem.Describe        Reimplemented.Describe
ShadowingItem.Describe (shadows)
Reimplemented.Describe

The table summarises the three rows of output.

Derived class Call through the class Call through the interface

Book overrides Overridable Describe

its override

its override: the interface method is the virtual method

ShadowingItem declares Shadows Function Describe()

the base method; Shadows only changes what the derived-class name means to callers who hold a ShadowingItem

the base method: the interface is still mapped to CatalogItem.Describe

Reimplemented repeats Implements IDescribable

the base method through a CatalogItem variable

the new method: the derived class has taken over the interface

The last two lines of the output show what a caller holding the exact derived type sees: the Shadows function in both cases, because the compiler binds shadowing.Describe() to the most derived declaration visible by name. Prefer Overridable/Overrides and keep the interface mapped in one place. Re-implementation is legitimate when you do not own the base class, but it makes the behaviour depend on the static type of the reference, which is exactly what a virtual method is meant to avoid. See Inheritance and Polymorphism for Shadows versus Overrides.

Structures that implement interfaces

A structure can implement interfaces (Isbn of the shelf implements IEquatable(Of Isbn)). Converting a structure to an interface type boxes it: the common language runtime copies the value into a new object on the heap and the interface variable refers to that copy. Calls through the interface mutate the copy.

Option Strict On

Public Interface ICounter
    Sub Increment()
    ReadOnly Property Value As Integer
End Interface

Public Structure Counter
    Implements ICounter

    Private _value As Integer

    Public Sub Increment() Implements ICounter.Increment
        _value += 1
    End Sub

    Public ReadOnly Property Value As Integer Implements ICounter.Value
        Get
            Return _value
        End Get
    End Property
End Structure

Module Program
    Sub Main()
        Dim a As New Counter()
        a.Increment()
        Console.WriteLine("a.Value after a.Increment(): " & a.Value)

        ' Converting to the interface boxes a copy of the structure.
        Dim boxed As ICounter = a
        boxed.Increment()
        boxed.Increment()
        Console.WriteLine("boxed.Value after two calls: " & boxed.Value)
        Console.WriteLine("a.Value is unchanged:       " & a.Value)

        ' Two boxes are two copies; assigning a box copies the reference, not the structure.
        Dim alias1 As ICounter = boxed
        alias1.Increment()
        Console.WriteLine("boxed.Value through the alias: " & boxed.Value)

        ' A constrained generic call does not box.
        Dim b As New Counter()
        Bump(b)
        Console.WriteLine("b.Value after Bump(Of T) with ByRef: " & b.Value)
    End Sub

    Sub Bump(Of T As {Structure, ICounter})(ByRef counter As T)
        counter.Increment()
    End Sub
End Module
a.Value after a.Increment(): 1
boxed.Value after two calls: 3
a.Value is unchanged:       1
boxed.Value through the alias: 4
b.Value after Bump(Of T) with ByRef: 1

Read the output line by line. a is incremented once, so a.Value is 1. boxed = a copies a (value 1) into a box; two increments through the box make it 3, while a stays 1. Assigning boxed to alias1 copies the reference to the same box, so the increment through alias1 shows through boxed (4). The generic method Bump constrained with As {Structure, ICounter} takes the argument ByRef and calls the interface member on the original structure without boxing it, which is the way to keep a mutable structure mutable behind an interface. Mutable structures remain a design smell; the better answer is an immutable structure with methods that return a new value (see Structures and Value Types).

Two practical consequences:

  • A generic algorithm over T As IComparable(Of T) does not box when T is a structure, because the JIT compiles a separate version for each structure type and calls the interface method directly. The same algorithm over a variable declared As IComparable(Of T) boxes every element.

  • A For Each over a List(Of T) uses the structure enumerator List(Of T).Enumerator, not IEnumerator(Of T), and does not box; through an IEnumerable(Of T) variable the same loop does.

Generic interfaces and variance

An interface can have type parameters with constraints, exactly like a class (see Generics). The constraint T As CatalogItem of IRepository is checked wherever the interface is closed: IRepository(Of String) does not compile. A type can implement the same generic interface several times with different type arguments, as long as each closed interface is distinct.

An interface type parameter may be marked Out (covariant: it appears only in return positions) or In (contravariant: it appears only in parameters). Variance is allowed for interfaces and delegates, never for classes or structures. The compiler checks the declaration and, under Option Strict On, converts between closed types that differ by variance without a cast.

Option Strict On

Public MustInherit Class CatalogItem
    Public Property Title As String = ""
End Class

Public Class Book
    Inherits CatalogItem
End Class

Public Class Magazine
    Inherits CatalogItem
End Class

' T must be a CatalogItem: the constraint is checked wherever the interface is closed.
Public Interface IRepository(Of T As CatalogItem)
    Sub Add(item As T)
    Function GetAll() As IEnumerable(Of T)
End Interface

Public Class ListRepository(Of T As CatalogItem)
    Implements IRepository(Of T)

    Private ReadOnly _items As New List(Of T)

    Public Sub Add(item As T) Implements IRepository(Of T).Add
        _items.Add(item)
    End Sub

    Public Function GetAll() As IEnumerable(Of T) Implements IRepository(Of T).GetAll
        Return _items
    End Function
End Class

' Out: T appears only in output positions. In: T appears only in input positions.
Public Interface IProducer(Of Out T)
    Function Produce() As T
End Interface

Public Interface IPrinter(Of In T)
    Sub Print(item As T)
End Interface

Public Class BookProducer
    Implements IProducer(Of Book)

    Public Function Produce() As Book Implements IProducer(Of Book).Produce
        Return New Book With {.Title = "Produced"}
    End Function
End Class

Public Class ItemPrinter
    Implements IPrinter(Of CatalogItem)

    Public Sub Print(item As CatalogItem) Implements IPrinter(Of CatalogItem).Print
        Console.WriteLine("printing " & item.GetType().Name & ": " & item.Title)
    End Sub
End Class

Module Program
    Sub Main()
        Dim books As IRepository(Of Book) = New ListRepository(Of Book)()
        books.Add(New Book With {.Title = "Shelf Design"})
        books.Add(New Book With {.Title = "Domain Modelling"})

        ' IEnumerable(Of Book) is covariant: it converts to IEnumerable(Of CatalogItem).
        Dim items As IEnumerable(Of CatalogItem) = books.GetAll()
        Console.WriteLine("items: " & items.Count())

        ' IProducer(Of Book) widens to IProducer(Of CatalogItem) (Out).
        Dim producer As IProducer(Of CatalogItem) = New BookProducer()
        Console.WriteLine("produced: " & producer.Produce().Title)

        ' IPrinter(Of CatalogItem) widens to IPrinter(Of Book) (In).
        Dim printer As IPrinter(Of Book) = New ItemPrinter()
        printer.Print(New Book With {.Title = "Async Cataloguing"})

        ' IRepository(Of T) is invariant: this line would be BC36757 under Option Strict On.
        ' Dim wrong As IRepository(Of CatalogItem) = books
        Console.WriteLine(GetType(IRepository(Of Book)).Name & " / " & GetType(IProducer(Of )).Name)
    End Sub
End Module
items: 2
produced: Produced
printing Book: Async Cataloguing
IRepository`1 / IProducer`1

Three conversions are demonstrated. A GetAll() returning IEnumerable(Of Book) is assignable to an IEnumerable(Of CatalogItem) because IEnumerable(Of Out T) is covariant. A BookProducer (an IProducer(Of Book)) is assignable to IProducer(Of CatalogItem) because Out T allows widening. An ItemPrinter, which prints any CatalogItem, is usable where an IPrinter(Of Book) is wanted because In T allows narrowing. The line that the example comments out, assigning an IRepository(Of Book) to an IRepository(Of CatalogItem), is invalid because IRepository(Of T) has T in both an input (Add) and an output (GetAll) position, so it is invariant. The compiler reports it as BC36757 and, helpfully but misleadingly, suggests Out T:

Program.vb(17,52): error BC36757: 'IRepository(Of Book)' cannot be converted to 'IRepository(Of CatalogItem)'. Consider changing the 'T' in the definition of 'Interface IRepository(Of T)' to an Out type parameter, 'Out T'.

That program declared IRepository(Of T) without constraints and with an Add(item As T) member, which is why Out T could not be applied to it: a covariant T may appear only in output positions. The suggestion is generic and does not check the interface. Arrays are covariant too (a Book() converts to a CatalogItem()), but a store into such an array can fail at run time with ArrayTypeMismatchException; the generic interfaces are checked at compile time instead.

Implementing the same generic interface twice:

Option Strict On

Public Interface IConverter(Of TIn, TOut)
    Function Convert(value As TIn) As TOut
End Interface

' The same generic interface can be implemented several times with different arguments.
Public Class FormatConverter
    Implements IConverter(Of Integer, String), IConverter(Of Date, String)

    Public Function ConvertNumber(value As Integer) As String Implements IConverter(Of Integer, String).Convert
        Return "#" & value.ToString("D4")
    End Function

    Public Function ConvertDate(value As Date) As String Implements IConverter(Of Date, String).Convert
        Return value.ToString("yyyy-MM-dd", Globalization.CultureInfo.InvariantCulture)
    End Function
End Class

Module Program
    Sub Main()
        Dim converter As New FormatConverter()
        Dim numbers As IConverter(Of Integer, String) = converter
        Dim dates As IConverter(Of Date, String) = converter

        Console.WriteLine(numbers.Convert(42))
        Console.WriteLine(dates.Convert(New Date(2026, 10, 19)))
        Console.WriteLine(GetType(FormatConverter).GetInterfaces().Length & " interfaces")
        For Each iface In GetType(FormatConverter).GetInterfaces().OrderBy(Function(t) t.ToString())
            Console.WriteLine("  " & iface.ToString())
        Next
    End Sub
End Module
#0042
2026-10-19
2 interfaces
  ShelfDocs.IConverter`2[System.DateTime,System.String]
  ShelfDocs.IConverter`2[System.Int32,System.String]

Each member names the closed interface it implements (IConverter(Of Integer, String).Convert), and each closed interface is its own entry in GetInterfaces(). A call converter.Convert(...) on the class variable would not compile, since the class has no member named Convert; callers use the interface variables.

The standard .NET interfaces

The framework contracts that a Visual Basic program implements most often are listed here with the namespace that holds them. Older sources get several of these wrong, because the non-generic ones moved or were replaced by generic versions; use the generic interface whenever there is one.

Interface Namespace What it is for

IComparable(Of T)

System

natural order of a type: List(Of T).Sort(), SortedSet(Of T), Enumerable.Max

IComparable

System

the non-generic version; implement both when you want old APIs (Array.Sort, ArrayList) to work

IEquatable(Of T)

System

typed equality without boxing; HashSet(Of T), Dictionary(Of K, V), List(Of T).Contains use it

IComparer(Of T)

System.Collections.Generic

an external order, kept apart from the type: Sort(New ByYearThenTitle())

IEqualityComparer(Of T)

System.Collections.Generic

an external equality and hash for dictionaries and sets

IEnumerable(Of T), IEnumerator(Of T)

System.Collections.Generic

For Each and LINQ; inherits the non-generic IEnumerable in System.Collections

ICollection(Of T), IList(Of T), IDictionary(Of K, V)

System.Collections.Generic

mutable collection contracts; prefer the read-only versions in signatures

IReadOnlyList(Of T), IReadOnlyCollection(Of T), IReadOnlyDictionary(Of K, V)

System.Collections.Generic

read-only views; the right return type of GetAll()

IDisposable, IAsyncDisposable

System

Using and Await Using release resources deterministically

INotifyPropertyChanged

System.ComponentModel

data binding in WPF, Windows Forms, MAUI

INotifyCollectionChanged

System.Collections.Specialized

observable collections for UI lists

IFormattable, IFormatProvider

System

ToString(format, provider) and culture-aware formatting

IConvertible

System

the Convert.ChangeType hook

ICloneable

System

avoid: it does not say whether the copy is shallow or deep

IServiceProvider

System

service lookup behind dependency injection

IComparable, IEquatable and IComparer

An ordering and an equality both belong with the type, and a second ordering belongs in a separate comparer. IComparable(Of T).CompareTo returns a negative number, zero or a positive number; it must be consistent with IEquatable(Of T).Equals (two objects that compare as zero should be equal) and with GetHashCode (equal objects must produce equal hashes). Visual Basic overloads Equals: the typed Equals(other As Book) implements the interface and needs Overloads, and the Overrides of Equals(Object) delegates to it (see Operator Overloading and Equality).

Option Strict On

Public Class Book
    Implements IComparable(Of Book), IEquatable(Of Book), IComparable

    Public ReadOnly Property Title As String
    Public ReadOnly Property Year As Integer

    Public Sub New(title As String, year As Integer)
        Me.Title = title
        Me.Year = year
    End Sub

    ' Natural order: by title, ignoring case.
    Public Function CompareTo(other As Book) As Integer Implements IComparable(Of Book).CompareTo
        If other Is Nothing Then Return 1
        Return String.Compare(Title, other.Title, StringComparison.OrdinalIgnoreCase)
    End Function

    ' The non-generic interface is still used by older APIs.
    Private Function CompareToObject(obj As Object) As Integer Implements IComparable.CompareTo
        Dim other = TryCast(obj, Book)
        If obj IsNot Nothing AndAlso other Is Nothing Then
            Throw New ArgumentException("Object must be a Book.", NameOf(obj))
        End If
        Return CompareTo(other)
    End Function

    Public Overloads Function Equals(other As Book) As Boolean Implements IEquatable(Of Book).Equals
        Return other IsNot Nothing AndAlso
               String.Equals(Title, other.Title, StringComparison.OrdinalIgnoreCase) AndAlso
               Year = other.Year
    End Function

    Public Overrides Function Equals(obj As Object) As Boolean
        Return Equals(TryCast(obj, Book))
    End Function

    Public Overrides Function GetHashCode() As Integer
        Return HashCode.Combine(Title.ToLowerInvariant(), Year)
    End Function

    Public Overrides Function ToString() As String
        Return Title & " (" & Year & ")"
    End Function
End Class

Public Class ByYearThenTitle
    Implements IComparer(Of Book)

    Public Function Compare(x As Book, y As Book) As Integer Implements IComparer(Of Book).Compare
        Dim byYear = x.Year.CompareTo(y.Year)
        Return If(byYear <> 0, byYear, x.CompareTo(y))
    End Function
End Class

Module Program
    Sub Main()
        Dim books As New List(Of Book) From {
            New Book("Shelf Design", 2021),
            New Book("domain modelling", 2019),
            New Book("Async Cataloguing", 2021)
        }

        books.Sort()                                  ' uses IComparable(Of Book)
        Console.WriteLine("natural    : " & String.Join("; ", books))

        books.Sort(New ByYearThenTitle())             ' uses IComparer(Of Book)
        Console.WriteLine("year, title: " & String.Join("; ", books))

        Dim set1 As New HashSet(Of Book) From {New Book("Shelf Design", 2021)}
        Console.WriteLine("contains equal book: " & set1.Contains(New Book("SHELF DESIGN", 2021)))
        Console.WriteLine("Max: " & books.Max().ToString())

        Dim sorted As New SortedSet(Of Book)(books)
        Console.WriteLine("SortedSet first: " & sorted.Min.ToString())

        Dim old As IComparable = New Book("A", 1)
        Try
            old.CompareTo("not a book")
        Catch ex As ArgumentException
            Console.WriteLine(ex.GetType().Name & ": " & ex.Message.Split(ControlChars.Lf)(0))
        End Try
    End Sub
End Module
natural    : Async Cataloguing (2021); domain modelling (2019); Shelf Design (2021)
year, title: domain modelling (2019); Async Cataloguing (2021); Shelf Design (2021)
contains equal book: True
Max: Shelf Design (2021)
SortedSet first: Async Cataloguing (2021)
ArgumentException: Object must be a Book. (Parameter 'obj')
  • books.Sort() calls CompareTo; books.Sort(New ByYearThenTitle()) uses the IComparer(Of Book) and leaves the type’s natural order untouched. Prefer a comparer when the order is context-specific.

  • HashSet(Of Book).Contains(New Book("SHELF DESIGN", 2021)) is True because Equals(Book) ignores case and GetHashCode hashes the lower-case title: the two methods agree. A hash code that included the title with its original case would make the lookup fail even though Equals returns True.

  • The Private IComparable.CompareTo(Object) throws ArgumentException for an object of another type, which is the contract of the non-generic interface. Private is correct for it, since it is not part of the type’s usual API.

  • CompareTo(Nothing) must return a positive number: any instance is greater than Nothing.

  • Max() and SortedSet(Of Book).Min use IComparable(Of Book). Without an IComparable implementation, Max() on a List(Of Plain) compiled and threw ArgumentException with the message "At least one object must implement IComparable." at run time, because the LINQ method has no constraint that the compiler could check.

IEnumerable(Of T)

Implement IEnumerable(Of T) to make a type usable in For Each and LINQ. In Visual Basic an Iterator function (see Iterators and Yield) writes the enumerator for you. The generic interface inherits the non-generic IEnumerable, so both GetEnumerator methods are needed: the generic one is the iterator, the non-generic one is a Private method that forwards to it.

Option Strict On

Imports System.Collections

Public Class Bookshelf
    Implements IEnumerable(Of String)

    Private ReadOnly _titles As New List(Of String)

    Public Sub Add(title As String)
        _titles.Add(title)
    End Sub

    ' An Iterator function implements the generic interface in a few lines.
    Public Iterator Function GetEnumerator() As IEnumerator(Of String) Implements IEnumerable(Of String).GetEnumerator
        For Each title In _titles
            Yield title
        Next
    End Function

    ' IEnumerable(Of T) inherits the non-generic IEnumerable, which must be implemented too.
    Private Function GetEnumeratorObject() As IEnumerator Implements IEnumerable.GetEnumerator
        Return GetEnumerator()
    End Function
End Class

Module Program
    Sub Main()
        Dim shelf As New Bookshelf()
        shelf.Add("Shelf Design")
        shelf.Add("Domain Modelling")
        shelf.Add("Async Cataloguing")

        For Each title In shelf
            Console.WriteLine(title)
        Next

        ' Every LINQ operator works because of the interface.
        Console.WriteLine(String.Join(" | ", From t In shelf Order By t Select t.ToUpperInvariant()))
        Console.WriteLine("count: " & shelf.Count())

        ' An old-style loop over the non-generic interface sees Object elements.
        Dim untyped As IEnumerable = shelf
        For Each item As Object In untyped
            Console.Write(item.GetType().Name & " ")
        Next
        Console.WriteLine()
    End Sub
End Module
Shelf Design
Domain Modelling
Async Cataloguing
ASYNC CATALOGUING | DOMAIN MODELLING | SHELF DESIGN
count: 3
String String String

For Each title In shelf needs only a public GetEnumerator method (the compiler uses the pattern, not the interface), but LINQ and the framework APIs ask for the interface. The non-generic loop in the last lines shows that the elements are still String objects, just typed as Object by the older interface. Implement the interface rather than wrapping a List(Of T) return type when the type should offer only enumeration; see also Collections.

IDisposable

IDisposable has one method, Dispose. A Using statement calls it when the block ends, even after an exception, for any variable whose type implements it. The pattern for a type that owns only managed resources is shown below: Dispose is idempotent, and members check the disposed state.

Option Strict On

Public Class ReportWriter
    Implements IDisposable

    Private _disposed As Boolean
    Private ReadOnly _name As String

    Public Sub New(name As String)
        _name = name
        Console.WriteLine("open " & _name)
    End Sub

    Public Sub Write(line As String)
        ObjectDisposedException.ThrowIf(_disposed, Me)
        Console.WriteLine(_name & ": " & line)
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        If _disposed Then Return
        _disposed = True
        Console.WriteLine("close " & _name)
        GC.SuppressFinalize(Me)
    End Sub
End Class

Module Program
    Sub Main()
        Dim kept As ReportWriter
        Using writer As New ReportWriter("loans.txt")
            writer.Write("3 loans overdue")
            kept = writer
        End Using

        kept.Dispose()                    ' calling Dispose again must be harmless
        Try
            kept.Write("too late")
        Catch ex As ObjectDisposedException
            Console.WriteLine(ex.GetType().Name & ": " & ex.ObjectName)
        End Try

        ' Using accepts any expression whose type implements IDisposable, also an interface type.
        Dim disposable As IDisposable = New ReportWriter("audit.txt")
        Using disposable
            Console.WriteLine("working through IDisposable")
        End Using
    End Sub
End Module
open loans.txt
loans.txt: 3 loans overdue
close loans.txt
ObjectDisposedException: ShelfDocs.ReportWriter
open audit.txt
working through IDisposable
close audit.txt

Using disposable (an existing variable of the interface type, without As) works too: the statement needs a reference whose static type implements IDisposable. The full pattern for finalizers and unmanaged resources, and IAsyncDisposable, are in Object Lifetime and Disposal. ObjectDisposedException.ThrowIf is the .NET 7 and later helper; the exception’s ObjectName shows the type name with the root namespace ShelfDocs.

INotifyPropertyChanged

INotifyPropertyChanged has a single event, PropertyChanged. The implementation declares the event with its delegate type and an Implements clause; the Event statement can name the delegate type (PropertyChangedEventHandler) instead of a parameter list. An observer attaches to the interface event with AddHandler, which also works when the class variable is not available. The CallerMemberName attribute (see Attributes, Caller Info and Reflection) supplies the property name, so a typo cannot slip into a string.

Option Strict On

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Public Class BookViewModel
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private _title As String = ""
    Private _isAvailable As Boolean = True

    Public Property Title As String
        Get
            Return _title
        End Get
        Set(value As String)
            SetField(_title, value)
        End Set
    End Property

    Public Property IsAvailable As Boolean
        Get
            Return _isAvailable
        End Get
        Set(value As Boolean)
            SetField(_isAvailable, value)
        End Set
    End Property

    ' CallerMemberName supplies "Title" or "IsAvailable" from the calling property.
    Private Sub SetField(Of T)(ByRef field As T, value As T, <CallerMemberName> Optional name As String = Nothing)
        If EqualityComparer(Of T).Default.Equals(field, value) Then Return
        field = value
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    End Sub
End Class

Module Program
    Sub Main()
        Dim vm As New BookViewModel()
        Dim observed As INotifyPropertyChanged = vm
        AddHandler observed.PropertyChanged,
            Sub(sender, e) Console.WriteLine("changed: " & e.PropertyName)

        vm.Title = "Shelf Design"
        vm.Title = "Shelf Design"      ' same value: no event
        vm.IsAvailable = False
        vm.Title = "Shelf Design, 2nd edition"
    End Sub
End Module
changed: Title
changed: IsAvailable
changed: Title

The second assignment of the same title raised no event because SetField compares the old and the new value with EqualityComparer(Of T).Default. A property declared Property Title As String with the auto-property syntax cannot be used for this: change notification needs a setter body. The RaiseEvent statement of an event that implements an interface event is the usual one, and PropertyChanged is Nothing-safe (an event without handlers does nothing). WPF and Windows Forms bindings are in WPF Applications and Windows Forms Applications.

Events and default properties in an interface

An interface can declare Event members and indexers. An indexer is a property marked Default, which makes the interface variable subscriptable with labels(1). Marking the implementing property Default as well makes the indexer work on LabelList variables, not only on interface variables. An event is implemented with an event of the same delegate type or signature and an Implements clause.

Option Strict On

Public Interface IShelfLabels
    Event Changed As EventHandler
    Default ReadOnly Property Item(index As Integer) As String
    ReadOnly Property Count As Integer
    Sub Add(label As String)
End Interface

Public Class LabelList
    Implements IShelfLabels

    Private ReadOnly _labels As New List(Of String)

    Public Event Changed As EventHandler Implements IShelfLabels.Changed

    Default Public ReadOnly Property Item(index As Integer) As String Implements IShelfLabels.Item
        Get
            Return _labels(index)
        End Get
    End Property

    Public ReadOnly Property Count As Integer Implements IShelfLabels.Count
        Get
            Return _labels.Count
        End Get
    End Property

    Public Sub Add(label As String) Implements IShelfLabels.Add
        _labels.Add(label)
        RaiseEvent Changed(Me, EventArgs.Empty)
    End Sub
End Class

Module Program
    Sub Main()
        Dim labels As IShelfLabels = New LabelList()
        AddHandler labels.Changed, Sub(sender, e) Console.WriteLine("labels changed")

        labels.Add("Fiction")
        labels.Add("Reference")
        ' The default property is visible through the interface variable.
        Console.WriteLine(labels(1) & " of " & labels.Count)

        ' WithEvents also works with an interface-typed field; see events-and-handlers.
    End Sub
End Module
labels changed
labels changed
Reference of 2

Source: Event Statement

A WithEvents field may have an interface type that declares events, and a Handles clause then names the field and the interface event (a test program with Private WithEvents _source As IChanged and Handles _source.Changed built and printed the handler’s message). Handles and custom events are covered in Events and Handlers, and default properties in Properties and Default Properties.

Interfaces from C#

C# has added two features to interfaces that Visual Basic only consumes: default interface members (an interface method with a body) and static abstract members (a contract on the type itself, the basis of generic math). Visual Basic’s strategy is to consume new .NET features, not to extend its own syntax (see History, Versions and Language Strategy). The results of the experiments are below; the library is a C# project with an interface that has a default method and a second interface with a static abstract member:

namespace CsShelf;

public interface IShelfLabel
{
    string Text { get; }

    // Default interface method: C# 8, runtime feature.
    string Describe() => "Label: " + Text;
}

public interface ILabelFactory
{
    // Static abstract member: C# 11.
    static abstract IShelfLabel Create(string text);
}

public sealed class SimpleLabel : IShelfLabel
{
    public SimpleLabel(string text) => Text = text;
    public string Text { get; }
}
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <RootNamespace>ShelfDocs</RootNamespace>
    <OptionStrict>On</OptionStrict>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <NuGetAudit>false</NuGetAudit>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\cs\cs.csproj" />
  </ItemGroup>
</Project>

The Visual Basic client of the default method:

Option Strict On

Imports CsShelf

Module Program
    Sub Main()
        ' SimpleLabel is written in C# and does not define Describe: the interface supplies it.
        Dim simple As New SimpleLabel("Maps")
        Dim label As IShelfLabel = simple
        Console.WriteLine(label.Text & " -> " & label.Describe())
        Console.WriteLine(DirectCast(simple, IShelfLabel).Describe())
    End Sub
End Module
/w/g3-interfaces-13/vb/Program.vb(7,16): error BC30149: Class 'PlainLabel' must implement 'Function Describe() As String' for interface 'IShelfLabel'. [/w/g3-interfaces-13/vb/vb.vbproj]
/w/g3-interfaces-13/vb/Program.vb(7,16): error BC37315: Class 'PlainLabel' cannot implement interface 'IShelfLabel' because it contains shared abstract or virtual 'Function Create(text As String) As IShelfLabel'. [/w/g3-interfaces-13/vb/vb.vbproj]

The build failed. Fix the build errors and run again.

The default method can be called from Visual Basic, through a variable of the interface type, on any object that implements the interface. What Visual Basic cannot do, on 17.13, is the following (each case was compiled):

Attempt Compiler message Meaning

simple.Describe() where simple is a SimpleLabel variable

BC30456: 'Describe' is not a member of 'SimpleLabel'.

The default method belongs to the interface, not to the class. Convert to the interface type first, as DirectCast(simple, IShelfLabel).Describe() does.

A Visual Basic class that implements IShelfLabel and does not define Describe

BC30149: Class 'PlainLabel' must implement 'Function Describe() As String' for interface 'IShelfLabel'.

For a type written in Visual Basic the default member counts as a required one: the class must write the member itself.

A Visual Basic class that implements an interface with a static abstract member

BC37315: Class 'Maker' cannot implement interface 'ILabelFactory' because it contains shared abstract or virtual 'Function Create(text As String) As IShelfLabel'.

Visual Basic cannot implement static abstract members, so it cannot implement a factory interface of this kind, nor a generic-math interface such as IAdditionOperators(Of Money, Money, Money) (BC37315 for a Structure Money that names it, once per static member).

A method body, or a Shared modifier, in a Visual Basic interface

BC30603: Statement cannot appear within an interface body. (the body); BC30270: 'Shared' is not valid on an interface method declaration.

Interfaces declared in Visual Basic cannot have implementations or static members.

The generic-math interfaces of the base library (INumber(Of T), IAdditionOperators(Of ...)) are the common case of the third row: a Visual Basic type cannot implement them. See Numbers and Math for the numeric types and Interop with C# and Other .NET Languages for what else crosses the language boundary.

Errors the compiler reports

All messages below are the compiler’s own text, printed by the .NET 10 SDK (10.0.402) for a complete program with Option Strict On that contained only that mistake. Learn documents most of these codes on pages of the form BC30270; BC30149, BC36757 and BC37315 had no page at the time of checking.

Code Message (shortened where it names your types) Cause

BC30149

Class 'MemoryStore' must implement 'Function Count() As Integer' for interface 'ICatalogStore'.

The class names the interface and does not implement every member.

BC30270

'Public' is not valid on an interface method declaration.

A modifier on an interface member: Public, Shared, Overridable and MustOverride all give this message. Delete it; the members are public.

BC30363

'Sub New' cannot be declared in an interface.

An interface has no constructors.

BC31444

'Property Name As String' cannot be implemented by a ReadOnly property.

A read/write interface property needs both accessors in the implementation.

BC30296

Interface 'IA' cannot inherit from itself:

A cycle in Inherits (the message continues with the cycle).

BC37315 (generic math)

Structure 'Money' cannot implement interface 'IAdditionOperators(Of Money, Money, Money)' because it contains shared abstract or virtual 'Function op_Addition(left As Money, right As Money)'.

A framework interface with static abstract members.

BC30401

'Find' cannot implement 'Find' because there is no matching function on interface 'ICatalogStore'.

The clause names an interface member that does not exist or whose signature differs (here Long instead of Integer, or a different Optional default). The compiler then also reports BC30149 for the member left unimplemented.

BC30401

'Remove' cannot implement 'Remove' because there is no matching sub on interface 'ICatalogStore'.

The member name after the dot is not in the interface.

BC30583

'ICatalogStore.Add' cannot be implemented more than once.

Two members of the type implement the same interface member.

BC30505

Methods or events that implement interface members cannot be declared 'Shared'.

Public Shared Function Today() ... Implements IClock.Today.

BC30231 and BC31083

'Implements' not valid in Modules. / Members in a Module cannot implement interface members.

A Module tried to implement an interface.

BC30602

Interface members must be methods, properties, events, or type definitions.

A field (Dim Name As String) or a constant in an interface.

BC30603

Statement cannot appear within an interface body.

A method body in an interface (the Console.WriteLine line).

BC31035

Interface 'IAuditable' is not implemented by this class.

An Implements clause on a member of a class whose body has no Implements IAuditable statement.

BC30683

'Inherits' statement must precede all declarations in a class.

Implements before Inherits: the base class goes first.

BC30390

'Report.Private Sub Audit()' is not accessible in this context because it is 'Private'.

Calling a Private implementation through the class; call it through the interface.

BC30521

Overload resolution failed because no accessible 'Name' is most specific for these arguments:

The member name is declared by two base interfaces of the derived interface that you call it on.

BC36757

'IRepository(Of Book)' cannot be converted to 'IRepository(Of CatalogItem)'. Consider changing the 'T' in the definition of 'Interface IRepository(Of T)' to an Out type parameter, 'Out T'.

A conversion that needs variance on an invariant interface.

BC30456

'Describe' is not a member of 'SimpleLabel'.

A C# default interface member called on the class instead of the interface.

BC37315

Class 'Maker' cannot implement interface 'ILabelFactory' because it contains shared abstract or virtual 'Function Create(text As String) As IShelfLabel'.

A C# static abstract member.

Source: BC30401, BC30583, BC30505

Two runtime exceptions are specific to interfaces. A DirectCast or CType to an interface that the object does not implement throws InvalidCastException, where TryCast returns Nothing. And a call through an interface variable that holds Nothing throws NullReferenceException, as for any object reference. A COM-exposed class that does not repeat its interface members is a separate matter (see COM Interop and Office Automation).

Designing with interfaces

  • Interface or base class? An interface is the right tool for a role that unrelated types play (IDisposable, ICatalogStore) or when a type needs several roles. A base class (MustInherit) is right when the types share state and implementation and form a hierarchy (CatalogItem and Book). The two combine well: an interface for clients, a MustInherit helper class that implements it for convenience, and implementations free to ignore the helper. See Inheritance and Polymorphism.

  • Small interfaces. ICatalogStore has three members. Clients that only read should depend on a smaller IReadOnlyCatalog, so a read-only fake or a cached view can implement it. This is the interface segregation principle; when a class implements an interface of ten members to supply one, split the interface.

  • Depend on the interface at the boundary. Constructor parameters, fields and return types name the interface (ICatalogStore, IReadOnlyList(Of CatalogItem)); the one place that says New InMemoryCatalogStore() is the composition root. The dependency injection container of Microsoft.Extensions.DependencyInjection registers AddSingleton(Of ICatalogStore, InMemoryCatalogStore)() and then resolves a Catalog (see Design Patterns in Visual Basic).

  • Test with fakes. Because Catalog takes an ICatalogStore, a unit test passes a small class or a library-generated mock; see Testing.

  • Do not change a published interface. Adding a member breaks every implementer outside your control. In C# a default interface member can soften that; Visual Basic cannot write one, so version a contract by adding a new interface that inherits the old one (ICatalogStore2 Inherits ICatalogStore) and test with TypeOf store Is ICatalogStore2.

  • Naming. I prefix, PascalCase, no Hungarian prefixes; an interface that names a capability uses an adjective (IComparable, IDisposable), a role uses a noun (ICatalogStore).

  • Trimming and Native AOT. Code that discovers implementers by reflection (Type.GetInterfaces, Assembly.GetTypes) depends on metadata that trimming may remove; code that names the interface and its implementers statically does not.

  • Not interfaces. A Delegate fits a contract of one method better (see Delegates and Lambda Expressions), and an Enum or a MustInherit class fits a closed set of alternatives.

Comparison with C#

Topic Visual Basic C#

Implementing a member

Implements IFoo.Bar clause on a member of any name and accessibility

matching by name and signature of a public member, or IFoo.Bar (explicit)

Hiding an implementation from the class

Private method with an Implements clause

explicit interface implementation

One method for several members

Implements IA.M, IB.M

one public method matches both interfaces by name

Interface members

Sub, Function, Property, Event, nested types

also static members, default bodies and static abstract (C# 8, 11)

Default interface members

can be called through the interface; types must still implement them

declared and used

static abstract members

cannot be implemented (BC37315)

declared and implemented

Interface inheritance

Inherits IA, IB in the body

interface IC : IA, IB

Variance

Out T, In T

out T, in T

Re-implementation

repeat Implements and the clause

repeat the interface in the base list

Access modifier on an interface member

not allowed (BC30270)

allowed in C# 8 and later for default and static members

The corresponding C# page is Interfaces in the C# reference.

Older material you may meet

  • Visual Basic 6 and VBA have Implements as well, with a convention rather than a clause: the implementing procedure must be called InterfaceName_MemberName (IShelfLabel_Text), the interface is another class module whose methods are empty, and a class can implement an interface but cannot inherit a class. In Visual Basic .NET the method can have any name and the clause says which member it implements. Ported code usually keeps the underscored names and gains an Implements clause. See The VB Family: VB6, VBA and VBScript.

  • Older .NET Framework samples use the non-generic IComparable, IEnumerable and ICollection with ArrayList and Object elements, and ICloneable for copies. Today, implement the generic interfaces (IComparable(Of T), IEnumerable(Of T)) and give the non-generic ones a Private forwarding implementation only when an old API needs it.

  • Older INotifyPropertyChanged samples raise the event with a string literal in every setter (RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Title"))). CallerMemberName removes the literals, and a renamed property can no longer leave a stale name behind.

  • The "interface or abstract class" advice in older material predates default interface members. Visual Basic still cannot declare them, but it can consume them (see Interfaces from C#), so a library written in C# can add a member to an interface without breaking Visual Basic callers, while a Visual Basic class that implements it must still write the member.

  • The interface rules are the same on .NET Framework 4.x and on .NET 10 for Visual Basic. Default interface members are a runtime feature of .NET Core 3.0 and later, so a .NET Framework application cannot run a library that uses them.