C# Corner

Choose Between Methods and Properties

Language features aren't good or bad. The choice of language feature depends on what you're doing. In this issue, we dive into the methods vs. properties debate.

TECHNOLOGY TOOLBOX: C#

A quick Web search on any number of language features will turn up numerous blogs and forum posts espousing that some feature of some programming language is pure evil and should be avoided at all costs. I might agree in the case of GOTO, but just about every other feature of every modern programming language has some use. For example, I've been getting some questions recently about whether properties are a bad idea, and whether developers should use methods instead. The short answer is as clear as mud: Sometimes you should use properties, and sometimes you should use methods. The longer answer is that you must live up to your client developers' expectations. The developers that use your class will view a method call differently than they'll view a property access. You can see the fact that properties and methods are the same because you're the one building the class, but your users see the world differently. Properties look like data and the developers that use your class will expect those properties to behave like data. This column will explore those expectations, and show you how to best determine when you should implement functionality as a property or as a method.The most obvious expectation is that users will believe the observable state for your object won't change simply because they've accessed a property. Retrieving a value from an object shouldn't make any changes to the object. Property getters should have no reason to change an object's state. You'd never write this property:

// Obvious bad example.
private int numberofAccesses;
public int NumberOfAccesses
{
    get
    {
        return ++numberofAccesses;
    }
}

But real problems like this are much more subtle. Consider writing this property with some extra code to keep track of the number of accesses:

// this seems ok.
private int numberofAccesses;

private int someValue;
public int SomeValue
{
    get
    {
        numberofAccesses++;
        return someValue;
    }
}

Assume the enclosing class should utilize value-based semantics. You'd probably add code like this:

public class BadPropertiesSample : 
    IEquatable<BadPropertiesSample>
{
    private int numberofAccesses;

    private int someValue;
    public int SomeValue
    {
        get
        {
            numberofAccesses++;
            return someValue;
        }
    }
    
	#region IEquatable<BadPropertiesSample> Members
    public bool Equals(BadPropertiesSample other)
    {
        return (other.someValue == someValue) && 
            (other.numberofAccesses == numberofAccesses);
    }
    #endregion
}

This breaks a user's expectations. What do you suppose this code fragment produces for output?

BadPropertiesSample s1 = new BadPropertiesSample();
BadPropertiesSample s2 = new BadPropertiesSample();

Console.WriteLine(s1.Equals(s2));
Console.WriteLine(s1.SomeValue);
Console.WriteLine(s1.Equals(s2));

This code writes "true", "0", and then "false". Examining a property changes the type's concept of equality, which breaks a user's expectation for the type. Somehow, examining a property changes the object's state in a suprising manner. That's counterintuitive, and it's going to cause client developers lots of headaches. In fact, this points to one of the few design flaws in the .NET Framework.

DateTime.Now violates similar principles. Repeated calls to the same property, without intervening calls, should return the same value. In the case of DateTime.Now, that's not true. The return from DateTime.Now changes with time, not with any explicit code changes to some static property of DateTime.

Client developers always hold the expectation that calling a property getter won't change the object's state. Repeated calls to a getter will always return the same value. You shouldn't do anything to violate those expections.

Performance Expectations
Developers using your type will also have expectations about the performance of properties. Property getters shouldn't perform lengthy operations to calculate the return value. Consider a simple loop around an array:

for (int index = 0; index < numbers.Length; index++)

Internally, the Length property is stored in the Array object; it returns the cached value. But, as an alternative, assume the Length property was implemented by counting the number of values in the array. That would make the Length property performance change linearly with the length of the array. In this loop, the Length property is called once for every element in the array. That means the Length property is called N times for an N length array, so the loop would have quadratic performance (N x N) if the Length property were implemented by counting the values in the array. That's going to be such a problem that users would never use that feature. It's not implemented that way in the .NET Framework. The Length property is implemented in such a way that it has constant, usually quick performance.

You could run into this problem if your property accessors do too much work. This could be local code that performs lengthy calculations to compute a property value. Or worse, you could implement property get and set accessors to load or store data from a remote data store, such as a database. That would cause your client developers even greater headaches. Retrieving a value would access network resources, access a database, retrieve values across the wire, and then provide you with the answer. Think about how you and your peers use properties: You would never think that a property accessor did that much work. It's counterintuitive. The same is true for setters. You probably assume that some validation takes place when you update a property value, but you probably don't expect network access, possible concurrency exceptions, or network access exceptions. Again, the problem is that you've violated your client developers' expectations. They'd believe that a property accessor, either for get or set, would be a quick, local action. Instead, you've performed a mountain of work underneath what looks like a simple act.

Consider Alternatives
Please don't take these examples as a global recommendation against properties, or even against executing code in property accessors. Rather, it's a strong recommendation to consider methods over properties when a property's implementation would lead a developer to get results different from what he would normally expect. You can and should consider lazy evaluation for property getters. You should add appropriate validation to your set accessors. It's when that validation, or lazy evaluation, becomes unwieldy that you violate your client developers' expectations.

Think about all the counter examples I gave earlier. Any of those would make reasonable methods. Developers using your class would expect that a method called "SaveChangesToDatabase()" would require network calls and might have concurrency issues. Setting MyType.SomeValue doesn't carry that same expectation. Developers using your code will write different access code for the method and the property. The same assumptions are true for a method named "LoadRecordCountFromDatabase()" and a property named MyCollection.RowCount. The first implies the performance metrics associated with a network call and database access. The second doesn't.

Note that the change from a property to a well-formed method name also implies different behavior. I mentioned in the opening that users expect repeated calls to the same property getter will always return the same value, and should not change any state. Your expectations for a method called "LoadRowCountFromDatabase()" carries different expectations. You expect that repeated calls might return different values. For example, other users might have added or removed rows. You would also expect that it might fail if the network were unavailable. The same analogy exists for the save methods. Developers using your code will consider many different ways that a save method to a database might fail. There could be concurrency failures, network failures, or validation errors. Properties don't carry those expectations.

Properties and Method members both have proper places in your class design. Just remember that your client developers will have different expectations for each, and your job as an API designer is to ensure that the implementation lives up to those expectations.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube