Understanding Static Attributes and Methods in C#

In object-oriented programming languages, static attributes and methods serve as a means to establish class-level members that are independent of particular object instances but are linked to the class as a whole. This article offers a basic overview of the concept of static in .NET, encompassing its application, memory allocation, and its impact on program execution.

Introduction

Objects are essentially instances of classes that come to life at runtime, thanks to the new operator. Each object carries its own set of instance variables, representing the object's unique attributes or state. These instance variables are exclusive to each object, and any changes made to one object's attributes will not affect the attributes of other objects belonging to the same class.

On the other hand, the concept of static, manifested through the static keyword, is used within a program to qualify either attributes or methods and signify that they are related to the class itself rather than being tied to individual object instances. As a result, static members exist independent of whether objects of that class have been instantiated.

To gain a deeper understanding of how static members are used, let's delve first into the memory management aspects associated with them.

Memory Management Overview: MethodTable

In a previous article, we explored the details of memory management within the .NET framework, specifically focusing on how the stack and heap handle different data types, such as value types and reference types. However, whilst we covered the allocation of data associated with a program, one key aspect we did not touch upon was the storage and execution of program code during runtime.

In the .NET ecosystem, programs are compiled into what is known as Common Intermediate Language (CIL) code, which is then stored in assembly files. This CIL code represents the program's logic and instructions, including the code definitions of all the classes and methods.

As the figure below shows, when a .NET program is executed, different memory heaps are used in addition to the Managed Heap discussed in the context of data storage. These are known as the Loader Heaps. Unlike the Managed Heap, the Loader Heaps are not touched by garbage collection and serve as a repository for information that remains constant throughout the entire lifecycle of a program.

Among the Loader Heaps, the HighFrequencyHeap stores a set of the above information accessed frequently during a program execution, such as a MethodTable for each class defined within that program. A MethodTable contains common class metadata the .NET CRL uses to create objects of that class. These metadata also include references to the location of the CIL code of the methods the CRL loads from the program's assemblies when the program execution commences.

As mentioned at the beginning of this article, static attributes are shared among all instances of a class and exist regardless of whether objects of that class have been instantiated. Therefore, they find their place within the MethodTable, as they are connected to the class rather than specific object instances. Their storage location within the MethodTable emphasises the class-level nature of static attributes and their lasting presence throughout a program's execution.

Static Attributes

Let's illustrate the memory allocation process of static attributes with the example shown in the following code snippet:

class BankAccount
{
    private string number;
    private double balance;
    private static int accountsCreated = 0;

    public BankAccount(string num, double bal) 
    {
        number = num;
        balance = bal;
        accountsCreated++;
    }

    public int GetAccountsCreated() 
    {
        return accountsCreated;
    }

    // all other BankAccount methods: Deposit, Withdraw, etc.
}

class Program
{
    public static void Main() 
    {
        BankAccount account1, account2;
        account1 = new BankAccount("A0123", 500.5);
        account2 = new BankAccount("BD324", 100.0);

        account1.GetAccountsCreated();
        account2.GetAccountsCreated();
    }
}

When the above program is started, and the first instruction of the Main is executed, the MethodTable of the BankAccount class is created inside the HighFrequencyHeap of the Loader Heaps. As mentioned, the MethodTable includes metadata required to create object instances of that class. It also includes references to the location of the CIL code for the class' methods' definitions and to the corresponding JIT-compiled native code generated as those methods are invoked.

As the figure below shows, since the above BankAccount class defines a static attribute accountsCreated, space for this class member will also be allocated on the MethodTable, even before the creation of the two objects referenced by account1 and account2.

Inside the constructor of the class, the content of the static attribute accountsCreated is incremented every time a new BankAccount is allocated on the Managed Heap. Because accountsCreated is a static attribute that is shared among all the BankAccount object instances, all these object instances will reference the same memory location of the MethodTable where the attribute is stored. As a result, accountsCreated can be used as a counter that keeps track of the number of BankAccount objects created since the beginning of the program execution.

When at the end of the Main inside the Program class, the method GetAccountsCreated is executed on each of the object instances referenced by account1 and account2, the return value of those invocations will be identical. Again, this is due to the content of the static accountsCreated attribute being shared among those instances and not being specific to each of them.

