Mat DeLong just posted another great example of when not to abuse class helpers in Delphi (though I should add that he didn’t seem to see it that way). 🙂
But you don’t need helpers to do what this technique achieves, and in my view you really shouldn’t be using helpers for it in the first place.
When hacking around in ways that you really shouldn’t, dressing it up in the respectability of a (mis-used) language feature is asking for trouble. Not to mention that once you introduce a helper you must forever live with your fingers crossed that you don’t have in, or in the future happen to bring into, scope some other helper for the target class (and indeed that any 3rd party code you use now or in the future doesn’t do likewise).
I also believe this is almost certainly a bug in helpers as you should surely only have access to protected members from a helper, not private and certainly not strict private. Or so I would have thought and expected. Bizarrely this expectation is seemingly reflected in the Code Completion suggestions when referencing “Self” in a helper implementation, but is not enforced by the compiler.
Very odd. But perhaps cross some other fingers against the day that this bug – if such it is – is fixed. Just in case.
However, there is an alternative that doesn’t rely on mis-appropriating language features.
We have always (since Delphi 1!!) been able to reach private members of other classes by simply creating an overlay class and type-casting. Using Mat’s example, to gain access to the fComponentList member of a TDSCustomServer:
TDSCustomServerCracker = class abstract(TComponent) // :begin DSCustomServer member decls: protected FDbxContext: TDBXContext; private FStarted: Boolean; FConfig: TDSConfiguration; FServerMethodProvider: TDSServerMethodProvider; FComponentList: TDBXArrayList; FHideDSAdmin: Boolean; // :end DSCustomServer decl // Expose private members with some additional property declarations public property ComponentList: TDBXArrayList read fComponentList; end;
Now in your code, when you need to dig inside a DSComponent, hard-cast using your cracker class:
theList := TDSCustomServerCracker(someServer).ComponentList;
This works because your “fake” cracker class has the same layout in memory of its declared instance data, so you can use your cracker class to expose the instance data of some other class instance by hard casting.
Yes, this is more work and yes it’s much, much nastier. But what we are doing here is nasty. In my experience, as uncomfortable as it may make us feel, it is far better to be open and honest about hack-o-logy when we have to resort to it. Tarting it up and casting a veil of respectability over our misdeeds is just a pretense; a sop to our own ego’s (“I’m a professional developer, not a hacker!”).
Writing such code that draws attention to itself by laying it’s hackology out for all to see is absolutely the way to go in my view. Your future self will thank you, if no one else. 🙂
But over and above all of that, this also has the added advantage of being able to happily co-exist with any legitimate “helper” that is in scope (or is brought into scope in the future).
Class helpers are just wannabe extension methods. And like extension methods you have full access to the type you are extending. So I highly doubt accessibility of private members is a bug and will ever change.
Also you can have as much class helpers as you want. You just cannot use more than one in the same scope!
There is also some weirdness with inheriting from some class that has an class helper because then RTTI is generated for the class helper method or something like that – I cannot remember exactly.
And you really suggest using some ugly hack that relies on memory layout and will blow up whenever I change any field in this or some parent class at runtime instead of compiletime? Just wow…
@Stefan: Class helpers are themselves an ugly hack (and a facilitator for ugly hacks). The whole point of the post is that they are an ugly hack that masquerade as legitimate behind a facade of respectable elegance.
What I’m suggesting is that if you are going to hack then hack openly and honestly because when you come back to your code in years to come and are wondering why things aren’t working as you expect, the fact that you have deliberately breached the safety mechanisms in the language will be far more apparent and thus more readily identifiable as the potential source of your woes. It’s also more work to circumvent the safety’s in this way, which itself is likely to cause someone to think twice before reaching for it to solve some irksome problem.
When considering tinkering with the private details of a class you really had better have a _damned_ good reason. Making it easy to do so just make it more likely that people will use the technique without properly considering whether they should and that perhaps there may be an alternative. Hacks should be an unappealing last resort, not a neat, convenient solution.
Yes, the private member layout may change and break an overlay class, but when that happens at least you will be able to see quite clearly where your dependency and your vulnerability lies. Equally, the private implementation details of a class are subject to change willy nilly – there shouldn’t be any code that relies on a private implementation detail outside of a class itself. So “fComponentList” may be renamed, change type or even be removed completely, which will break a helper just as completely. But if/when they get such a compiler error I suspect that someone looking at a helper will be far less likely to consider that it is some dependency on the private implementation details of the helped class and waste far more time jumping at shadows and chasing wild geese before they land on the real source of the problem.
Yes it’s true, you can have as many helpers as you want as long as only one is in scope (any colour, as long as it’s black – LOL) – but the fact that only one can be in scope is a problem you expose yourself to if we accept that using helpers is a legitimate practice. The more helpers you have in the wild the greater the chance that one day the code that relies on one helper will be broken (or worse: behaviour changed) by the inadvertent introduction into scope of some other helper that you are perhaps aren’t even aware of.
Consider that simply adding a helper to a unit that I share with others risks breaking _their_ code! That, if nothing else, should send shivers down the spine of anyone considering the appeal of helpers.
I find it incredible that I *still* have to refer class helper proponents to the simple fact that the documentation for this feature specifically advises against using them – and always has, since their introduction. The reasons for that are very clear. Yet people insist on treating them as first-class tools in the tool box when they were never intended to be regarded as such.
I agree that the current implementation of class helpers are a hack.
Nonetheless the concept of extension methods (you might read about it on msdn if you are not familiar with them) is something great and can give you great benefits for clean and decoupled code. Of course on the other hand power does not come without responsibility but it is always up to the user how to use his tools. It’s not the tool that is evil, it’s what you do with it.
That being said I really hope we will see true extension methods in Delphi (also for interfaces and maybe even simple types) soon. It actually is not something difficult – it requires a bit of compiler magic and that’s it. It’s important to understand how they work in C# because actually an extension method cannot override an instance method while class helpers can hide instance methods which is one of the dangerous things about them.
But even among the Java and C# people there are some that don’t like them and call them evil. So I guess its just a matter of taste.
@Stefan: Extension methods might have some merit, but class helpers are not extension methods and if they were they could not be used to “solve” this problem: Extension methods cannot access private data in the extended class.
Consider that using a “helper” class in this way relies on your having access to the source of the “helped” class so that you can see what private members are available, since the IDE (rightly imho) does not expose those to you (unless someone implements the helper in the unit containing the “helped” class itself, in which case they need some serious help themselves). 🙂
In the case of encapsulation you are right – I thought they are the same in C# but if I think about it again, it makes sense.
If actually the purpose of the whole post is about that exact usage of class helpers I tend to agree but if it’s against their usage at all I don’t. Although the one class helper per scope limitation might give you some problems as you already explained. That’s why I hope they get implemented the “right” way in the future. Well there is a reason the documentation says: Class and record helpers provide a way to extend a type, but they should not be viewed as a design tool to be used when developing new code. For new code you should always rely on normal class inheritance and interface implementations.
Nice post! 🙂 I’d like to point out my blog post you mentioned starts off by saying: “I should start off by saying that with great power comes great responsibility. You can use this tip to gain access to private or strict private members of a class in another unit. But should you?”
I was just letting people know they *could* do it. Should they? Probably not. Your hack isn’t really “better”, and if the class being hacked adds another private field, it will mysteriously stop working. Not that the approach I suggested is any better… just making sure one doesn’t come across as more glamorous than the other.
Again, great post!
@Mat – thank you. Yours too. 🙂
I noticed your “great power/responsibility” disclaimer, but the biggest problem with class helpers is that two people can write perfectly responsible and well behaved class helpers but if one of those people then brings the other persons helper into a scope where they are expecting to use their own, their helper suddenly disappears. Class Helpers do have a legitimate use, but it is a very narrowly defined and highly limited use; it is the only use I put them to (and advocate). I intend blogging on that soon.
Developers can be well behaved and responsible all they like, but class helpers by their very nature (or perhaps rather, by their current design) are highly anti-social.
I think if you put class/record helpers into their own unit you cannot have the problem of getting them into your scope by accident because that requires adding said unit to your uses clause.
@Stefan – placing helpers in a separate unit makes it less likely that you will get into the situation accidentally, but now consider if I have written a class helper and I wish to use some 3rd party library which also contains a helper for the same class.
Yet again, *I* can be as safe and responsible as I wish, but if someone else, whose code I am using, just lumps their helpers into a unit with a bunch of other stuff that I might wish to “use” even if not interested in the helpers themselves, then again their code potentially screws up my carefully manicured code garden.
Further, if I *do* want to use both a helper of my own and someone else’s then I simply can’t. I have to either combine both helpers into a 3rd helper which amalgamates both (and ensure that only that 3rd amalgamated helper remains the one with higher scope precedence), or duplicate one inside the other.
Remind me again… class helpers were the “elegant” solution, yes ?
The advantage of class and record helpers over extension methods is that you can add anything but fields to a class or record
Extension methods are just that: instance methods added to the type. No properties, no class methods.
Both are not the nicest language enhancements I know of, but they do serve interesting purposes.
If we just had interface helpers… and parameterized interface methods.
RTTI anybody…
@Alister – good suggestion. The only potential problem being that it relies on the target class having been compiled with the necessary RTTI support.