Interfaces facilitate writing code so that your program can be called into by some other code. This style of programming has been available for a long time, under the guise of "callback" functions. In this section we examine delegates in VB.NET, which can be thought of as type-safe and object-oriented callback functions. Delegates are the foundation for a design pattern, known as events , which we'll look at in the next section. A callback function is one which your program specifies and "registers" in some way, and which then gets called by other code. In C and C++ callback functions are implemented using function pointers. In VB.NET you can encapsulate a reference to a method as a delegate object. A delegate can refer to either a static method or an instance method. When a delegate refers to an instance method, it stores both an object instance and an entry point to the instance method. The instance method can then be called through this object instance. When a delegate object refers to a static method, it stores just the entry point of this static method. You can pass this delegate object to other code, which can then call your method. The code that calls your delegate method does not have to know at compile time which method is going to be called at runtime. In VB.NET a delegate is considered a reference type that is similar to a class type. A new delegate instance is created just like any other class instance, using the New operator. In fact, VB.NET delegates are implemented by the .NET Framework class library as a class, derived ultimately from System.Delegate . Delegates are object oriented and type safe, and they enjoy the safety of the managed code execution environment. Declaring a Delegate You declare a delegate in VB.NET using a special notation with the keyword Delegate and the signature of the encapsulated method. A naming convention suggests that your name should end with "Callback." We illustrate delegates in the sample program DelegateAccount . Here is an example of a delegate declaration from the file DelegateAccount.vb . The name NotifyCallback is arbitrary, but note that it follows the convention of ending with "Callback." Public Delegate Sub NotifyCallback(_ ByVal balance As Decimal) Defining a Method When you instantiate a delegate, you will need to specify a method, which must match the signature in the delegate declaration. The method may be either a static method or an instance method. Here are some examples of methods that can be hooked to the NotifyCallback delegate: Private Shared Sub NotifyCustomer(ByVal balance As Decimal) Console.WriteLine("Dear customer,") Console.WriteLine(_ " Account overdrawn, balance = {0}", balance) End Sub Private Shared Sub NotifyBank ( ByVal balance As Decimal) Console.WriteLine("Dear bank,") Console.WriteLine(_ " Account overdrawn, balance = {0}", balance) End Sub Private Sub NotifyInstance ( ByVal balance As Decimal) Console.WriteLine("Dear instance,") Console.WriteLine(_ " Account overdrawn, balance = {0}", balance) End Sub Creating a Delegate Object You instantiate a delegate object with the New operator, just as you would with any other class. The following code illustrates creating two delegate objects. The first delegate variable, named custDlg , is associated with a static method named NotifyCustomer . The second one, named instDlg , is associated with an instance method named NotifyInstance . The second delegate object internally will store both a method entry point and an object instance that is used for invoking the method. Dim custDlg As NotifyCallback = _ New NotifyCallback(AddressOf NotifyCustomer ) ... Dim da As DelegateAccount = New DelegateAccount() Dim instDlg As NotifyCallback = _ New NotifyCallback(AddressOf da.NotifyInstance ) Calling a Delegate You "call" a delegate just as you would a method. The delegate object is not a method, but it has an encapsulated method. The delegate object "delegates" the call to this encapsulated method, hence the name "delegate." In the following code the delegate object m_notifyDlg is called whenever a negative balance occurs on a withdrawal. In this example the m_notifyDlg delegate object is initialized in the method SetDelegate . Private m_notifyDlg As NotifyCallback ... Public Sub SetDelegate (ByVal dlg As NotifyCallback) m_notifyDlg = dlg End Sub ... Public Sub Withdraw (ByVal amount As Decimal) m_balance -= amount If m_balance < 0 Then m_notifyDlg(Balance) ' call the delegate End If End Sub Combining Delegate Objects A powerful feature of delegates is that you can combine them. Delegates are "multicast," in which they have an invocation list of methods. When such a delegate is called, all the methods on the invocation list will be called in the order they appear in the invocation list. The Combine method of the Delegate class can be used to combine the invocation methods of two delegate objects. The Remove method of the Delegate class can be used to remove methods from the invocation list. Dim custDlg As NotifyCallback = _ New NotifyCallback(AddressOf NotifyCustomer) Dim bankDlg As NotifyCallback = _ New NotifyCallback(AddressOf NotifyBank) Dim currDlg As NotifyCallback = _ NotifyCallback.Combine (custDlg, bankDlg) ... currDlg = NotifyCallback.Remove (currDlg, bankDlg) In this example we construct two delegate objects, each with an associated method. We then create a new delegate object whose invocation list will consist of both the methods NotifyCustomer and NotifyBank . When curr-Dlg is called, these two methods will be invoked. Later on in the code we remove the bankDlg delegate. Once this is done, the NotifyBank method is no longer in the delegate's invocation list, and the next time currDlg is called, only NotifyCustomer will be invoked. Complete Example The program DelegateAccount illustrates using delegates in our bank account scenario. The file DelegateAccount.vb declares the delegate NotifyCallback . The class DelegateAccount contains methods matching the signature of the delegate. The Main method instantiates delegate objects and combines them in various ways. The delegate objects are passed to the Account class, which uses its encapsulated delegate object to invoke suitable notifications when the account is overdrawn. Observe how this structure is dynamic and loosely coupled . The Account class does not know or care which notification methods will be invoked in the case of an overdraft. It simply calls the delegate, which in turn calls all the methods on its invocation list. These methods can be adjusted at runtime. Here is the code for the Account class: ' Account.vb Public Class Account Private m_balance As Decimal Private m_notifyDlg As NotifyCallback Public Sub New(_ ByVal balance As Decimal, _ ByVal dlg As NotifyCallback) m_balance = balance m_notifyDlg = dlg End Sub Public Sub SetDelegate(ByVal dlg As NotifyCallback) m_notifyDlg = dlg End Sub Public Sub Deposit(ByVal amount As Decimal) m_balance += amount End Sub Public Sub Withdraw(ByVal amount As Decimal) m_balance -= amount If m_balance < 0 Then m_notifyDlg(Balance) 'call the delegate End If End Sub Public ReadOnly Property Balance() As Decimal Get Return m_balance End Get End Property End Class Here is the code declaring and testing the delegate: ' DelegateAccount.vb Imports System Public Delegate Sub NotifyCallback(_ ByVal balance As Decimal) Class DelegateAccount 'Note: This is a Class, not a Module Shared Sub Main() Dim custDlg As NotifyCallback = _ New NotifyCallback(AddressOf NotifyCustomer) Dim bankDlg As NotifyCallback = _ New NotifyCallback(AddressOf NotifyBank) Dim currDlg As NotifyCallback = _ NotifyCallback.Combine(custDlg, bankDlg) Dim acc As Account = New Account(100, currDlg) Console.WriteLine("balance = {0}", acc.Balance) acc.Withdraw(125) Console.WriteLine("balance = {0}", acc.Balance) acc.Deposit(200) acc.Withdraw(125) Console.WriteLine("balance = {0}", acc.Balance) currDlg = NotifyCallback.Remove(currDlg, bankDlg) acc.SetDelegate(currDlg) acc.Withdraw(125) Dim da As DelegateAccount = New DelegateAccount() Dim instDlg As NotifyCallback = _ New NotifyCallback(AddressOf da.NotifyInstance) currDlg = NotifyCallback.Combine(currDlg, instDlg) acc.SetDelegate(currDlg) acc.Withdraw(125) End Sub Private Shared Sub NotifyCustomer(_ ByVal balance As Decimal) Console.WriteLine("Dear customer,") Console.WriteLine(_ " Account overdrawn, balance = {0}", balance) End Sub Private Shared Sub NotifyBank(_ ByVal balance As Decimal) Console.WriteLine("Dear bank,") Console.WriteLine(_ " Account overdrawn, balance = {0}", balance) End Sub Private Sub NotifyInstance(_ ByVal balance As Decimal) Console.WriteLine("Dear instance,") Console.WriteLine(_ " Account overdrawn, balance = {0}", balance) End Sub End Class Here is the output from running the program. Notice which notification methods get invoked, depending upon the operations that have been performed on the current delegate object. balance = 100 Dear customer, Account overdrawn, balance = -25 Dear bank, Account overdrawn, balance = -25 balance = -25 balance = 50 Dear customer, Account overdrawn, balance = -75 Dear customer, Account overdrawn, balance = -200 Dear instance, Account overdrawn, balance = -200 Stock Market Simulation As a further illustration of the use of delegates, consider the simple stock-market simulation, implemented in the directory StockMarket . The simulation consists of two modules: -
The Admin module provides a user interface for configuring and running the simulation. It also implements operations called by the simulation engine. -
The Engine module is the simulation engine. It maintains an internal clock and invokes randomly generated operations, based on the configuration parameters passed to it. Figure 6-3 shows the high-level architecture of the simulation. Figure 6-3. Architecture of stock-market simulation. The following operations are available: The following configuration parameters can be specified: Running the Simulation Build and run the example program in StockMarket . Start with the default configuration: Ticks are OFF, Trades are ON, Run count is 100. (Note that the results are random and will be different each time you run the program.) If you enter the command run , then the output shows columns of data for clock tick, stock, price, and volume. Ticks are OFF Trades are ON Run count = 100 Enter command, quit to exit : run 2 ACME 23 600 27 MSFT 63 400 27 IBM 114 600 38 MSFT 69 400 53 MSFT 75 900 62 INTC 27 800 64 MSFT 82 200 68 MSFT 90 300 81 MSFT 81 600 83 INTC 30 800 91 MSFT 73 700 99 IBM 119 400 : The available commands are listed when you type help at the colon prompt. The commands are count set run count ticks toggle ticks trades toggle trades config show configuration run run the simulation quit exit the program Delegate Code Two delegates are declared in the Admin.vb file. Public Delegate Sub TickCallback(ByVal ticks As Integer) Public Delegate Sub TradeCallback(_ ByVal ticks As Integer, _ ByVal stock As String, _ ByVal price As Integer, _ ByVal volume As Integer) As we saw in the previous section, a delegate is similar to a class, and a delegate object is instantiated by New . Dim tickDlg As TickCallback = _ New TickCallback(AddressOf PrintTick) Dim tradeDlg As TradeCallback = _ New TradeCallback(AddressOf PrintTrade) A method is passed as the parameter to the delegate constructor. The method signature must match that of the delegate. Public Sub PrintTick(ByVal ticks As Integer) Console.Write("{0} ", ticks) If (++printcount = LINECOUNT) Then Console.WriteLine() printcount = 0 End If End Sub Passing the Delegates to the Engine The Admin class passes the delegates to the Engine class in the constructor of the Engine class. Dim engine As Engine = New Engine(tickDlg, tradeDlg) Random-Number Generation The heart of the simulation is the Run method of the Engine class. At the core of the Run method is assigning simulated data based on random numbers . We use the System.Random class, which we discussed in Chapter 4 in connection with the ArrayDemo program. While (i < stocks.Length) Dim r As Double = rangen.NextDouble() If (r < tradeProb(i)) Then Dim delta As Integer = _ price(i) * volatility(i) If ( rangen.NextDouble() < 0.5) Then delta = -delta End If price(i) += delta Dim volume As Integer = _ rangen.Next(minVolume, maxVolume) * 100 tradeOp(_ tick, stocks(i), _ price(i), volume) End If i += 1 End While Using the Delegates In the Engine class, delegate references are declared: Dim tickOp As TickCallback Dim tradeOp As TradeCallback The delegate references are initialized in the Engine constructor: Public Sub New(_ ByVal tickOp As TickCallback, _ ByVal tradeOp As TradeCallback) Me.tickOp = tickOp Me.tradeOp = tradeOp End Sub The method that is wrapped by the delegate object can then be called through the delegate reference: If showTicks Then tickOp(tick) |