Monday, October 5, 2009

Static and Virtual?

  A few times while reading developer forums I've come across people asking if it possible to have a class member which is both static and virtual. The obvious answer is 'no'. Imagine a hypothetical static and virtual method named Foo. Because static members belong to no particular instance, when we say BaseClass.Foo() there is no way to determine whether we really meant to invoke DerivedClass.Foo(). Therefore it makes no sense to be both static and virtual.

  All very interesting, but how does it apply to the Z-machine? I wanted to be able to invoke a particular operation by using a simple array of delegates and accessing them by index (the opcode number). While the eight different versions of the Z-machine define different operations, all instances of a particular version have identical sets. Because of this and the fact that I implement each version as a class, the array of operations for a given version would logically be a static member of that version's class. This led to the situation of eight different static arrays, each one defined in a different class. This was wasteful because they have more operations in common than they have differences. Additionally it was ugly because it forced me to wrap the call to the delegate in a virtual method to ensure the proper array was accessed. Yuck.

  After thinking about the situation a little I thought it would be nice to have a static array of delegates to virtual methods, then I could just use a single array in my base class. After unsuccessfully trying to do just that, I ended up creating a single static array in my base class populated with delegates which take a ZMachine parameter and that call a virtual method on the instance passed to it to ensure the proper operation is invoked. This was ugly too because it involved a wrapper method to call the virtual method for each operation. I was able to use lambda syntax to clean it up quite a bit.

The result in my abstract ZMachine class looks like this:

protected delegate void Operation(ZMachine machine);

private static readonly ImmutableArray<Operation> operations = InitializeOperations();

private static ImmutableArray<Operation> InitializeOperations()
  var op = new ImmutableArray<Operation>.Builder(256);
  op[0] = op[32] = op[64] = op[96] = op[192] = z => z.Op0();
  op[1] = op[33] = op[65] = op[97] = op[193] = z => z.Op1();

  op[254] = z => z.Op254();
  op[255] = z => z.Op255();
  return op.MakeImmutable();

  The methods Op1, Op2, etc. are virtual methods in the abstract base class with the default behavior of calling an abstract method called InvalidOperation, allowing derived classes to implement their own handling for any illegal operations as well as override only those operations which they care about.

  Overall I'm pleased with the result. I ended up with a single array of delegates to invoke, and virtual operations to override, with the only wart being the extra method call using the lambdas.


  1. Static virtual methods are a reasonable thing to look for if you come from the Delphi world, where "class methods" are similar to static methods but don't have to be statically bound.

    A virtual class method is useful in Delphi because metaclasses (roughly equivalent to System.Type) form a parallel hierarchy. So if you have a variable whose type is "class of TObject", it can hold a reference to the class TObject or any class derived from it, and you can use that variable to call any of the class methods exposed by TObject -- which might be overridden if the variable holds a reference to a derived class. Even constructors can be called this way, so you get the factory pattern for free.

    Unfortunately, this is one of Delphi's ideas that wasn't copied in .NET. There is only one System.Type, so all Type instances expose the same members no matter which types they represent, and the only way to call a static method given a Type instance is to use reflection.

  2. You do know that delegates can be replaced with Func<>s or Action<>s, and it makes for easier code manipulation, right? Plus, you can stuff regular functions into them, delegates, lambdas....

  3. It is common in emulators to use large switch statements for this sort of thing. I can see the appeal of using delegates and inheritance, but, like the random number generator class you described previously, this smacks of over-engineering and trying to be clever. The fact that you're having to explain it is quite revealing.

    You say, "I wanted to be able to invoke a particular operation by using a simple array of delegates and accessing them by index", but you don't explain _why_ you would want to do this.

    Why not KISS?

  4. I don't know if both anonymous posters are the same, so I'll address each separately:

    First poster - I am aware of Func and Action, but I don't see how they would simplify anything here. That may be because I don't have a lot of experience with functional programming and often have trouble thinking that way. If you could post an example of a simpler, cleaner way using those classes, I'd be interested in seeing it.

    Second poster - The fact that I am explaining anything is the entire point of this blog. Believe it or not my original design did use a huge switch statement here. It was really ugly and that was my main motivation for the array. I think the array is very clean, efficient and has one aspect I really like: In the Z-machine certain opcodes are repeated but with different operand types, e.g Ops 0-31 occur five times, while 128-143 occur three times. The InitializeOperations() method lays out the opcodes in a way that this pattern is obvious. You can't see it here because I left out the bulk of the operations.
    As for the random number generator, I don't see how it is over-engineered. It seems a pretty simple class to me. If I were just writing an interpreter, making it abstract would be unnecessary, however that is not my goal. To replace my (completely arbitrary) algorithm choice requires inheriting from the base class and overriding a grand total of two methods. How is that complicated? Would you recommend I hard code the algorithm? Why?

  5. My favorite way to set up tables of methods like this is to decorate the method with attributes containing the opcode numbers (and names, special rules, etc.) and then use reflection to fill in the array, dictionary, or whatever runtime structure you need. Easier to maintain because everything about each opcode is in one place.

  6. That's a nice use of reflection. I've avoided reflection mostly because I do run this on Xbox360 where a lot of the reflection namespace is absent.

  7. "Because I use inheritance to implement the various versions, the array of operations would logically be a static member."

    I don't understand this statement. The two clauses seem to be completely unrelated. Why should anything in this example be static at all?

  8. I worded that badly. The sentences before and after play a part as well.

    The set of operations is static because:
    1) All Z-machine instances of a given version contain an identical set of operations. For example: Any given Version 3 machine has the exact same set of operations as all other Version 3 machines.
    2) I implemented each version as a separate class. (inheritance isn't really a factor, just the fact they are separate classes is)

    In this case it makes no sense for the set of operations to be an instance member since it will be identical for every instance.

    I didn't like the idea of having separate arrays for each class because of the large amount of duplication between them, so my final solution reduced this to one static array in the base class.

  9. I edited my post to make my intent a bit clearer.

  10. (Same anonymous as second comment) Unless you specifically need the ability to trigger lists of operations at a time, Func>s and Actions can be passed around like regular variables and are generally eadier to use. For instance, in a hobby project which i'm working on (a very simple virtual machine), i use an int keyed Dictionary of Funcs in one case to pipe data out of my machine. It would be much more difficult with delegates, especially since i want a large number of these pipes.