Thursday, October 2, 2008

Mark a C# class data member as readonly when it’s read only

2009.01.30 - Updated in order to clarify about immutability.
The readonly modifier is used to declare an instance-specific constant data member which can be initialized in the class constructor and is not limited to compiler-time constants only.
The readonly modifier is used to declare a constant data member in a class, but providing more flexibility than using the const modifier. The const modifier implicitly defines the data member as static, and must be initialized along with its declaration, as follows:
public const int myConst = 5;
If the const is not initialized along with the declaration, the compiler will generate an error.
Moreover, a const data member must be initialized with a compile-time constant – in other words there is no way to initialize a constant using a runtime value. For instance, the following code doesn’t compile:
public class MyClass
{
 public static string nonConstValue = "This is a variable initialized at runtime";
 public const string constValue = nonConstValue;  // Compilation error
 ...
}
as the compiler will inform that “The expression being assigned to 'MyClass.constValue' must be constant”.
The advantage of defining a data member as const is that it cannot be changed – the drawback being the inability to initialize the field in a different place other than the declaration.
On the other hands, the readonly modifier extends the const modifier by allowing the data member to be initialized in any class constructor. This allows a data member to be initialized when a class is being instantiated, preventing it from being modified in any other class method – in other words, once the const data member is initialized it never changes during the class instance lifetime. As opposed to the const modifier, the readonly modifier creates an instance-specific data member, so each class instance will have its own readonly data member instances. This doesn’t mean that a readonly data member cannot be static – simply it must explicitly be declared as such.
All the following declarations and initializations are valid:
public class MyClass
{
 public readonly int _myIntField = 5; // Allowed
 public readonly string _myStringField; // Allowed

 public MyClass()
 {
  _myIntField = 8;         // Allowed
  _myStringField = "Initialized in the constructor"; // Allowed
 }

 public static void Main()
 {
  MyClass myClass;
  
  myClass = new MyClass();
  
  System.Console.Out.WriteLine(string.Format("myIntField = {0}", myClass._myIntField));
  System.Console.Out.WriteLine(string.Format("myStringField = {0}", myClass._myStringField));
 }
}
The member declared at line 4 is initialized in the constructor with a string constant, whereas the int member declared at line 3 is initialized both in the declaration and in the constructor. In this case, the constructor initialization takes precedence, as it is executed after the data member instantiation.
The following case instead is not valid
public class MyClass
{
 public readonly string _myStringField; // Allowed

 public MyClass()
 {
  Init();
 }

 private void Init()
 {
  _myStringField = "Cannot be initialized here";  // Compilation error
 }
 
 ... 
}
That’s because the readonly member is initialized in a class member which is not a constructor, even if this method is called from the constructor. The reason why this is not allowed is that the Init() method can be called from anywhere within the class, or even outside if it is protected, internal or public rather than private.
Another advantage of the readonly modifier is that it can be initialized with a runtime value or an expression evaluated at runtime, for instance:
public class MyClass
{
 public readonly int  _myIntField; // Allowed

 public MyClass(int runtimeValue)
 {
  _myIntField = 10 * runtimeValue;
 }

 
 public static void Main()
 {
  MyClass myClass;
  
  myClass = new MyClass(5);
  
  System.Console.Out.WriteLine(string.Format("myIntField = {0}", myClass._myIntField));
 }
}
As we can expect, the _myIntField readonly data member will be initialized to 10 * 5 = 50.
Readonly data members are not limited to basic data types – it can be used for class instances. For example, when writing a database accessor class we might want to provide a database connection:
public class MyDatabaseAccessor
{
 private readonly SqlConnection _connection;
 
 public MyDatabaseAccessor (SqlConnection connection)
 {
  _connection = connection;
  ...
 }
 
 ...

}

public class MyApp
{
 public static void Main()
 {
  SqlConnection connection = new SqlConnection();
  
  // Initialize the connection
  ...
  
  MyDatabaseAccessor myAccessor = new MyDatabaseAccessor(connection);
  
  // Do something
  ...
 }
}

The above code would work without declaring the _connection data member as readonly – but in such case the connection can be changed anywhere in the class.
It’s important to keep in mind that when a readonly data member is of a reference type (i.e. instance of a class and not either a struct or a base data type) the reference is constant, but the instance it points to is not. So the internal status of a readonly instance can change and can be changed, as proven by the following sample:
// Class with a data member
public class MyClassType
{
 private string _internalStatus;
 
 public string InternalStatus {
  get {return _internalStatus;} 
  set {_internalStatus = value;}
 }
}