Static Methods

Like static attributes, static methods are behaviours inherently linked to the class where they are defined, which can be invoked without requiring an object instance of that class. static methods do not operate on instance variables (attributes) of an object because they are not bound to a specific object instance and do not have access to an object's state.

As a result, they can only access static attributes and methods of the same class or static members exposed by other classes. They can, of course, operate on value types and reference types received via their parameters and on their locally defined variables.

Utility Methods

The first common usage of static methods is to implement behaviours that do not depend on the state of individual objects but rather perform operations on parameters or other static members. An example is shown in the following code snippet:

public static class CalcManager
{
    public static bool IsEven(int n)
    {
        return n % 2 == 0;
    }

    public static int Cube(int n)
    {
        return n * n * n;
    }

    public static double Add(double[] x)
    {
        double sum = 0.0;
        foreach (double e in x)
            sum += e;
        return sum;
    }
}

class Program
{    
    public static void Main() 
    {
        int number = 3;
        double[] values = { 0.4, 3.5, 7.8, 0.5 };

        Console.WriteLine(CalcManager.IsEven(number));
        Console.WriteLine(CalcManager.Add(values));   
    }
}

The CalcManager class does not include any attributes' declaration. Therefore, objects created from this class would not have any state associated. The class also defines three methods—IsEven, Cube, and Add—that perform operations solely based on their parameters and return values with the calculated results. Because these methods do not access instance variables (attributes), they can be declared as static. Such methods are often referred to as utility methods because they provide utility functions that are not tied to the state of specific objects.

It should be noted that, as the above code snippet shows, a class that contains only static methods can also be declared with the static keyword. This indicates that no object instances of that class can ever be created. When a class defines static methods, these methods can be invoked by other classes directly by referring to the class name . the method name. This is shown in the above code snippet for the Program class when the static methods IsEven and Add are called.

Methods that Access Other Static Members

Another common use of static methods is to implement behaviours operating on other static attributes and methods of the same class or static members exposed by other classes. The method GetAccountsCreated in the code snippet of the previous section on static attributes could be an example of this scenario.

As discussed earlier, when that method is invoked in the Main on separate object instances, the result of those invocations is identical. This suggests that the behaviour is related to the class BankAccount rather than specific object instances. Therefore, the definition of the GetAccountsCreated method can become static, as shown in the following code snippet:

class BankAccount
{
    private string number;
    private double balance;
    private static int accountsCreated = 0;

    public BankAccount(string num, double bal) 
    {
        number = num;
        balance = bal;
        accountsCreated++;
    }

    public static int GetAccountsCreated() 
    {
        return accountsCreated;
    }

    // all other BankAccount methods: Deposit, Withdraw, etc.
}

class Program
{
    public static void Main() 
    {
        BankAccount.GetAccountsCreated(); // 0

        BankAccount account1, account2;
        account1 = new BankAccount("A0123", 500.5);
        account2 = new BankAccount("BD324", 100.0);

        BankAccount.GetAccountsCreated(); // 2    
    }
}

Because the method is now associated with the class and not a specific object, it can be invoked via the class name . the method identifier, i.e., BankAccount.GetAccountsCreated(), even when no BankAccount objects have been allocated on the heap at the beginning of the Main.

It should be noted that because the method is now attached to a class and not to a particular object instance, it will not have access to instance variables. In fact, no specific object reference is passed implicitly to the method when it is invoked; therefore, the this keyword cannot be used inside the body of the method.

On the other hand, if the static GetAccountsCreated method had reference type variables passed via its parameters or declared and instantiated within its body, attributes of objects referenced through those variables would be accessible.

Conclusions

Static is a concept that applies to both attributes and methods. In C# .NET and other object-oriented programming languages, Static attributes and methods are associated with a class rather than specific object instances. Static attributes are shared among all instances of the class and exist independently of whether objects of that class are instantiated.

Static methods are behaviours that are attached to a class and can be called without referencing an object instance. They do not operate on instance variables but can use other static members of the class or those exposed by other classes. Static methods are commonly used for utility functions that perform operations without relying on an object-specific state.