Properties and Default Properties
|
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 (except where the text
says otherwise) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The project’s root
namespace is ShelfDocs, which matters only where a stack trace or a reflection name prints it. Examples that are meant to fail
say so and show the compiler messages that the SDK printed.
A property looks like a field to its caller and runs code like a method inside. It is the unit of encapsulation of the .NET
type system: data binding, serialization, designers and reflection-based frameworks work with properties, not with public
fields, and an interface can declare a property but not a field. In Visual Basic a property is declared with a
Property ... End Property block that holds a Get accessor, a Set accessor or both, or, since Visual Basic 10, in a one-line
auto-implemented form. The running example is the shelf library catalogue: Book, Loan, Author and LoanState use
properties everywhere, a Shelf collection exposes its books through a Default property, and a view-model reports changes to
a user interface.
The page covers the full declaration syntax and its exact rules, auto-implemented properties, ReadOnly and WriteOnly,
access levels of the accessors, parameterized and Default properties with the bang operator, overriding and implementing
properties, a series of pitfalls that the compiler reports (and two that it only warns about), what Visual Basic can and
cannot do with init-only and required members declared in C#, INotifyPropertyChanged, a comparison with C#, and the points
where older material is outdated. Fields, constructors and access modifiers are introduced on
Classes and Objects; the types a property can return are on
Data Types and Literals and
Nothing and Nullable Value Types.
Property procedures
The general form, from the Language Reference, is:
[<attributes>] [Default] [accessmodifier] [Shared | Overridable | Overrides | MustOverride | NotOverridable | Shadows]
[ReadOnly | WriteOnly] Property Name[(parameters)] [As Type] [Implements InterfaceName.Member]
[<attributes>] [accessmodifier] Get
' statements
End Get
[<attributes>] [accessmodifier] Set[(ByVal value As Type)]
' statements
End Set
End Property
The rules that decide whether a declaration compiles:
-
A property without
ReadOnlyorWriteOnlymust have both aGetand aSet(otherwise BC30124). AReadOnlyproperty has onlyGet; aWriteOnlyproperty has onlySet(aSetin aReadOnlyproperty is BC30022). -
The
Setaccessor receives the assigned value in a parameter. By convention it is declaredSet(value As T); the parameter name is free, but its type must be the property’s type (BC31064). When the parentheses are omitted, the compiler supplies a parameter namedValueof the property’s type. -
Getis a function without parameters of its own: the property’s parameters (if any) are the parameters of the property, and the same values arrive atGetandSet.Setreceives them plus the new value. -
A property is an ordinary member for the purposes of
Shared,Overridable,Overrides,MustOverride,NotOverridable,ShadowsandImplements.Sharedproperties belong to the type and are described on Modules and Shared Members. -
The access modifier of the property is the default for the accessors; one accessor may be narrower (see below).
-
Under
Option Strict OntheAsclause is mandatory (BC30210);Option Strict Offlets a property default toObject. -
Each accessor is compiled to a method named
get_Nameandset_Name, and the property itself is metadata that links them. TheGetandSetblocks of one property can carry their own attributes.
The first example uses the full form: validation in the setter, a computed read-only property, a write-only property and the
older style in which the property name is assigned to produce the result before Exit Property.
Option Strict On
Public Class Loan
Private _borrower As String = ""
Private _dueOn As Date
' A read/write property with validation in both accessors.
Public Property Borrower As String
Get
Return _borrower
End Get
Set(value As String)
If String.IsNullOrWhiteSpace(value) Then
Throw New ArgumentException("A borrower name is required.", NameOf(value))
End If
_borrower = value.Trim()
End Set
End Property
' A read-only property computed from other state: no backing field.
Public ReadOnly Property IsOverdue(today As Date) As Boolean
Get
Return today.Date > _dueOn.Date
End Get
End Property
Public WriteOnly Property DueOn As Date
Set(value As Date)
_dueOn = value
End Set
End Property
' Older style: assign to the property name inside Get, then Exit Property.
Public ReadOnly Property Label As String
Get
If _borrower.Length = 0 Then
Label = "(nobody)"
Exit Property
End If
Label = "Loan to " & _borrower
End Get
End Property
End Class
Module Program
Sub Main()
Dim loan As New Loan()
Console.WriteLine(loan.Label)
loan.Borrower = " Ada Lovelace "
loan.DueOn = New Date(2026, 10, 1)
Console.WriteLine(loan.Label)
Console.WriteLine(loan.IsOverdue(New Date(2026, 10, 19)))
Try
loan.Borrower = " "
Catch ex As ArgumentException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message.Replace(Environment.NewLine, " | "))
End Try
Console.WriteLine("[" & loan.Borrower & "]")
End Sub
End Module
(nobody)
Loan to Ada Lovelace
True
ArgumentException: A borrower name is required. (Parameter 'value')
[Ada Lovelace]
Source: Property Statement
-
Return valueinGetis the normal form. Assigning to the property’s name inside its ownGetsets the return value, exactly as assigning to a function’s name does inFunction(see Procedures: Sub and Function).Exit Propertyleaves an accessor early and is the property counterpart ofExit Function. -
IsOverdue(today As Date)is a read-only property with a parameter. It is legal, rarely the best design (a method namedIsOverdueOnis clearer), and is the topic of the section on parameterized properties. -
The setter rejects an empty borrower by throwing
ArgumentException; the message includes(Parameter 'value')because the setter’s parameter is calledvalue. The Framework Design Guidelines (the page notes that it reprints the 2008 edition) ask that getters be simple and not throw, and that a setter which throws leave the previous value in place, as this one does.
Why a property and not a public field
A Public field would compile to the same call syntax, but a field is not a property in .NET: it cannot implement an interface
member, data binding and most serializers ignore it by default, it cannot validate or raise a change notification, and
replacing it with a property later is a binary-breaking change (callers are compiled against a field access). The next example
shows what reflection and System.Text.Json see.
Option Strict On
Imports System.Text.Json
Public Class Probe
Public Field As Integer = 1
Public Property Prop As Integer = 2
Public ReadOnly Property Computed As Integer
Get
Return Prop * 10
End Get
End Property
End Class
Module Program
Sub Main()
Dim p As New Probe()
Console.WriteLine(JsonSerializer.Serialize(p))
Dim t = GetType(Probe)
Console.WriteLine("members: " & String.Join(", ", t.GetMembers().
Where(Function(m) m.DeclaringType Is t).
Select(Function(m) m.MemberType.ToString() & " " & m.Name).
OrderBy(Function(s) s)))
End Sub
End Module
{"Prop":2,"Computed":20}
members: Constructor .ctor, Field Field, Method get_Computed, Method get_Prop, Method set_Prop, Property Computed, Property Prop
The serializer wrote Prop and the read-only Computed, and skipped Field. The compiler also produced get_Prop,
set_Prop and get_Computed methods; there is no set_Computed. The property is metadata, the accessors are the code.
Computed has no backing field at all: it is recomputed on every read, which is the right choice for a value derived from other
state.
Auto-implemented properties
Writing a field, a Get that returns it and a Set that assigns it for every plain property is the boilerplate that Visual
Basic 10 removed. A property declared on one line, without Get/Set blocks, is auto-implemented: the compiler generates a
private backing field and the accessors.
-
Syntax:
Public Property Title As String. An initializer follows theAsclause:Public Property Title As String = "Untitled". TheAs Newform works too:Public ReadOnly Property Authors As New List(Of Author). -
The backing field is named with an underscore and the property’s name (
_Title). The name is visible to reflection and, within the class, to the editor as a hidden member; other code in the same class cannot refer to it by name. -
ReadOnlyauto-implemented properties (Visual Basic 14 and later) can be given an initializer, and can be assigned in the constructor of the declaring class and nowhere else. Assigning one in an ordinary method is BC30526. -
An auto-implemented property cannot have accessor blocks, so a narrower setter, a validation or a change notification needs the full form. There is no
Initaccessor and no way to declarerequired(see the section on C# interop). -
Visual Basic 9 and earlier reject the syntax (BC36716); see History, Versions and Language Strategy.
-
Under
Option Strict OntheAsclause cannot be omitted (BC30210), even when an initializer would allow the compiler to infer the type; withOption Strict Offthe property silently becomes anObjectproperty (a test printedObjectforPublic Property Year = 2000). -
An initializer runs as part of every constructor, before the constructor body and, in a derived class, after the base class’s constructor has run (a test printed
Base ctor,P initializer,Derived ctor bodyin this order). C# runs field initializers before the base constructor.
Option Strict On
Imports System.Reflection
Public Class Author
Public Property Name As String
Public Property BornOn As Date?
End Class
Public Class Book
' Auto-implemented: initializer, As New, ReadOnly with initializer.
Public Property Title As String = "Untitled"
Public Property Year As Integer = 2000
Public ReadOnly Property Authors As New List(Of Author)
Public ReadOnly Property Created As Date = New Date(2026, 10, 19)
Public Property Copies As Integer
Public ReadOnly Property Shelf As String
Public Sub New(shelf As String)
Me.Shelf = shelf ' a ReadOnly auto-property can be set in the constructor
End Sub
End Class
Module Program
Sub Main()
Dim book As New Book("A-12")
Console.WriteLine(book.Title & " / " & book.Year & " (" & book.Year.GetType().Name & ") / " & book.Shelf)
Console.WriteLine(book.Authors.Count & " authors, copies " & book.Copies)
' The compiler generates a backing field named _ + property name.
Dim fields = GetType(Book).GetFields(BindingFlags.Instance Or BindingFlags.NonPublic)
For Each f In fields.OrderBy(Function(x) x.Name)
Console.WriteLine("field " & f.Name & " : " & f.FieldType.Name & If(f.IsInitOnly, " (initonly)", ""))
Next
Console.WriteLine(GetType(Book).GetProperty("Year")?.PropertyType.Name)
End Sub
End Module
Untitled / 2000 (Int32) / A-12
0 authors, copies 0
field _Authors : List`1
field _Copies : Int32
field _Created : DateTime
field _Shelf : String
field _Title : String
field _Year : Int32
Int32
Source: Auto-Implemented Properties
Two details of this output are worth remembering. First, Authors is read-only but its list is not: the property cannot be
re-assigned, while book.Authors.Add(...) is allowed; if the intention is a collection that callers cannot change, expose
IReadOnlyList(Of Author) (see
Collections). Second, the backing field of a ReadOnly
auto-implemented property is not marked initonly in the IL: the compiler enforces the rule, the runtime does not.
When to leave the auto form
Switch to a full property when the setter must validate, when a change must be announced (see INotifyPropertyChanged below),
when the getter computes a value, when the two accessors need different access levels, when the value lives somewhere else (a
dictionary, a settings store, another object), or when the property implements an interface member under a different name.
You can also convert later without breaking callers, because the property is already the public contract.
ReadOnly, WriteOnly and Mixed Access
ReadOnly means that code outside the constructor, and outside the declaring class, cannot assign the property. WriteOnly
properties are unusual: the Framework Design Guidelines say not to provide set-only properties and to write a method
named Set... instead (for example SetPassword(value)).
A property can give each accessor a different access level, as long as the property is declared with both accessors and only one accessor is narrower than the property itself:
-
Public Property Count As IntegerwithPrivate Setmakes a property that is publicly readable and privately writable. The accessor modifier may beProtected,Friend,Private,Protected FriendorPrivate Protected(both compound forms were compiled for this page), and must be strictly more restrictive than the property’s access level (otherwise BC31100). -
Only one of the two accessors may carry a modifier (otherwise BC31101), and a
ReadOnlyorWriteOnlyproperty cannot have one at all (BC31105).
Option Strict On
Public Structure Point
Public X As Integer
Public Y As Integer
End Structure
Public Class Shelf
Private _count As Integer
' Public Get, Private Set: callers read, only the class writes.
Public Property Count As Integer
Get
Return _count
End Get
Private Set(value As Integer)
_count = value
End Set
End Property
' An auto-property cannot have accessor blocks, so a narrower setter needs the full form above.
Public Property Origin As Point
Public ReadOnly Property Fixed As Point
Public Sub Add()
Count += 1
End Sub
Public Sub BumpByRef()
Program.Increment(Count) ' ByRef to a property: copy-in, call, copy-out through the setter
End Sub
End Class
Public Module Program
Friend Sub Increment(ByRef value As Integer)
value += 1
End Sub
Sub Main()
Dim s As New Shelf()
s.Add()
s.Add()
Console.WriteLine("Count = " & s.Count)
' A property of a structure type returns a copy; the compiler rejects
' s.Origin.X = 5 (see the error example below). Copy, modify, assign back:
Dim p = s.Origin
p.X = 5
s.Origin = p
Console.WriteLine("Origin.X = " & s.Origin.X)
s.BumpByRef()
Console.WriteLine("after BumpByRef = " & s.Count)
' Passing a property ByRef: copy-in/copy-out.
Dim copy As Integer = s.Count
Increment(copy)
Console.WriteLine("local copy " & copy & ", property still " & s.Count)
End Sub
End Module
Count = 2
Origin.X = 5
after BumpByRef = 3
local copy 4, property still 3
Three behaviours of the example are not obvious.
-
Count += 1insideShelfreads and writes the property: the compound assignment callsGet, adds and callsSet, so a setter with side effects runs on every+=. -
Originis a property of a structure type. Readings.Originreturns a copy, sos.Origin.X = 5modifies a temporary copy; the compiler therefore rejects it (BC30068, below) instead of silently doing nothing. Read the value into a local, change it and assign the whole structure back, as the example does. A class-typed property does not have this problem. See Structures and Value Types. -
Passing a property
ByRefis accepted, but aByRefargument that is a property is implemented as copy-in, call and copy-out throughSet. PassingCountfrom inside the class updated the property (3), while passing a local copy left the property alone (the local became 4,Countstayed 3). Passing aReadOnlypropertyByRefis accepted without a diagnostic too, and the callee’s change is silently lost (a test withBump(t.R)leftt.Rat 3), so avoid passing propertiesByRef.
Compiler errors for property declarations
The next three programs fail on purpose. They are separate programs because the compiler reports declaration errors first and stops before it checks the code that uses the declarations. The messages are exactly what the SDK printed.
Option Strict On
Public Class Broken
Private _x As Integer
Public Property Untyped = 1
Public Property HalfDone As Integer
Get
Return _x
End Get
End Property
Public ReadOnly Property Mixed As Integer
Get
Return _x
End Get
Set(value As Integer)
_x = value
End Set
End Property
Default Public Property Plain As Integer
Get
Return _x
End Get
Set(value As Integer)
_x = value
End Set
End Property
Default Public Property First(i As Integer) As Integer
Get
Return i
End Get
Set(value As Integer)
End Set
End Property
Default Public Property Second(i As String) As Integer
Get
Return 0
End Get
Set(value As Integer)
End Set
End Property
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(6,21): error BC30210: Option Strict On requires all Function, Property, and Operator declarations to have an 'As' clause.
Program.vb(8,21): error BC30124: Property without a 'ReadOnly' or 'WriteOnly' specifier must provide both a 'Get' and a 'Set'.
Program.vb(18,9): error BC30022: Properties declared 'ReadOnly' cannot have a 'Set'.
Program.vb(23,29): error BC31048: Properties with no required parameters cannot be declared 'Default'.
Program.vb(32,29): error BC30359: 'Default' can be applied to only one property name in a class.
Program.vb(40,29): error BC30359: 'Default' can be applied to only one property name in a class.
Option Strict On
Public Class Broken
Private _x As Integer
Protected Property Narrow As Integer
Public Get
Return _x
End Get
Set(value As Integer)
_x = value
End Set
End Property
Public Property BothNarrow As Integer
Private Get
Return _x
End Get
Private Set(value As Integer)
_x = value
End Set
End Property
Public ReadOnly Property Priv As Integer
Private Get
Return _x
End Get
End Property
End Class
Module Program
Sub Main()
End Sub
End Module
Program.vb(7,9): error BC31100: Access modifier 'Public' is not valid. The access modifier of 'Get' and 'Set' should be more restrictive than the property access level.
Program.vb(19,9): error BC31101: Access modifier can only be applied to either 'Get' or 'Set', but not both.
Program.vb(25,9): error BC31105: 'ReadOnly' properties cannot have an access modifier on 'Get'.
Source: Property Statement
The first program also shows the Default rules that the next section explains: BC31048 for a Default property without
parameters, and BC30359 for the Default declarations (First and Second) that follow Plain, which has already used
the one default name the class is allowed. The second program shows the access-level rules.
The third program fails while using properties. It is built with warnings as errors, so BC42104 (a warning) is an error too.
Option Strict On
Public Structure Point
Public X As Integer
End Structure
Public Class Holder
Public Property Origin As Point
Public ReadOnly Property Fixed As Integer = 1
Public Property Count As Integer
Public Property Name As String
Get
Dim before = Name
Return before
End Get
Set(value As String)
End Set
End Property
End Class
Module Program
Sub Main()
Dim h As New Holder()
h.Origin.X = 3
h.Fixed = 5
Dim n = h.Count(1)
End Sub
End Module
Program.vb(14,26): error BC42104: Variable 'Name' is used before it has been assigned a value. A null reference exception could result at runtime.
Program.vb(25,9): error BC30068: Expression is a value and therefore cannot be the target of an assignment.
Program.vb(26,9): error BC30526: Property 'Fixed' is 'ReadOnly'.
Program.vb(27,17): error BC32016: 'Public Property Count As Integer' has no parameters and its return type cannot be indexed.
Source: Property Statement
-
BC42104 deserves attention. Inside the
Getof a property, a bareNameis not a call to the property: it is the implicit return variable, exactly like a function’s name in aFunction. Reading it before assigning it readsNothing(the default). The next section shows the opposite trap inSet. -
BC30068 is the structure-copy rule from the previous example; BC30526 is the assignment to a
ReadOnlyproperty from outside the constructor; BC32016 is a call with arguments on a property that has none (the compiler tries to index theIntegerthatCountreturns).
Recursion inside accessors
A common mistake is to use the property name inside its own accessors when the intent was the backing field. The two accessors behave differently, and the compiler warns in both cases but does not fail unless warnings are errors.
Option Strict On
Public Class Box
Private _name As String = "stored"
Public Property Name As String
Get
Dim seen = Name
Return If(seen, "(Nothing)")
End Get
Set(value As String)
Name = value
_name = value
End Set
End Property
End Class
Module Program
Sub Main()
Dim b As New Box()
b.Name = "x"
Console.WriteLine(b.Name)
End Sub
End Module
This program was built with TreatWarningsAsErrors set to false, because both lines are warnings. The compiler printed:
Program.vb(7,24): warning BC42104: Variable 'Name' is used before it has been assigned a value. A null reference exception could result at runtime.
Program.vb(11,13): warning BC42026: Expression recursively calls the containing property 'Public Property Name As String'.
and dotnet run ended in a crash, whose first line is:
Stack overflow.
(The runtime then prints the repeated frames, ShelfDocs.Box.set_Name, followed by ShelfDocs.Program.Main(); the number of
repetitions varies from run to run and is omitted.)
-
In
Get,Nameis the implicit return variable (BC42104 says it is read before being assigned). It is not a recursion. -
In
Set,Name = valueis a call to the setter itself (BC42026: Expression recursively calls the containing property), so the first assignment recurses until the stack is exhausted. A .NET process cannot recover from a stack overflow. -
The cure is to use the backing field (
_name = value) or an auto-implemented property.
Source: Property Statement
Parameterized properties
A property may have parameters. The parameters are declared after the name, as for a Sub: Public ReadOnly Property Slot(row As
Integer, column As Integer) As String. The call syntax is shelf.Slot(2, 3), and an assignment is shelf.Slot(2, 3) = "x".
The rules are those of procedure parameters (Procedures:
Sub and Function):
-
ByValis the default and the only form allowed: aByRefproperty parameter is BC30651. Parameters can beOptionalwith a constant default value (Opt(Optional i As Integer = 5)is read ast.Optwhen the argument is omitted), andParamArrayis allowed on the last one (t.PA(1, 2, 3)); both were built and run for this page. -
Parameter names appear in IntelliSense and support named arguments (
ByTitle("PER", ignoreCase:=False)). -
Properties can be overloaded by parameter list, like methods; the return type is not part of the signature.
-
Setreceives the parameters plusvalue; theGetandSetblocks of one property share the parameter list.
C# has no parameterized properties other than this[...] indexers. C# code sees a Visual Basic parameterized property as
its accessor methods (get_Slot, set_Slot), and as an indexer only if the property is the Default one.
Default properties
A Default property is the type’s indexer: shelf(0) is shorthand for shelf.Item(0). The compiler records the name of the
default property in the DefaultMemberAttribute on the type, and any language that understands the attribute (C# indexers,
VB, F# .[ ]) can use it.
-
Rule: a
Defaultproperty must have at least one parameter (BC31048); there is no parameterless default property in .NET Visual Basic. This is a change from Visual Basic 6, whereText1 = "x"could meanText1.Text = "x". -
Only one name may be the default in a class (BC30359), but that name can be overloaded with several parameter lists. Typically the property is called
Item, which is what the .NET collections use and what a C#this[]indexer is compiled to, but any name works. -
The modifier
Defaultmay stand before or after the access modifier (Default Public Property Item(...)andPublic Default Property Item(...)are both used in the .NET documentation). It cannot be combined withShared: that is BC30502. -
A default property is inherited and is also declared in an interface (
Default ReadOnly Property Item(name As String) As String). The implementing class names its default property withImplements IIndexed.Item. -
Chained use is legal: if
shelvesis an array ofShelf,shelves(0)(0)indexes the array and then the default property of the element. -
Under
Option Strict Onthe default property of anObject-typed expression cannot be used: indexing anObjectis late binding and is rejected with BC30574 (see Early and Late Binding).
Option Strict On
Imports System.Reflection
Public Class Book
Public ReadOnly Property Title As String
Public ReadOnly Property Isbn As String
Public Sub New(title As String, isbn As String)
Me.Title = title
Me.Isbn = isbn
End Sub
Public Overrides Function ToString() As String
Return Title
End Function
End Class
Public Class Shelf
Private ReadOnly _books As New List(Of Book)
Public Sub Add(book As Book)
_books.Add(book)
End Sub
' The Default property: shelf(0) is shelf.Item(0).
Default Public Property Item(index As Integer) As Book
Get
Return _books(index)
End Get
Set(value As Book)
If value Is Nothing Then Throw New ArgumentNullException(NameOf(value))
_books(index) = value
End Set
End Property
' An overload of the default property with a String key: shelf!Dune and shelf("Dune") both work.
Default Public ReadOnly Property Item(isbn As String) As Book
Get
Return _books.FirstOrDefault(Function(b) b.Isbn = isbn)
End Get
End Property
' A non-default parameterized property: always written with its name.
Public ReadOnly Property ByTitle(text As String, Optional ignoreCase As Boolean = True) As IEnumerable(Of Book)
Get
Dim comparison = If(ignoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal)
Return _books.Where(Function(b) b.Title.Contains(text, comparison))
End Get
End Property
' Two-dimensional parameterized property.
Public ReadOnly Property Slot(row As Integer, column As Integer) As String
Get
Return "R" & row & "C" & column
End Get
End Property
End Class
Module Program
Sub Main()
Dim shelf As New Shelf()
shelf.Add(New Book("Dune", "9780441013593"))
shelf.Add(New Book("Emma", "9780141439587"))
Console.WriteLine(shelf(0)) ' default property by index
Console.WriteLine(shelf.Item(1)) ' the same, spelled out
Console.WriteLine(shelf("9780141439587")) ' overload by String
shelf(1) = New Book("Persuasion", "9780141439686")
Console.WriteLine(shelf(1).Title)
For Each b In shelf.ByTitle("per")
Console.WriteLine("found " & b.Title)
Next
Console.WriteLine(shelf.ByTitle("PER", ignoreCase:=False).Count())
Console.WriteLine(shelf.Slot(2, 3))
Try
Console.WriteLine(shelf(5))
Catch ex As ArgumentOutOfRangeException
Console.WriteLine(ex.GetType().Name)
End Try
Dim member = GetType(Shelf).GetCustomAttribute(Of DefaultMemberAttribute)()
Console.WriteLine("DefaultMember = " & member?.MemberName)
Dim shelves As Shelf() = {shelf}
Console.WriteLine(shelves(0)(0)) ' array index first, then the default property
End Sub
End Module
Dune
Emma
Emma
Persuasion
found Persuasion
0
R2C3
ArgumentOutOfRangeException
DefaultMember = Item
Dune
Notes on the output:
-
shelf(1)andshelf.Item(1)are the same call.shelf("9780141439587")picks the overload onString. Aftershelf(1) = New Book(…)the setter ran andshelf(1).TitleisPersuasion. -
ByTitle("per")returned the renamed book because the optional argumentignoreCasedefaults toTrue; withignoreCase:=Falsethe same text"PER"found nothing (0). -
An out-of-range index raised
ArgumentOutOfRangeExceptionfrom the underlyingList(Of T). The default property did not add its own check; as with any indexer, the contract for invalid indices is yours to document. -
DefaultMember = Itemis the attribute that makes C# see an indexer.
The bang operator
The bang operator ! is a shorthand for a default property whose single parameter is a String: tags!genre means
tags("genre"). It is a historical spelling used with dictionaries and with ADO recordsets (rs!Title), and it also works with
With blocks (!genre). The key is written as an identifier and is converted to a string literal, so a key that is not a valid identifier needs
the parenthesized form. The default property must take one String parameter: with an
Integer parameter the key is still a string, and Option Strict On reports BC30512 for the implicit conversion.
Option Strict On
Public Interface IIndexed
Default ReadOnly Property Item(name As String) As String
End Interface
Public Class Tags
Implements IIndexed
Private ReadOnly _values As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase)
' Default, single String parameter: this is what the ! operator needs.
Default Public Property Item(name As String) As String Implements IIndexed.Item
Get
Dim value As String = Nothing
Return If(_values.TryGetValue(name, value), value, "")
End Get
Set(value As String)
_values(name) = value
End Set
End Property
End Class
Module Program
Sub Main()
Dim t As New Tags()
t("genre") = "novel"
t!language = "en" ' bang: t("language") = "en"
Console.WriteLine(t!genre & "/" & t("LANGUAGE")) ' keys are case-insensitive here
Dim viaInterface As IIndexed = t
Console.WriteLine(viaInterface("genre"))
With t
!year = "1965"
Console.WriteLine(!year)
End With
End Sub
End Module
novel/en
novel
1965
Some points of style: ! hides a string key in something that looks like a member access, which means typos are not found
by the compiler. New code should write tags("genre") or, better, expose strongly typed properties and keep the dictionary
private. Notice also that a property can implement an interface property of the same name and signature in one line by
naming it in the Implements clause; the interface declares its own Default property.
Overriding and implementing properties
Properties follow the same inheritance rules as methods (Inheritance and Polymorphism and Interfaces).
-
A property is virtual only if it is declared
Overridable,Overrides(which is itself overridable unlessNotOverridable) orMustOverride. Without a modifier a property is not overridable, andShadowshides rather than overrides. -
An override has the same signature as the property it overrides, including
ReadOnly/WriteOnly, and may not widen the access level. In the example, the base class exposes a protected setter and the override keeps it protected. -
An override reads or writes the base implementation with
MyBase.Rating. -
A class that implements an interface property needs at least the accessors the interface declares. It may have more: a
ReadOnlyinterface property may be implemented by a read/write property, and the extra accessor is simply not part of the interface. In the example the interface’sPriceis read-only whileBook.Priceis read/write. -
Implementsties a property to the interface member by name, so the class property can use a different name (RemarkimplementsIPriced.Note). A caller through the interface seesNote, through the classRemark. -
MustOverrideproperties have no body (Public MustOverride ReadOnly Property Kind As String), and the class must beMustInherit.
Option Strict On
Public Interface IPriced
ReadOnly Property Price As Decimal
Property Note As String
End Interface
Public MustInherit Class CatalogItem
Public Property Title As String = ""
' MustOverride property: derived classes must supply it.
Public MustOverride ReadOnly Property Kind As String
' Overridable property with a protected setter.
Public Overridable Property Rating As Integer
Get
Return _rating
End Get
Protected Set(value As Integer)
_rating = Math.Clamp(value, 0, 5)
End Set
End Property
Private _rating As Integer
Public Sub Rate(value As Integer)
Rating = value
End Sub
End Class
Public Class Book
Inherits CatalogItem
Implements IPriced
' ReadOnly in the interface, read/write in the class: allowed (an extra accessor is fine).
Public Property Price As Decimal Implements IPriced.Price
' Different name than the interface member.
Private _note As String = ""
Public Property Remark As String Implements IPriced.Note
Get
Return _note
End Get
Set(value As String)
_note = value
End Set
End Property
Public Overrides ReadOnly Property Kind As String
Get
Return "Book"
End Get
End Property
Public Overrides Property Rating As Integer
Get
Return MyBase.Rating
End Get
Protected Set(value As Integer)
MyBase.Rating = value
Console.WriteLine("rated " & value & " -> " & MyBase.Rating)
End Set
End Property
End Class
Module Program
Sub Main()
Dim b As New Book() With {.Title = "Dune", .Price = 9.5D, .Remark = "signed"}
b.Rate(9)
Console.WriteLine(b.Kind & ": " & b.Title & " " & b.Rating)
Dim priced As IPriced = b
Console.WriteLine(priced.Price & " " & priced.Note)
priced.Note = "reprint"
Console.WriteLine(b.Remark)
End Sub
End Module
rated 9 -> 5
Book: Dune 5
9.5 signed
reprint
Source: Property Statement
Rate(9) calls the protected setter of the base through the public method; the override clamps through MyBase.Rating
(Math.Clamp limits it to 0..5), so the printed rating is 5 even though 9 was requested. Because the override’s setter
contains the Console.WriteLine, the output shows both numbers. Through the interface, priced.Note = "reprint"
reached Remark, as b.Remark shows.
A property with the name of its type
Public Property Isbn As Isbn is legal and common: the property is named like its type (the "Color Color" situation). Within
the class, Isbn can mean the property or the type, and the compiler picks by context: Isbn.Parse(...) is a call on the
type, Isbn.Value is a member of the property’s value. Shared members of the type are therefore reachable even though the
instance property has the same name.
Option Strict On
Public Structure Isbn
Public Shared ReadOnly Empty As New Isbn("")
Public ReadOnly Value As String
Public Sub New(value As String)
Me.Value = value
End Sub
Public Shared Function Parse(text As String) As Isbn
Return New Isbn(text)
End Function
End Structure
Public Class Book
' "Color Color": the property is named like its type.
Public Property Isbn As Isbn
Public Function Describe() As String
Dim parsed = Isbn.Parse("111") ' the type's Shared member, or the property?
Return Isbn.Value & "/" & parsed.Value & "/" & Isbn.Empty.Value
End Function
Public Shared Property Total As Integer ' Shared property: one value for the type
End Class
Module Program
Sub Main()
Dim b As New Book() With {.Isbn = New Isbn("999")}
Console.WriteLine(b.Describe())
Book.Total += 2
Console.WriteLine(Book.Total)
End Sub
End Module
999/111/
2
Source: Property Statement
Book.Total is a Shared property, accessed through the type name. Accessing it through an instance (other.Total) compiles
with warning BC42025 (Access of shared member, constant member, enum member or nested type through an instance), because the
qualifying expression is not evaluated; with warnings as errors, the build fails, which is a good reason to keep them on.
Consuming C# init-only and required properties
C# 9 added init-only properties ({ get; init; }), which can be assigned only in an object initializer or a constructor,
and C# 11 added required members, which an object creation must set. Visual Basic cannot declare either, but its compiler
understands both when it consumes a library:
-
Visual Basic 16.9 enables consumption of init-only properties (the What’s new page lists this for 16.9). The property appears as an ordinary property with an assignable setter in an object initializer, and as read-only everywhere else. Assigning one in another context is BC37311.
-
requiredmembers declared in a C# library are enforced by the Visual Basic compiler of this SDK:New Reader()without setting a required property is BC37321, unless the constructor is marked[SetsRequiredMembers]in C# (the compiler then trusts the constructor). Do not rely on Visual Basic ignoringrequired: on this compiler it does not. -
Visual Basic cannot declare them. There is no
Initaccessor (the compiler reports a syntax error), and applyingRequiredMemberAttributeyourself is BC37325, because that attribute is reserved for the compiler. -
C# indexers appear as the type’s default property (
reader(3)), and C# records give Visual Basic ordinary read-only properties for their positional members.
The example is a two-project solution: a C# class library compiled by the same SDK, and the Visual Basic console that references it. The C# source is:
using System.Diagnostics.CodeAnalysis;
namespace CsLib;
public class Reader
{
public string Name { get; init; } = "";
public required string CardNumber { get; init; }
public int Visits { get; set; }
public Reader() { }
[SetsRequiredMembers]
public Reader(string cardNumber) { CardNumber = cardNumber; }
// C# indexer: compiled as a property named Item with DefaultMember("Item").
public string this[int index] => $"slot {index}";
}
public record Member(string Name, int Id);
and the Visual Basic program is:
Option Strict On
Imports CsLib
Module Program
Sub Main()
' Init-only and required properties are set in an object initializer.
Dim reader As New Reader() With {.Name = "Ada", .CardNumber = "C-1"}
' This constructor is marked [SetsRequiredMembers] in the C# source.
Dim viaCtor As New Reader("C-2")
Console.WriteLine(reader.Name & " " & reader.CardNumber & " " & viaCtor.CardNumber)
reader.Visits += 1 ' an ordinary setter is still fine
Console.WriteLine(reader.Visits)
' A C# indexer is seen as the default property.
Console.WriteLine(reader(3))
Dim m As New Member("Grace", 7)
Console.WriteLine(m.Name & " #" & m.Id)
End Sub
End Module
Ada C-1 C-2
1
slot 3
Grace #7
Source: What’s new in Visual Basic
Assigning an init-only or required property outside an initializer, or creating the type with a parameterless New and no
initializer for a required property, produced these errors (separate program, not part of the example above):
BC37311: Init-only property 'Name' can only be assigned by an object member initializer, or on 'Me', 'MyClass` or 'MyBase' in an instance constructor.
BC37321: Required member 'Public Overloads Property CardNumber As String' must be set in the object initializer or attribute arguments.
Source: required modifier (C# reference)
Practical consequence: when a Visual Basic type must be consumable from C# with the same guarantee, declare the properties as ordinary read/write properties and a constructor that takes the mandatory values, or provide a factory. Init-only semantics are a C# feature that Visual Basic can honour but not produce.
INotifyPropertyChanged and data binding
User interface frameworks (Windows Forms, WPF, MAUI, Avalonia) bind controls to properties and update the screen when
the object raises PropertyChanged from System.ComponentModel.INotifyPropertyChanged. In Visual Basic the interface is
implemented with Implements and an Event; the <CallerMemberName> attribute (the .NET compiler fills in the name of the
calling member for an optional String parameter) removes the magic strings.
Option Strict On
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Public MustInherit Class ObservableObject
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
' Returns True when the value changed; ByRef lets one helper serve every backing field.
Protected Function SetField(Of T)(ByRef field As T, value As T,
<CallerMemberName> Optional propertyName As String = Nothing) As Boolean
If EqualityComparer(Of T).Default.Equals(field, value) Then Return False
field = value
OnPropertyChanged(propertyName)
Return True
End Function
End Class
Public Class LoanViewModel
Inherits ObservableObject
Private _borrower As String = ""
Private _daysLate As Integer
Public Property Borrower As String
Get
Return _borrower
End Get
Set(value As String)
SetField(_borrower, value)
End Set
End Property
Public Property DaysLate As Integer
Get
Return _daysLate
End Get
Set(value As Integer)
If SetField(_daysLate, value) Then
OnPropertyChanged(NameOf(Fine)) ' dependent, computed property
End If
End Set
End Property
Public ReadOnly Property Fine As Decimal
Get
Return _daysLate * 0.25D
End Get
End Property
End Class
Module Program
Sub Main()
Dim vm As New LoanViewModel()
AddHandler vm.PropertyChanged,
Sub(sender, e)
Console.WriteLine("changed: " & e.PropertyName)
End Sub
vm.Borrower = "Ada"
vm.Borrower = "Ada" ' same value: no notification
vm.DaysLate = 4
Console.WriteLine("Fine = " & vm.Fine.ToString("N2", Globalization.CultureInfo.InvariantCulture))
End Sub
End Module
changed: Borrower
changed: DaysLate
changed: Fine
Fine = 1.00
Source: INotifyPropertyChanged Interface
-
SetFieldtakes the backing fieldByRef, so one generic helper serves every property. It returnsTrueonly when the value changed, which is how the secondvm.Borrower = "Ada"produced no notification. -
DaysLatealso raisedFine, becauseFineis computed from it. A binding onFineneeds that second notification; the compiler cannot know about the dependency. -
NameOf(Fine)is checked at compile time and survives a rename; theEventis the standard .NET handler type and is raised withRaiseEvent. Events are the subject of Events and Handlers. -
Raise the event on the thread that owns the control (the UI thread) in Windows Forms and WPF, or marshal to it; see Windows Forms Applications and WPF Applications for binding to these properties.
-
A
WithEventsfield is a special case that Visual Basic implements as a property withGetandSetaccessors, so thatHandlesclauses are wired and unwired inSet(see the page on events).
Choosing between a property, a field and a method
| Situation | Use | Reason |
|---|---|---|
Plain data with no rules |
auto-implemented property |
binding, serialization, interfaces and future validation all work |
Value derived from other state, cheap |
|
always up to date, no backing field |
Expensive or side-effecting work |
|
a property read should not surprise the caller |
Must be set only by the class |
|
the type controls its invariants |
Needs an argument, or may be called often with different values |
method, or an indexer if it is a lookup |
|
A constant |
|
no accessor cost, usable in attributes |
Internal mutable state |
|
not part of the contract |
Properties are called through method calls, so each access has a cost the JIT usually removes by inlining. Do not hold a lock, hit the file system or the network, or throw for ordinary conditions inside a getter.
Visual Basic compared with C#
| Feature | Visual Basic | C# |
|---|---|---|
Auto-implemented |
|
|
Read-only auto |
|
|
Narrow setter |
full form with |
|
Init-only |
not declarable; consumed since VB 16.9 |
|
Required |
not declarable; enforced when consuming C# |
|
Expression-bodied |
not available; write the |
|
Indexer |
|
|
Named indexer |
any name with |
|
Parameterized property |
|
not available; use an indexer or a method |
Bang operator |
|
not available |
Accessor access |
|
|
Property named like its type |
allowed ("Color Color") |
allowed |
Backing field name |
|
|
Using the name in an accessor |
|
|
Interface implementation |
|
implicit by name or explicit |
Older material you may meet
Tutorials, books and code written for Visual Basic 6 and the first versions of Visual Basic .NET use idioms that no longer match the current language:
-
VB6 declared
Property Get,Property LetandProperty Setas three separate procedures, withSetfor object-valued properties. In Visual Basic .NET there is onePropertyblock withGetandSet, and object assignment is the plain=statement. The keywordSetfor assignment (Set x = New Collection) andLetare gone. -
VB6 allowed a default property without parameters (
Text1 = "x"). It is BC31048 in .NET; write the property name. -
Older .NET code writes every property in the full form with a field prefixed
m_or_. Since Visual Basic 10 the auto form is the idiom for plain data, and the generated backing field is_PropertyName. -
Examples that return a value by assigning to the property name inside
Getstill compile but are rarely idiomatic; useReturn. -
Code that indexes an
Object(obj(0),obj!Name) relies on late binding of the default property. It compiles only withOption Strict Off(BC30574 otherwise) and throwsMissingMemberExceptionat run time when the object has no default property. -
Visual Basic Express-era tutorials describe the
Propertieswindow and designer-generatedMy.Settingsas the way to keep data; settings are covered on The My Namespace. -
Code that predates Visual Basic 14 has no
ReadOnlyauto-implemented property; it writes aReadOnlyfull property over aPrivatefield instead.
References
-
What’s new in Visual Basic (init-only property consumption)
-
INotifyPropertyChanged, CallerMemberNameAttribute and DefaultMemberAttribute
-
Property Design (.NET Framework Design Guidelines)