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:
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.
Why?
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:
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.