Not only do AVR for .NET shared class members generate a lot of questions for us in ASNA tech support, but their misuse cause AVR for .NET programmers grief and consternation. Let's a look at exactly what shared members are and when to use them.

Consider the need for a set of reusable math functions (beyond those that the System.Math namespace already provides). In our case, we need a Squared method which returns the squared value of an 32-bit integer (an academic example indeed, but it works for our purposes!).

A way to write and use the Squared method is shown below in Figure 1:

DclFld MyMath Type(MathFunctions) New()
DclFld Result Type(*Integer4) 
   
Result = MyMath.Squared(5) 

...

BegClass MathFunctions Access(*Public)
    BegFunc Squared Access(*Public) Type(*Integer4) 
        DclSrParm x Type(*Integer4) 

        LeaveSr x * x 
    EndFunc 
EndClass

Figure 1. The Squared function as an instance member.

In the Figure 1 example, Squared is an instance member of the MathFunctions class. Instance members belong to class instances, not to the class itself. In this example, Squared belongs to the MyMath instance of the MathFunctions class.

In this example, consider the attributes of the Squared function:

  • Its return value is guaranteed to be same, every time called, with a given argument value. That is, if you call it six times with the argument 5 each call returns the value 25.

  • The Squared function has zero side effects. It doesn't change program state in any way. The Squared function is a pure black box, give it an input and it returns the same output every time.

A function meeting these two attributes is known as a pure function.. Pure functions are superb candidates for being shared members. Consider the code below in Figure 2.

DclFld Result Type(*Integer4) 
   
Result = MathFunctions.Squared(5)

...

BegClass MathFunctions Access(*Public) 
    BegFunc Squared Access(*Public) Type(*Integer4) Shared(*Yes) 
        DclSrParm x Type(*Integer4) 

        LeaveSr x * x 
    EndFunc 
EndClass

Figure 2. The Squared function as a shared member.

In Figure 2 above, the Squared is marked with the Shared(*Yes). A shared member belongs to the class, not instances of a class. Shared members provide the shortcut of not needing to instance their owning class to use them--which is the primary payoff of a shared member.

VB.NET also has a shared keyword which works exactly like AVR's Shared(*Yes). In C#, however, a shared member is marked with the static keyword. Otherwise, though, the behavior a static C# class member is identical to an AVR for VB.NET shared class member.

Shared members rules-of-thumb

  • Don’t make DclDB, DclDiskFile, or DclMemoryFile objects shared. Although its legally syntactically, behavior of these IO-related objects when shared is quite hard to predict.
  • All public constants in a class are implicitly shared. Constants are, for all intents and purposes, the most pure functions ever. A constant never fiddles with program state and a constant always returns the same value.
  • All shared members are shared by all users of Web applications. Ponder that statement a minute. It means that if you have a shared counter field in a Web app, that value is being shared by all users of the Web app! Probably not what you intended! Except for packaging library routines, be especially wary of shared members in browser-based applications.
  • In any one class, shared members can only reference other shared members. Shared members cannot reference instance members from the same class.
  • Unlike C# and VB, AVR for .NET doesn't allow the Shared(*Yes) attribute at the class level; it is allowed in AVR only at the member level.

The most common mistake with shared members

A common mistake with shared members is to attempt to use them as a way to make class members globally available to other parts of the project.

In .NET, once a project has a reference to class, that class is available to all other classes in the project. Classes can be considered fully globally in your project. Even if a class is identified with a namespace other than what the project namespace is, that class is available anywhere in the project with a fully-qualified reference.

For example, consider putting a MyFuncs class in a Utilities namespace, as shown below in Figure 3.

DclNamespace Utilities

BegClass MyFuncs
    ...
EndClass

Figure 3. The MyFuncs class inside the Utilities namespace

In any class in the project, the MyFuncs class is available by either fully qualifying the class name or with a Using statement, as shown below in Figure 4:

Using Utilities

BegClass Form1 
    // Getting access to the MyFuncs functions
    // class via the `Using` statement. 
    DclFld mf1 Type(MyFuncs) New()

    // Fully-qualified access to the MyFuncs class.
    DclFld mf2 Type(Utilities.MyFuncs) New()
EndClass 

Figure 4. Two ways

The point is that classes are global to a project and available anywhere in a project. However, class instances are not global. For example, in Figure 4 above, the class instances mf1 and mf2 are only available to the Form1 class instance. This leads us to this axiom:

Classes are available globally in a project; class instances are localized to the class in which they are declared.

What does this have to with shared variables? Let's say that you've declared an instance of MyFuncs in Form1 as mf1 (as above in Figure 4) and now you need that instance available in Form2. To achieve this you need to pass the mf1 reference to Form2. This takes a little thought and care. You could, for example, customize Form2's constructor and pass the mf1 reference to this constructor or you could pass the mf1 instance as an argument to a function.

Some AVR coders, especially newer ones, get a little waylaid by this need to pass class instance references around and just give up and make the members in MyFuncs shared. This is, at least first, great because shared members are easily available because the class (not the class instance) is easily available. This solution usually breaks down quickly though because in Form2 what you really need is access to instance data (ie, non-shared members) from the mf1 instance--and shared members can only reference other shared members--they cannot reference instance members.

This leads us to another axiom:

Don't attempt to use shared members to break what are otherwise OOP's rigid rules of scope. Rather, engineer good ways to pass the class instances around as needed.

Ideal candidates for shared members

The generally qualifications for using a shared member in your code is pretty short:

  • Constants (which is provided automatically for you)
  • Pure functions (like date or time functions). You'll notice that all of the methods in the .NET System.Math class are shared (or static)--because they are all pure functions.

Beyond these qualifications, there are others but they exceed the scope of this article. One example of this is the Factory pattern.

Which brings us to our third axiom:

If you can’t state clearly why a member should be shared, you’re probably sharing it for the wrong reasons. (This axiom is often couched under the larger programming axiom: "If you don't know what you're doing, don't do it!")

Summary

Use shared members where they fit and make sense--but don't use them to get out from under a scoping issue. Making a member shared may indeed be a quick band aid to make a member visible to other classes, but more times than not, that quick-fix mentality comes back to bite you!

Remember also that shared variables in a Web app can be the path to pain and heartache. I helped rewrite an AVR ASP.NET app once where a frustrated developer gave up and made every member of a class shared. Every user of that Web app was working against the same shared data. There wasn't any correct data in sight!



Please login or create an account to post comments.