- Introduction to using classes with AVR for .NET
We've noticed in tech support that customer interest seems to move in waves. One week it seems that everyone wants to know about data grids, then the next week everyone wants to know about open query files. It's almost like our customers have a secret meeting on Friday afternoon and then spring the new topic on us all at once the following Monday.
Lately, I've noticed the Friday meeting must have been about using AVR for .NET's OO capabilities to add power and sophistication to their programs. I think one of the reasons so many customers are riding this wave is the number of AVR Classic customers moving to AVR for .NET. The advent of Windows 10, and the realization that COM won't last forever, has probably provided some motivation for this.
We have a several-years-old AVR class workbook that has two important chapters on using classes with AVR. Chapters 2 and 5 are required reading for anyone moving to AVR for .NET from any procedural language. That's the good news. The bad news is that while the material isn't really technically dated, it needs to be on the Web and it needs a bit of a refresher (it has some typos and its original format doesn't lend itself well to conversion to HTML). So, that's what we're starting here. This is the first in a new series of articles about using classes and some OO concepts with AVR. Some of these articles will be featured in the ASNA newsletter, but they'll also be posted as regular content in our new knowledge base. Keep an eye on that section of ASNA.com for more articles in this series.
The video version
For better control and resolution view the video directly at YouTube
The text version
Unlike AVR Classic or green-screen RPG, AVR for .NET is built on an object oriented foundation. In AVR for .NET (for the rest of this article "AVR" refers to AVR for .NET), classes define a "program boundary". That is, variable scope (which includes disk files and their record formats) is contained within a class--unless you specifically declare a variable public.
In .NET, some classes present themselves implicitly. For example, in Win or Web forms, each form gets its own class (often called the "code-behind" or "code-beside" class). Think of these classes as structural. You can create a pretty good AVR app using nothing but these implicit structural classes and procedural code. However, for non-trivial applications, putting all of your code into these structural classes makes a challenging, hard-to-test and hard-to-maintain application. You end up with a boar's nest of subroutines, event handlers, and virtually no distinction between business logic, file IO, and UI management.
One of the things many AVR coders could do to improve their applications is to use more classes to separate, or partition, distinct parts of the app from each other. We'll dig more into the motivations for partitioning in a moment, but first, let's have a class refresher course. In academic parlance, a class is a template (or blueprint) for an instance of an object. The class provides data and, usually, actions to perform on that data. It "encapsulates" that data and those actions from the rest of the application. This separation of concerns improves maintenance, reuse, and testing by imposing a strict boundary between the class and the rest of the app (its other classes). The only data and/or actions that a class makes available to other classes are those members that you explicitly expose. Let's look at a small example.
A simple class
Figure 1a below shows a very simple class with two public properties (Sales
and Returns
) and one private field (Rank
). The Sales
and Returns
values are available to the outside world; the Rank
value is only visible inside an instance of the class. The Access
keyword controls member visibility. *Public
members are available outside the class instance, *Private
members are not. You'll often see using private members referred to as data hiding. While that might sound like a bad thing, it's a good thing. Private members do work inside the class and their values don't need, and shouldn't be, visible or changeable outside the class.
BegClass SalesData Access(*Public)
DclProp Sales Type(*Packed) Len(12,2) Access(*Public)
DclProp Returns Type(*Packed) Len(12,2) Access(*Public)
DclFld Rank Type(*Integer4) Access(*Private)
EndClass
Figure 1a. A small class with data members only.
In AVR Classic or green-screen RPG, you would have probably used a multi-occurrence data structure or a data structure array to store values. As you'll see, using a class, rather than these old-school storage techniques, adds considerable possibilities.
The code below in Figure 1b shows how to use an instance of the SalesData class. Note the word "instance." That's important. Your code doesn't work with the SalesData class, rather it works with an instance of it. The SalesData class is an ethereal definition of data (and, optionally actions) and it doesn't contain data. An instance of it does.
DclFld sd Type(SalesData) New()
sd.Sales = 45
sd.Returns = -17
Figure 1b. A small class with data members only.
In the code in Figure 1b, the instance of SalesData
is named sd
. The New
after its declaration causes an instance of SalesData
to be created in memory. That instance is assigned to sd
. Access to a class instance's public members is available through a fully qualified reference; ie, sd.Sales
allows access to this instance's Sales
member. For all intents and purposes, what we've created here is a special case data structure. As written it doesn't seem to offer much of a benefit over a traditional data structure.
Passing class instances
A benefit of a class instance over a traditional RPG data structure is that class instances can be passed as arguments to subroutines and functions. This allows you to dispatch the global nature of a traditional data structure. Before we dig into passing a class instance as an argument, let's review passing scalar numeric types.
By default AVR for .NET passes arguments to functions and subroutines by value. This ensures that when you pass an AVR intrinsic scalar value type (*Integer4, *Zoned, *Packed, etc) changes made to passed arguments are not seen by the caller. For example:
DclFld y Type(*Integer4)
...
y = 55
MySubr(y)
// y is still 55 after the call.
...
BegSr MySubr
DclSrParm x Type(*Integer4)
x = 5
EndSr
Value types (which, with one exception are all numeric) directly contain a value. Instances of classes you create with AVR are reference types, where the instance variable contains a reference to the class instance. For example, in Figure 1c below, the sd
variable contains a reference to an instance of SalesData
. Surprisingly, when you pass the sd
instance by value, changes made in the called routine are reflected in the caller. Notice how sd.Sales
is 450 after the call to TestByVal
. What you're passing in this case is a reference to the class, not a direct value or values.
DclFld sd Type(SalesData) New()
sd.Sales = 45
sd.Returns = -17
TestByVal(sd)
// sd.sales is now 450.
BegSr TestByVal
DclSrParm sd Type(SalesData)
sd.Sales = 450
EndSr
Further, if you pass sd
by reference, changes made in the called routine are also
reflected in the caller. For example, in the code below, sd.Sales
is 450 after the call to TestByRef.
DclFld sd Type(SalesData) New()
sd.Sales = 45
sd.Returns = -17
TestByVal(*ByRef sd)
// sd.sales is now 450.
BegSr TestByRef
DclSrParm sd Type(SalesData) By(*Reference)
sd.Sales = 450
EndSr
The full reason for this behavior is beyond the scope of this article, but is related to how value types and reference types are stored in the stack and the heap. Read this article for more info. But don't agonize over it very much, just understand the implications.
The notion that members in a class are exposed as you pass a class instance around is both a good thing and a bad thing. It's a good thing because you can pass around a class instance with lots of data in it effectively because you are simply passing a reference to the class instance around. However, as your programming antennae probably tells you, it's also a potentially bad thing because any routine to which you pass the class instance has the right to change public values in that class instance.
The upshot? Code your class member visibility carefully, making sure to only make public those members that truly need to be public. Remember, too, that AVR arrays are reference types, so this warning applies to them, too.
Note also that while strings in .NET are reference types, they are exempt from the exposure just discussed. AVR's *Char
is implemented as a System.String. System.String is a reference type, but it is a special-case reference type. Most reference types have to be explicitly instanced and most reference types can't be passed by value; *Char
and its kissing cousin System.String
are reference types that obey value type semantics. That is, they don't need to be explicitly instanced and they can be passed by value.
Methods give classes actions
A modified SalesData
class is shown in Figure 1c with an added member, GetNetSales()
. This is a function that returns the sum of Sales
and Returns
.
BegClass SalesData Access(*Public)
DclProp Sales Type(*Packed) Len(12,2) Access(*Public)
DclProp Returns Type(*Packed) Len(12,2) Access(*Public)
DclFld Rank Type(*Integer4) Access(*Private)
BegFunc GetNetSales Type(*Packed) Len(12,2) Access(*Public)
LeaveSr *This.Sales + *This.Returns
EndFunc
EndClass
Figure 1d below shows GetNetSales()
being called to return the net sales. While this calculation is very simple, in the real world it represents a complex calculation. With this computation power added to the SalesData
class, that class is now much more powerful than a traditional data structure. In this case, it is a data structure smart enough to total itself. Not a ton of smarts, for sure, but 100% more than any old-school data structure has!
DclFld sd Type(SalesData) New()
DclFld NetSales Type(*Packed) Len(12,2)
sd.Sales = 45
sd.Returns = -17
NetSales = sd.GetNetSales()
Enough for now
That's plenty to absorb right now. As we move forward, the article in this series will focus on delivering something tangible with AVR and classes. File IO is one place where a great payoff is available by the effective partitioning of your code into classes. Check back soon for part 2.
- Introduction to using classes with AVR for .NET