// Class with a readonly data member
public class MyClassWithReadonlyMember
{
 private readonly MyClassType _readonlyInstance;
 
 public MyClassWithReadonlyMember(string value)
 {
  _readonlyInstance = new MyClassType();
  _readonlyInstance.InternalStatus = value;
 }
 
 public void SetStatus(string message)
 {
  _readonlyInstance.InternalStatus = message;
 }
 
 public void OutputStatus()
 {
  System.Console.Out.WriteLine(string.Format(
    @"Internal status = '{0}'", _readonlyInstance.InternalStatus));
 }
}

public class MyApp
{
 public static void Main()
 {
  MyClassWithReadonlyMember classInstance;
  
  // Create an instance of MyClassWithReadOnlyMember class
  // The instance of MyClassType is initialized to the default 
  // value provided in the constructor
  classInstance = new MyClassWithReadonlyMember("Initial value");
  classInstance.OutputStatus();
  
  // The internal status of the readonly data member of 
  // MyClassWithReadonlyMember is changed
  classInstance.SetStatus("New Value");
  classInstance.OutputStatus();  
 }
}
If this code is compiled and run. it will generate the following output:
Internal status = 'Initial value'
Internal status = 'New Value'

2009.01.30 Addendum

Thanks to what Chris Marisic highlighted in his comments below, I need to clarify a few points to prevent some misleading assumptions.
#1 - readonly members can be modified using reflection. The readonly modifier is a compiler directive, so any attempt to modify a readonly data member is detected during compilation. A consequence is that using reflection it's possible to modify a readonly data member
#2 - I wrote above that the const modifier "allows a data member to be initialized when a class is being instantiated, preventing it from being modified in any other class method". This statement doesn't mean that the data member is immutable, at least not always. .NET supports two kinds of data types: value types and reference types.:
  • value types are allocated on the thread's stack and they hold the actual value
  • reference types are allocated on the thread's stack and they hold a pointer to an object allocated in the managed heap
In both cases variables are allocated on the thread's stack, the difference being that a value type holds the actual value (for example, an integer, a struct), whereas the reference type holds a pointer to the memory area where the actual value is stored (for example, an instance of a StringBuilder class).
When a value type is declared as readonly, its value is immutable, meaning that once assigned it cannot be modified outside constructors.
When a reference type is declared as readonly, the pointer is immutable, but not the object it points to. This means that:
  • a reference type data member can be initialized in order to point to an instance of a class, but once this is done it's not possible to make it point to another instance of a class outside of constructors
  • the readonly modifier has no effect on the object the readonly data member points to.
Maybe a real example helps to better understand the difference. Let's create a simple class and a simple struct. They have one int data member only.
public class MyClass
{
 public int IntField;
}

public struct MyStruct
{
 public int IntField;
}
Now let's use them in a test class:
public class MyTestClass
{
 private readonly MyClass _myClass;
 private readonly MyStruct _myStruct;

 public MyTestClass()
 {
  _myClass = new MyClass();
  _myClass.IntField = 4;

  _myStruct.IntField = 5;
 }
}
The _myClass data member is a reference type, so it must be initialized by allocating a new instance of MyClass and assigning its pointer, as seen at line 8. On the other hand, the _myStruct data member is a value type, meaning that it is already an instance of MyStruct. Initialization of their respective fields are done at lines 9 and 11.
Let's add a method to the MyTestClass class.
public class MyTestClass
{
 private readonly MyClass _myClass;
 private readonly MyStruct _myStruct;

 public MyTestClass()
 {
  _myClass = new MyClass();
  _myClass.IntField = 4;

  _myStruct.IntField = 5;
 }

 public void TestReadonly()
 {
  _myClass.IntField = 7;  // Valid statement
  _myStruct.IntField = 10; // Error: _myStruct is readonly
  _myClass = null;   // Error: _myClass is readonly
 }
}
If we try to compile this code, we get 2 errors:
MyTest.cs(26,3): error CS1648: Members of readonly field 'MyTestClass._myStruct'         cannot be modified (except in a constructor or a variable initializer)
MyTest.cs(27,3): error CS0191: A readonly field cannot be assigned to (except in         a constructor or a variable initializer)

MyStruct is a struct, hence a value type; _myStruct is immutable since declared as readonly, so any of its data members cannot be modified outside of a constructor.
MyClass is a class, hence a reference type; _myClass is immutable since declared as readonly, so it cannot be modified in order to point to another instance of a class (or to null) outside of a constructor.

0 comments:

Copyright © 2013. All Rights Reserved.