I can’t say I’ve asked the framework guidelines folks about this but I’m fairly sure there would be a lot of agreement from the guidelines gurus; so in the spirit of approximately correct advice I give you Rico’s Guidelines for Performant Properties.
I should start by saying I wish more people just used fields instead of committing these terrible sins with their properties. If we had the notion of a type-safe, read-only field the world would be a better place. Alas…
So, you’re using a property, the most important thing to remember is that it will seem very much like a field in all ways. It looks like a field and feels like a field, people will expect it to perform like a field. So with that in mind:
- Accessing the property should not allocate any memory; people are going to use this thing in loops walking over whatever data structure you are presenting, they expect that this data is sitting around and being accessed.
- Accessing the property should not “synchronize” – if there is any locking to be done it should be done at a higher level than a single field, by the time you’ve acquired whatever object has the property on it, it should already be safe to read it.
- Accessing the property should not do any I/O, especially not any network I/O, again see above, by the time you’re reading the property the object offering it should have done whatever I/O was needed.
- Accessing the property should not be an operation with complexity greater than O(1) – that means no loops. At all. You could haggle me as high as O(lg(N)) if we’re talking about an property that is an “indexer” but no higher.
- Accessing the property should not have side-effects (i.e. it’s strictly a read operation, it changes nothing)
What you’re left with is you can use your object state and the argument (if an indexer) to do a constant-time lookup, or log-time at worst, in an already existing data structure and immediately return the result. That’s it.
Pit of Success is the only reason you need. In my opinion, patterns more complicated than the above are doomed to fail. If you allow say network access, an RPC or the like, on each property fetch, then you promote things like field-at-a-time access to remote data. Not only is this astonishing to users of the API it is incredibly inefficient. If you follow the rules above you soon realize that you must allow some kind of query to get the data you need and then you can use properties all you like to access that data. That is a much better pattern, it leads to success.
Remember that if you allow things like I/O or synchronization you can easily get property access times that are measured in milliseconds, this simply will not do. A typical interactive scenario might have a budget of say 100ms for prompt response and it might require access to several dozen properties just to paint the screen properly. This has several issues:
- If property access is say 1 millisecond then accessing 10 properties (not at all crazy) would be 10% of your overall budget – that’s nuts!
- The interactive code must be able to use your properties with confidence, that means they cannot block and should not be subject to large variability, else you get UI that is non-responsive for extended periods of time
Frequent access to properties is very common and therefore must be astonishingly cheap if only to keep a handle on the Joules your program consumes – an increasingly important metric for modern platforms.
From a time perspective, I like to see properties that have access time measured in nanoseconds. Let’s say 10^-8s. I can reasonably see systems with tricky indexing that might go has high as 10^-7 or even 10^-6. When your time is getting to be 10^-5 or larger, you really need to start thinking about your design. That’s too much work to put inside a thing that looks like a field access. Give your users a fighting chance to understand where their costs are and let them query for a batch of results, probably asynchronously and then access them quickly when they arrive. That’s going to be a successful pattern.
It goes without saying that under these conditions, the flexibility to version/replace/subtype virtual-call properties is actually illusory. If you were to try to do something new and costly, or even just very different in a subtype you would likely cause all manner of problems for callers. So while you do get to change things a somewhat, you cannot and must not, use that flexibility to access new and different subsystems or the like inside of a method that is supposed to be as small as a getter. You are necessarily very constrained.
When I consider all of this I arrive squarely at the conclusion that more fields and fewer properties would be better. Don’t hide real work inside a property and don’t use them as a synchronization tool. Once you decide that you need a fetch/read pattern you soon find that that reading thing can be very simple/fast indeed and that perhaps fields were just fine after all.
Something to think about anyway.