Extension Methods and Partial Methods
|
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 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 one Windows Forms example is built but not run, because it needs a Windows desktop. The
scratch projects use a root namespace (ShelfDocs or Shelf), which matters in two places that the text points out. The
project-level imports that the SDK adds (System, System.Collections.Generic, System.Linq and others) are why Console,
List(Of T) and the LINQ methods need no Imports line.
Two features share this page because both change how a type is assembled from pieces that live somewhere else. An extension method adds a method to a type you do not own, from a different module and often a different assembly. A partial declaration lets one type be written in several source files, and a partial method is a hook that one part declares and another part may or may not implement. Together they are the reason generated code and hand-written code can coexist: the generator owns one file, you own another, and neither edits the other.
| Feature | What it is | Where it is declared |
|---|---|---|
Extension method |
a |
a |
Partial type |
one |
each part says |
Partial method |
a |
declaration without body, implementation with body, same type |
Designer file |
the generated part of a form, a control or a |
a |
Both features are older than the baseline: partial types arrived with Visual Basic 2005 (VB 8), extension methods and partial methods with Visual Basic 2008 (VB 9) together with LINQ. Nothing was added to them in the releases up to Visual Basic 17.13, because the language is in maintenance mode (see History, Versions and Language Strategy). The C# features that have grown in the meantime (extension members in C# 14, extended partial methods in C# 9) are not available to define from Visual Basic; what can be consumed is described below.
Extension methods
An extension method is declared once, in a module, and then called as if it were a member of the extended type. The compiler
rewrites text.Shorten(8) into an ordinary call of the shared function ShelfExtensions.Shorten(text, 8), so nothing about the
extended type changes: no inheritance, no wrapper object, no change to the assembly that declares the type. That is why you
can extend String, IEnumerable(Of T) or a type from a NuGet package, and also why an extension method can never see
private members of the type it extends.
The rules, all enforced by the compiler:
-
The declaring type must be a
Module. AClassorStructurecannot hold extension methods (BC36551); this is the main difference from C#, where the container is astatic class. -
The method must carry the
<Extension>attribute fromSystem.Runtime.CompilerServices, so that namespace is imported in the declaring file (the attribute can be written in full instead). Marking the module as well is optional. The attribute can be applied only to aModule,SuborFunction(BC36550);PropertyandEventcannot be extension members (BC30662 for a property). -
The first parameter is the receiver, the type being extended. A method with no parameters (BC36552), with an
Optionalfirst parameter (BC36553) or aParamArrayfirst parameter (BC36554) is rejected. -
The remaining parameters follow the usual rules for procedures, including
OptionalandParamArrayparameters, named arguments,ByRefand generic type parameters (see Procedures: Sub and Function). -
The extension is a normal
Public,FriendorPrivatemethod of its module. AFriendextension is visible only inside its assembly, aPrivateone only inside its module. -
The call is a static call:
Nothingas the receiver does not throw at the call itself, so a null check in the extension works as in the first example.
Option Strict On
Imports System.Runtime.CompilerServices
Public Class Book
Public Property Title As String
Public Property Year As Integer
Public Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
Public Overrides Function ToString() As String
Return Title & " (" & Year & ")"
End Function
End Class
Public Module ShelfExtensions
' The first parameter is the receiver: it is the type that gets the "new" method
<Extension>
Public Function Shorten(text As String, maxLength As Integer) As String
If text Is Nothing Then Return ""
If text.Length <= maxLength Then Return text
Return text.Substring(0, maxLength - 1) & "~"
End Function
' An extension on a sequence of books; optional parameters work as in any Function
<Extension>
Public Function PublishedSince(books As IEnumerable(Of Book), year As Integer,
Optional inclusive As Boolean = True) As IEnumerable(Of Book)
Return books.Where(Function(b) If(inclusive, b.Year >= year, b.Year > year))
End Function
' A generic extension: the receiver type is inferred from the call
<Extension>
Public Function SecondOrDefault(Of T)(source As IEnumerable(Of T)) As T
Return source.Skip(1).FirstOrDefault()
End Function
End Module
Module Program
Sub Main()
Dim books As New List(Of Book) From {
New Book("The Pragmatic Programmer", 1999),
New Book("Domain-Driven Design", 2003),
New Book("Refactoring", 2018)
}
' Instance-call syntax
Console.WriteLine("Dummy title".Shorten(8))
' The receiver may be Nothing: no NullReferenceException at the call itself
Dim missing As String = Nothing
Console.WriteLine("[" & missing.Shorten(8) & "]")
' The same method called the ordinary way: it is only a Function in a Module
Console.WriteLine(ShelfExtensions.Shorten("Refactoring: Improving", 12))
For Each b In books.PublishedSince(2003)
Console.WriteLine(b)
Next
Console.WriteLine(books.PublishedSince(2003, inclusive:=False).Count())
Console.WriteLine(books.SecondOrDefault())
Console.WriteLine(New List(Of Integer) From {7}.SecondOrDefault())
End Sub
End Module
Dummy t~
[]
Refactoring~
Domain-Driven Design (2003)
Refactoring (2018)
1
Domain-Driven Design (2003)
0
Source: Extension Methods (Visual Basic)
In that example missing.Shorten(8) works because the call is static. The same expression with an instance method, such as
missing.ToUpper(), throws NullReferenceException. That asymmetry is a feature when the extension is designed for it (a
null-tolerant Shorten) and a trap when readers assume every dotted call dereferences the receiver. Document a null-tolerant
extension as such, and do not rely on it for general code.
Which receivers are possible
The receiver parameter can be any type: a class, an interface, a structure, an enumeration, a delegate type, an array, a
constructed generic type (IEnumerable(Of Book)) or an open generic type parameter. The argument is converted to the
parameter type by a widening conversion, which is why an extension on Object accepts a boxed Integer and an extension on
an interface applies to every implementing type. A ByRef receiver is allowed for value types and reference types alike, and
then the call changes the caller’s variable, not a copy.
Option Strict On
Imports System.Runtime.CompilerServices
Public Interface IShelved
ReadOnly Property Title As String
End Interface
Public Class Mag
Implements IShelved
Public ReadOnly Property Title As String Implements IShelved.Title
Get
Return "mag"
End Get
End Property
End Class
Public Structure Counter
Public Value As Integer
End Structure
Public Module Ext
<Extension>
Public Function Echo(o As Object) As String
Return "obj:" & o.ToString()
End Function
<Extension>
Public Sub Reset(ByRef x As Integer)
x = 0
End Sub
<Extension>
Public Sub Bump(ByRef c As Counter)
c.Value += 1
End Sub
<Extension>
Public Sub BumpByVal(c As Counter)
c.Value += 1
End Sub
<Extension>
Public Sub Replace(ByRef s As String)
s = "replaced"
End Sub
<Extension>
Public Function Loud(x As IShelved) As String
Return x.Title.ToUpperInvariant()
End Function
<Extension>
Public Function Times(n As Integer) As String
Return "times" & n
End Function
<Extension>
Public Function Biggest(Of T As IComparable(Of T))(items As IEnumerable(Of T)) As T
Return items.Max()
End Function
<Extension>
Public Function Twice(f As Func(Of Integer, Integer), x As Integer) As Integer
Return f(f(x))
End Function
End Module
Module Program
Sub Main()
' A value type is boxed to reach an extension on Object
Console.WriteLine(5.Echo())
' An extension on an interface works for every implementing type
Console.WriteLine(New Mag().Loud())
' ByRef receiver: the caller's variable is changed
Dim n = 7
n.Reset()
Console.WriteLine("after Reset: " & n)
' A ByVal receiver of a Structure is a copy; a ByRef one is the variable
Dim c As New Counter
c.Bump()
c.BumpByVal()
Console.WriteLine("counter: " & c.Value)
Dim s As String = "x"
s.Replace()
Console.WriteLine("after Replace: " & s)
' Literals can be receivers
Console.WriteLine(3.Times())
' Generic extension with a constraint
Console.WriteLine(New List(Of Integer) From {3, 9, 2}.Biggest())
' Delegates can be extended too
Dim f As Func(Of Integer, Integer) = Function(v) v * 3
Console.WriteLine(f.Twice(2))
End Sub
End Module
obj:5
MAG
after Reset: 0
counter: 1
after Replace: replaced
times3
9
18
What the example shows, receiver by receiver:
| Receiver | Behaviour |
|---|---|
|
|
Interface |
|
|
|
|
|
Literal |
|
Delegate |
|
Generic with constraint |
|
A ByRef receiver is legal but rare. Callers cannot tell from n.Reset() that n changes. When the receiver is a property, as in
holder.Count.Reset(), Visual Basic passes a temporary copy and writes it back afterwards, so both the getter and the setter run
(checked with a scratch program: the property became 0). Prefer a function that returns the new value.
How the compiler resolves a call
When the compiler meets b.Label(">> ") it first looks for an ordinary member of the receiver’s type with that name. Only if
no instance method is applicable to the arguments does it look at extension methods, scope by scope, from the innermost
outwards, and the first scope that holds an applicable candidate decides. Within one scope the usual overload resolution picks
the best match, and a tie is an error.
named Name applicable
to the arguments?"} inst -- yes --> useinst["Call the instance member:
an extension is never considered"] inst -- no --> scope["Take the next scope outwards:
the namespace of the call site,
its enclosing namespaces,
then file and project imports"] scope --> cand{"Does the scope contain extension methods
named Name with a receiver
that the argument converts to?"} cand -- no --> more{"Any scope left?"} more -- yes --> scope more -- no --> err["Compile error:
BC30456 not a member,
BC30057 too many arguments,
or late binding BC30574"] cand -- yes --> best{"Is there one best
overload by the usual rules?"} best -- yes --> useext["Call that extension method"] best -- no --> amb["BC30521 ambiguous"]
The first example below puts an instance Describe() and two extension Describe methods side by side. The instance method
wins when both apply; the extension with a different parameter list is chosen when the instance method is not applicable.
Option Strict On
Imports System.Runtime.CompilerServices
Imports Shelf.Plain
Public Class Book
Public Property Title As String
Public Sub New(title As String)
Me.Title = title
End Sub
' An instance method that applies to Describe() wins over any extension method
Public Function Describe() As String
Return "instance: " & Title
End Function
Public Function Label(prefix As String) As String
Return prefix & Title
End Function
End Class
Namespace Plain
Public Module PlainExtensions
<Extension>
Public Function Describe(book As Book) As String
Return "plain extension: " & book.Title
End Function
<Extension>
Public Function Describe(book As Book, width As Integer) As String
Return "plain extension (" & width & "): " & book.Title
End Function
' Different arity from the instance Label(String): applicable only with two arguments
<Extension>
Public Function Label(book As Book, prefix As String, suffix As String) As String
Return prefix & book.Title & suffix
End Function
<Extension>
Public Function Summary(book As Book) As String
Return "plain summary of " & book.Title
End Function
End Module
End Namespace
Namespace Fancy
Public Module FancyExtensions
<Extension>
Public Function Summary(book As Book) As String
Return "FANCY summary of " & book.Title
End Function
End Module
End Namespace
Namespace App
Module Program
Sub Main()
Dim b As New Book("Refactoring")
Console.WriteLine(b.Describe())
Console.WriteLine(b.Describe(40))
Console.WriteLine(b.Label(">> "))
Console.WriteLine(b.Label(">> ", " <<"))
Console.WriteLine(b.Summary())
End Sub
End Module
End Namespace
instance: Refactoring
plain extension (40): Refactoring
>> Refactoring
>> Refactoring <<
plain summary of Refactoring
Two consequences follow, and the first one causes real bugs:
-
An extension method can never replace an existing instance method with the same signature. If the extended type gains an instance method that matches (for example in a new .NET release), your extension silently stops being called and the new member runs instead. The code still compiles, so the change shows up as different behaviour. Give extension methods names that say what is specific to your application (
ToShelfLabel, notToString), and cover them with tests. -
Label(">> ", " <<")has an instance method calledLabeltoo, but with one parameter. Because that instance method is not applicable to two arguments, the extension is used. Overloading across an instance and an extension method is allowed, but it makes readers search two places.
Scope: Imports, namespaces and the project
An extension method is visible only where its module is in scope. A Module is in scope when the call site is inside the same
namespace or a nested one, when the file has an Imports statement for the namespace that holds the module, or when the project
imports the namespace for all files. Importing the module itself also works (Imports Shelf.Plain.PlainExtensions), and this is
the way to bring in one module without the rest of its namespace.
<ItemGroup>
<Import Include="Shelf.Plain" />
</ItemGroup>
The SDK uses exactly this mechanism to add System.Linq to every project, which is how Where and Select appear without an
Imports line. Imports statements must be the first declarations of a file, before the first Namespace or type
(BC30465 otherwise), so there is no way to import a namespace for only one namespace block of a file.
Scope also decides priority: an extension in the namespace of the call site beats one that arrived through Imports, even
when both would be applicable. The example puts Origin in two places.
Option Strict On
Imports System.Runtime.CompilerServices
Imports Shelf.Imported
Public Class Book
Public Property Title As String = "Refactoring"
End Class
Namespace Imported
Public Module ImportedExtensions
<Extension>
Public Function Where(b As Book) As String
Return "imported"
End Function
<Extension>
Public Function Origin(b As Book) As String
Return "imported (via Imports)"
End Function
End Module
End Namespace
Namespace App
Public Module NearExtensions
<Extension>
Public Function Origin(b As Book) As String
Return "near (same namespace as the caller)"
End Function
End Module
Module Program
Sub Main()
Dim b As New Book()
Console.WriteLine(b.Origin())
Console.WriteLine(b.Where())
End Sub
End Module
End Namespace
near (same namespace as the caller)
imported
b.Where() finds the only candidate in the imported module. Note that an Imports for the namespace changes which method a
call binds to, without any change to the call itself. A new Imports line, or a new module added to a namespace that was already
imported, can therefore change the meaning of existing code or make a call ambiguous. When two imported namespaces both hold an
applicable extension, the compiler cannot choose:
Option Strict On
Imports System.Runtime.CompilerServices
Imports Shelf.Plain
Imports Shelf.Fancy
Public Class Book
Public Property Title As String
End Class
Namespace Plain
Public Module PlainExtensions
<Extension>
Public Function Summary(b As Book) As String
Return "plain"
End Function
End Module
End Namespace
Namespace Fancy
Public Module FancyExtensions
<Extension>
Public Function Summary(b As Book) As String
Return "fancy"
End Function
End Module
End Namespace
Module Program
Sub Main()
Dim b As New Book()
Console.WriteLine(b.Summary())
Dim o As Object = b
Console.WriteLine(o.Summary())
End Sub
End Module
Program.vb(32,29): error BC30521: Overload resolution failed because no accessible 'Summary' is most specific for these arguments:
Program.vb(35,27): error BC30574: Option Strict On disallows late binding.
Source: Extension Methods (Visual Basic)
BC30521 is fixed by removing one Imports, by importing only the module you need, or by calling the method in its ordinary
form (PlainExtensions.Summary(b)), which names the module explicitly. The second message shows the late-binding limit: an
Object variable is late bound under Option Strict Off, and the compiler does not look for extension methods at run time.
Under Option Strict On the call is a compile error (BC30574). With Option Strict Off the same code compiles and fails when it
runs; the scratch program printed MissingMemberException: Public member 'Summary' on type 'Book' not found. for the call on
the Object variable, while the call through a variable typed Book worked. Extension methods are therefore a compile-time
feature of statically typed code; see
Early and Late Binding.
Declaration errors
The declaration rules are enforced one by one. This program contains one violation per rule and is compiled with the SDK:
Option Strict On
Imports System.Runtime.CompilerServices
Public Class Book
Public Property Title As String
End Class
' Extension methods can be defined only in modules
Public Class NotAModule
<Extension>
Public Shared Function Bad1(b As Book) As String
Return b.Title
End Function
End Class
' The attribute itself is valid only on a Module, Sub or Function
<Extension>
Public Class Tagged
End Class
Public Module Wrong
' The extended type is the first parameter, so there must be one
<Extension>
Public Function Bad2() As String
Return ""
End Function
' The first parameter cannot be Optional
<Extension>
Public Function Bad3(Optional b As Book = Nothing) As String
Return ""
End Function
' The first parameter cannot be ParamArray
<Extension>
Public Function Bad4(ParamArray b() As Book) As String
Return ""
End Function
' There are no extension properties
<Extension>
Public Property Bad5(b As Book) As String
Get
Return ""
End Get
Set(value As String)
End Set
End Property
End Module
Module Program
Sub Main()
End Sub
End Module
Program.vb(11,6): error BC36551: Extension methods can be defined only in modules.
Program.vb(19,14): error BC36550: 'Extension' attribute can be applied only to 'Module', 'Sub', or 'Function' declarations.
Program.vb(25,21): error BC36552: Extension methods must declare at least one parameter. The first parameter specifies which type to extend.
Program.vb(31,35): error BC36553: 'Optional' cannot be applied to the first parameter of an extension method. The first parameter specifies which type to extend.
Program.vb(37,37): error BC36554: 'ParamArray' cannot be applied to the first parameter of an extension method. The first parameter specifies which type to extend.
Program.vb(42,6): error BC30662: Attribute 'ExtensionAttribute' cannot be applied to 'Bad5' because the attribute is not valid on this declaration type.
Source: Extension Methods (Visual Basic)
LINQ operators are extension methods
Where, Select, OrderBy and the other query operators are extension methods on IEnumerable(Of T) declared in the static
class System.Linq.Enumerable. This is also what the Aggregate, From, Where and Select clauses of a query expression
translate into (see LINQ and Query Expressions). Two
practical results: removing the System.Linq import takes the methods away, and Enumerable.Where(books, predicate) is a
valid call at any time, because every extension is also an ordinary shared method of its module. The shelf’s own query helpers
should follow the same pattern: take IEnumerable(Of T), return IEnumerable(Of T) and keep the deferred execution that the
callers expect (see Iterators and Yield).
Using extension methods from other .NET languages
A Visual Basic extension method is compiled to a shared method with ExtensionAttribute on the method and on its assembly, so
it is an ordinary extension method for any .NET language. The reverse is also true: extension methods written in C# are
extension methods in Visual Basic, and no Imports of the attribute is needed to consume them. The next two projects were built
and run: a Visual Basic library used from C#, and a C# library, with one classic extension method and one C# 14 extension
block, used from Visual Basic.
Option Strict On
Imports System.Runtime.CompilerServices
Public Module VbExtensions
<Extension>
Public Function Shout(text As String) As String
Return text.ToUpperInvariant() & "!"
End Function
End Module
using System;
using VbLib;
Console.WriteLine("dune".Shout());
Console.WriteLine(VbExtensions.Shout("direct"));
DUNE!
DIRECT!
namespace CsLib;
public static class CsExtensions
{
// Classic extension method
public static string Whisper(this string text) => text.ToLowerInvariant() + "...";
// C# 14 extension block with an extension property
extension(string text)
{
public bool IsBlank => string.IsNullOrWhiteSpace(text);
public string Exclaim() => text + "?";
}
}
Option Strict On
Imports CsLib
Module Program
Sub Main()
Console.WriteLine("Dune".Whisper())
Console.WriteLine("Dune".Exclaim())
End Sub
End Module
dune...
Dune?
Source: What’s new in C# 14
Exclaim(), declared in a C# 14 extension(string text) block, is callable from Visual Basic with instance syntax, because the C#
compiler also emits it as a classic extension method. IsBlank, declared in the same block as an extension property, is
not: "Dune".IsBlank fails with BC30456: 'IsBlank' is not a member of 'String' (tested on SDK 10.0.402), because Visual Basic has
no notion of an extension property. Visual Basic can neither declare extension properties nor extension blocks, and does not
see C# 14 extension members other than methods. Treat C# extension properties as a reason to wrap them in a method when a shared
library has to serve Visual Basic callers too. The C# side is described on
Extension Members and the general interop rules are on
Interop with C# and Other .NET Languages.
Differences from C#
| Topic | Visual Basic | C# |
|---|---|---|
Container |
a |
a non-generic, non-nested |
Marker |
|
|
Receiver |
any parameter type; |
any type; |
Properties |
not supported |
extension properties through C# 14 |
Resolution |
instance method first; then scope by scope from the call site outwards; |
instance method first; then |
|
call compiles for typed expressions |
call compiles for typed expressions |
Late binding |
extension methods are not found on |
extension methods are not found on |
Static call syntax |
|
|
Design guidance
-
Put extensions in a module that is named for what it extends or the feature it serves (
BookExtensions,CatalogQueryExtensions) and in a namespace that the callers import on purpose. -
Prefer a real method when you own the type, because an extension is invisible to reflection on the type and to interface contracts, and cannot be overridden.
-
Do not extend
Objector a very common type such asStringwith names that are likely to collide with a later instance method of the .NET type or with other libraries. -
Keep extensions free of hidden state; because they are shared methods, the only state they can use is their arguments and the module’s own shared members.
-
Do not use an extension method to hide a conversion that
Option Strictwould otherwise report; see Type Conversions. -
For a collection of related helpers that need no receiver, a plain
Modulewith ordinary functions is clearer (see Modules and Shared Members).
Partial types
Partial splits a declaration across any number of places in the same assembly and the same namespace. The compiler treats the
type as the union of the parts: members from every part exist together, Inherits and Implements clauses are merged,
attributes are combined, and every modifier that appears on one part applies to the whole type. The keyword exists for tools:
the Windows Forms designer, the WPF markup compiler and the My namespace use it to keep generated code out of the file you edit.
The rules, with the compiler messages for the violations:
| Rule | Violation | Error |
|---|---|---|
|
- |
- |
At most one part may omit |
two parts without |
BC30179 |
Parts that state an access level agree on it |
|
BC30925 |
Only one base class is possible; repeating the same |
|
BC30928 |
Modifiers such as |
- |
- |
|
- |
- |
All parts are in one assembly and one namespace; a part cannot be added from another project |
- |
- |
Option Strict On
Public Class Animal
End Class
Partial Public MustInherit Class Shape
Inherits Animal
Public MustOverride Function Area() As Double
End Class
Partial Public Class Shape
Inherits Animal
Implements IComparable(Of Shape)
Public Function CompareTo(other As Shape) As Integer Implements IComparable(Of Shape).CompareTo
Return Area().CompareTo(other.Area())
End Function
End Class
Public Class Square
Inherits Shape
Public Side As Double = 3
Public Overrides Function Area() As Double
Return Side * Side
End Function
End Class
Module Program
Sub Main()
Dim a As New Square()
Dim b As New Square() With {.Side = 4}
Console.WriteLine(a.CompareTo(b))
Console.WriteLine(GetType(Shape).IsAbstract)
Console.WriteLine(GetType(Shape).GetInterfaces().Length)
' Dim s As New Shape() ' BC30112 / BC30610: MustInherit
End Sub
End Module
-1
True
1
Source: Partial (Visual Basic)
Shape is declared MustInherit in the first part only, yet GetType(Shape).IsAbstract is True. Both parts repeat
Inherits Animal, which is allowed because they agree, and the Implements clause in the second part makes
IComparable(Of Shape) the type’s one interface (GetInterfaces().Length is 1). The usual use is much less clever: a tool owns
one file, you own the other.
A structure, an interface and a module can be partial too. This is a complete program that splits all three, plus a class that uses the next section’s partial methods:
Option Strict On
Public Interface IShelved
Property Title As String
End Interface
Partial Public Interface IShelved
Function Describe() As String
End Interface
Partial Public Structure Slot
Implements IShelved
Public Property Title As String Implements IShelved.Title
End Structure
Partial Public Structure Slot
Public Function Describe() As String Implements IShelved.Describe
Return "slot holding " & Title
End Function
End Structure
Partial Public Module Tools
Public Function Twice(value As Integer) As Integer
Return value * 2
End Function
End Module
Partial Public Module Tools
Public Function Triple(value As Integer) As Integer
Return value * 3
End Function
End Module
Partial Public Class Audit
Private Partial Sub Record(message As String)
End Sub
Private Partial Sub RecordNeverImplemented(message As String)
End Sub
Private Shared Function Expensive(label As String) As String
Console.WriteLine(" evaluating " & label)
Return label
End Function
Public Sub Run()
Console.WriteLine("calling Record")
Record(Expensive("first argument"))
Console.WriteLine("calling RecordNeverImplemented")
RecordNeverImplemented(Expensive("second argument"))
End Sub
End Class
Partial Public Class Audit
Private Sub Record(message As String)
Console.WriteLine(" recorded: " & message)
End Sub
End Class
Module Program
Sub Main()
Dim s As New Slot With {.Title = "Refactoring"}
Console.WriteLine(s.Describe())
Console.WriteLine(Tools.Twice(4) & " " & Tools.Triple(4))
Dim a As New Audit()
a.Run()
End Sub
End Module
slot holding Refactoring
8 12
calling Record
evaluating first argument
recorded: first argument
calling RecordNeverImplemented
Source: Partial (Visual Basic)
The partial violations (two parts without Partial, different base classes, different accessibility) are compiler errors, as the
partial-method rules below also are; the compiler printed all of them for one deliberately broken file:
Option Strict On
Public Class Animal
End Class
Public Class Plant
End Class
' Both parts omit Partial
Public Class Duplicate
End Class
Public Class Duplicate
End Class
' Two different base classes
Partial Public Class Mixed
Inherits Animal
End Class
Partial Public Class Mixed
Inherits Plant
End Class
' Two different access levels
Partial Public Class Visible
End Class
Partial Friend Class Visible
End Class
Partial Public Class Hooks
' A partial method must be a Sub
Private Partial Function Compute() As Integer
End Function
' It must be Private
Public Partial Sub Open()
End Sub
' The declaring part must have an empty body
Private Partial Sub Close()
Console.WriteLine("closing")
End Sub
Private Partial Sub Rename(first As Integer)
End Sub
Private Partial Sub Reset(Optional count As Integer = 1)
End Sub
Private Partial Sub Save()
End Sub
End Class
Partial Public Class Hooks
' The parameter name differs from the declaration
Private Sub Rename(second As Integer)
End Sub
' The default value differs from the declaration
Private Sub Reset(Optional count As Integer = 2)
End Sub
' Only one implementation is allowed
Private Sub Save()
End Sub
Private Sub Save()
End Sub
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(13,14): error BC30179: class 'Duplicate' and class 'Duplicate' conflict in namespace 'Shelf'.
Program.vb(22,14): error BC30928: Base class 'Plant' specified for class 'Mixed' cannot be different from the base class 'Animal' of one of its other partial types.
Program.vb(29,22): error BC30925: Specified access 'Friend' for 'Visible' does not match the access 'Public' specified on one of its other partial types.
Program.vb(34,30): error BC31437: 'Compute' cannot be declared 'Partial' because partial methods must be Subs.
Program.vb(38,5): error BC31431: Partial methods must be declared 'Private' instead of 'Public'.
Program.vb(42,25): error BC31435: Partial methods must have empty method bodies.
Program.vb(58,24): error BC31442: Parameter name 'second' does not match the name of the corresponding parameter, 'first', defined on the partial method declaration 'Rename'.
Program.vb(62,17): error BC37203: Optional parameter of a method 'Private Sub Reset([count As Integer = 2])' does not have the same default value as the corresponding parameter of the partial method 'Private Sub Reset([count As Integer = 1])'.
Program.vb(69,17): error BC31434: Method 'Save' cannot implement partial method 'Save' because 'Save' already implements it. Only one method can implement a partial method.
Source: Partial Methods (Visual Basic)
Things that are per file, not per type
-
Option Strict,Option Explicit,Option InferandOption Comparestatements apply to one source file, so the parts of a type can disagree. A designer file generated withoutOption Strict Oncan sit next to a hand-written part that has it, and the project-level setting decides for a file that has noOptionline (see Option Statements). Put the sameOptionlines in every file you write. -
Importsstatements apply to one file as well; a second part does not see the first part’s imports. -
Inside a module, a partial type declaration disables type promotion (the rule that makes members of a nested type visible from the enclosing module’s namespace), which can produce surprising name resolution; keep nested partial types out of modules.
Partial modules and the My namespace
My is built with partial types. The My.MyApplication, My.MyComputer and My.MySettings classes are partial, which is why a
project can add its own file with Namespace My and Partial Friend Class MyApplication to hook application events such as
Startup and UnhandledException. A partial Module is the right way to share one logical module between a generated file and a
hand-written one. See The My Namespace.
Partial methods
A partial method is a method whose declaration and implementation live in different parts of a partial type. The declaration
(Partial Private Sub OnTitleChanged(), or Private Partial Sub; the two modifiers can come in either order) has no body and is
called from the generated code like any Sub. The implementation, in another part, has a body and no Partial. If no part
implements it, the compiler removes every call, including the evaluation of the arguments. A partial method is therefore a
lightweight notification hook with no cost when it is unused.
The rules:
-
Subonly, never aFunction(BC31437). The declaration is the whole of the signature, so there is no return value to lose. -
Privateonly (BC31431), so a partial method is an implementation detail of the type; it cannot be called from outside and cannot beOverridable.Sharedand generic partial methods are allowed. -
The declaring part has an empty body (BC31435); the implementing part is the one with statements.
-
At most one implementation (BC31434), and the implementing method must repeat the parameter names of the declaration (BC31442) and the default values of
Optionalparameters (BC37203). -
No implementation is required: the call is silently removed when none exists, which is why a misspelled implementation name produces no warning; the hook is simply never run. Check names against the declaration, and consider a unit test that proves the hook runs.
The following project has three files, as a generator would produce. Book.Generated.vb declares the properties and the hooks
and calls them from the setters, Book.vb implements two of the three, and Program.vb uses the type.
Generated part:
Option Strict On
' Pretend this file is produced by a tool: it declares the hooks and never changes by hand
Partial Public Class Book
Private _title As String = ""
Private _year As Integer
Public Property Title As String
Get
Return _title
End Get
Set(value As String)
OnTitleChanging(value)
_title = value
OnTitleChanged()
End Set
End Property
Public Property Year As Integer
Get
Return _year
End Get
Set(value As Integer)
OnYearChanged(value)
_year = value
End Set
End Property
' Partial method declarations: Sub only, Private, no body, no Handles or Implements
Private Partial Sub OnTitleChanging(newValue As String)
End Sub
Private Partial Sub OnTitleChanged()
End Sub
Private Partial Sub OnYearChanged(newValue As Integer)
End Sub
End Class
Hand-written part:
Option Strict On
' The hand-written half. Partial is optional here because the other file already says it
Public Class Book
Public ReadOnly Property ChangeLog As New List(Of String)
' Implementations of two of the three hooks; OnYearChanged has none, so its call vanishes
Private Sub OnTitleChanging(newValue As String)
If String.IsNullOrWhiteSpace(newValue) Then
Throw New ArgumentException("A title is required.", NameOf(newValue))
End If
ChangeLog.Add("changing to " & newValue)
End Sub
Private Sub OnTitleChanged()
ChangeLog.Add("changed")
End Sub
End Class
Program:
Option Strict On
Module Program
Sub Main()
Dim b As New Book()
b.Title = "Refactoring"
b.Year = 2018
b.Title = "Refactoring, 2nd edition"
Try
b.Title = " "
Catch ex As ArgumentException
Console.WriteLine("rejected: " & ex.ParamName)
End Try
For Each line In b.ChangeLog
Console.WriteLine(line)
Next
Console.WriteLine(b.Title & " " & b.Year)
End Sub
End Module
rejected: newValue
changing to Refactoring
changed
changing to Refactoring, 2nd edition
changed
Refactoring, 2nd edition 2018
Source: Partial Methods (Visual Basic)
OnTitleChanging ran before the assignment and rejected the blank title by throwing, and that stopped the setter, so the
ChangeLog has no entry for it and the title keeps its earlier value. OnYearChanged has no implementation, so the call in the
Year setter generated no code and nothing was logged. The exception lives in the hook and is the author of the type’s
responsibility; a hook that throws is part of the type’s contract, so document it.
The Audit class in the program of the previous section makes the two behaviours of an unimplemented partial method visible.
Record has an implementation, RecordNeverImplemented does not, and both calls pass an argument produced by a method with a
side effect. evaluating second argument is missing from that output, because the compiler removed the whole call statement.
The consequence is that an argument with a side effect must not be placed in a call to a partial method, unless the side effect
is allowed to be optional: whether it happens depends on a file in another part. The calls and the output of that program also
show the order: the argument is evaluated first, then the body runs.
A smaller program shows the remaining modifiers, a generic and Shared partial method, an optional parameter and the
Partial Private order of the keywords:
Option Strict On
Partial Public Class Hooks
' The declaration may list Partial before or after Private; Shared and generic are allowed
Partial Private Shared Sub Tick(Of T)(value As T)
End Sub
' Optional parameters need the same default value in both parts
Private Partial Sub Opt(Optional amount As Integer = 1)
End Sub
' No implementation anywhere: the call below is removed by the compiler
Private Partial Sub Missing(message As String)
End Sub
Public Sub Go()
Tick(Of Integer)(5)
Opt()
Missing("never evaluated")
Console.WriteLine("done")
End Sub
End Class
Partial Public Class Hooks
Private Shared Sub Tick(Of T)(value As T)
Console.WriteLine("tick " & value.ToString())
End Sub
' Parameter names and optional defaults must repeat the declaration
Private Sub Opt(Optional amount As Integer = 1)
Console.WriteLine("opt " & amount)
End Sub
End Class
Module Program
Sub Main()
Dim h As New Hooks()
h.Go()
End Sub
End Module
tick 5
opt 1
done
Source: Sub Statement
Extended partial methods in C#, and generators
C# 9 extended partial methods: a C# partial method may have an access modifier, a return value and out parameters, and then
an implementation is required. Source generators use this for APIs such as GeneratedRegex and LibraryImport. None of that exists in
Visual Basic: a Visual Basic partial method is still the old kind, Private, Sub and optional. A partial Function is
rejected with BC31437 (see the table above), so the C# patterns that rely on a generated partial Function or on Public
partial methods cannot be written in Visual Basic. Use the non-generated equivalents (New Regex(...) with
RegexOptions.Compiled, Declare or <DllImport>; see
Native Interop: Declare and P/Invoke). Visual Basic
projects can still run analyzers and source generators written in C#, which add files to the compilation, but the generated code
must be usable from Visual Basic.
Designer-generated code
Windows Forms projects use partial classes in every form. For a form called SearchForm there are two files:
-
SearchForm.Designer.vbis written by the visual designer and rewritten whenever you change the form in the designer. It containsPartial Class SearchForm, theFriend WithEventsfields for the controls,InitializeComponent, aDisposeoverride and the<DesignerGenerated>attribute. You do not edit it by hand: hand edits are lost or confuse the designer. -
SearchForm.vbis yours: constructor, event handlers and logic.
WithEvents fields are what make Handles SearchButton.Click work in the hand-written part (see
Events and Handlers). A WPF window uses the same pattern in
another form: MainWindow.xaml is compiled into a generated partial class (the file MainWindow.g.vb under obj), and
MainWindow.xaml.vb is the code-behind part, which must call InitializeComponent() as well (see
WPF Applications).
This pair of files is the shape that Visual Studio produces and was built, but not run, with the .NET 10 SDK on Linux using
EnableWindowsTargeting, because Windows Forms cannot start there. The project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<RootNamespace>Shelf</RootNamespace>
<UseWindowsForms>true</UseWindowsForms>
<MyType>WindowsFormsWithCustomSubMain</MyType>
<StartupObject>Sub Main</StartupObject>
<OptionStrict>On</OptionStrict>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<NuGetAudit>false</NuGetAudit>
</PropertyGroup>
<ItemGroup>
<Import Include="System.Windows.Forms" />
</ItemGroup>
</Project>
The generated half:
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class SearchForm
Inherits System.Windows.Forms.Form
' Generated by the Windows Forms designer: do not edit by hand.
<System.Diagnostics.DebuggerNonUserCode()>
Protected Overrides Sub Dispose(disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
Private components As System.ComponentModel.IContainer
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Me.SearchBox = New System.Windows.Forms.TextBox()
Me.SearchButton = New System.Windows.Forms.Button()
Me.SuspendLayout()
'
'SearchBox
'
Me.SearchBox.Location = New System.Drawing.Point(12, 12)
Me.SearchBox.Name = "SearchBox"
Me.SearchBox.Size = New System.Drawing.Size(200, 23)
Me.SearchBox.TabIndex = 0
'
'SearchButton
'
Me.SearchButton.Location = New System.Drawing.Point(218, 12)
Me.SearchButton.Name = "SearchButton"
Me.SearchButton.Size = New System.Drawing.Size(75, 23)
Me.SearchButton.TabIndex = 1
Me.SearchButton.Text = "Search"
'
'SearchForm
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(305, 47)
Me.Controls.Add(Me.SearchBox)
Me.Controls.Add(Me.SearchButton)
Me.Name = "SearchForm"
Me.Text = "Shelf search"
Me.ResumeLayout(False)
Me.PerformLayout()
End Sub
Friend WithEvents SearchBox As System.Windows.Forms.TextBox
Friend WithEvents SearchButton As System.Windows.Forms.Button
End Class
The hand-written half:
Option Strict On
Public Class SearchForm
Public Sub New()
' Without this call the designer-generated controls are never created
InitializeComponent()
SearchBox.Text = "Refactoring"
End Sub
Private Sub SearchButton_Click(sender As Object, e As EventArgs) Handles SearchButton.Click
MessageBox.Show("Searching for " & SearchBox.Text)
End Sub
End Class
Friend Module Startup
<STAThread>
Public Sub Main()
Application.EnableVisualStyles()
Application.Run(New SearchForm())
End Sub
End Module
Build succeeded.
0 Warning(s)
0 Error(s)
Source: Windows Forms documentation
In the hand-written half, InitializeComponent() is called by the explicit constructor. Once you write Sub New yourself, you
must call it before touching any control, or the control fields are Nothing and the first use throws NullReferenceException.
The project sets MyType to WindowsFormsWithCustomSubMain because it supplies its own Sub Main, which is the setting a
Windows Forms project needs when it does not use the application framework.
Practical rules for generated-plus-hand-written pairs:
-
Put every change in your part. If a tool needs a change in the generated part, change the tool’s input (the form in the designer, the XAML file) instead.
-
Use partial methods as the extension points of generated code, so that a missing hook costs nothing.
-
Check generated files in the way the repository treats other source files, but exclude them from style checks and analyzer rules that you apply to hand-written code (see XML Documentation, Analyzers and Code Style), and from code coverage thresholds.
-
Resist splitting a large hand-written class across several partial files to make it look smaller; if a class needs that, it has more than one responsibility (see Classes and Objects).
Older books and articles
Material written for Visual Basic 2008 to Visual Basic 2010 shows <Extension()> with parentheses, ByVal on every parameter and
the first partial-method examples with a message box. All of that still compiles. What has changed around it: the code now
builds as SDK-style projects on .NET, not .NET Framework; designer files are the same in structure, but the project file no longer
lists them; Partial has no new rules. Older text that says extension methods need .NET Framework 3.5 or a reference to
System.Core is no longer relevant, because the attribute lives in the base class library; and text that shows a designer pair
with the Class header repeated, Public Class Form1 with Partial Public Class Form1, is valid for the same reason: at most
one part may omit Partial, but it is better to write it on every part.
Common mistakes
| Symptom | Cause and fix |
|---|---|
BC36551 on an extension method |
declared in a |
BC36550 or BC30662 |
|
BC30456 ( |
the module’s namespace is not imported; add |
BC30521 for an extension call |
two imported namespaces define the same extension; remove one import or call |
BC30574 on |
|
The extension is never called after upgrading a library |
a new instance method with the same signature was added to the type; rename the extension |
BC31437, BC31431, BC31435 |
a partial method that is a |
A hook never runs and nothing is reported |
no implementation exists or its name or parameter list differs; implementation is optional, so there is no error |
|
|
BC30179 on a class declared in two files |
neither declaration says |
BC30928 or BC30925 |
the parts disagree about the base class or the access level |