Returning Read-only References from Functions in C#
Mar 27, 2020 • 7 Minute Read
Introduction
In this guide you will learn a concept for writing type and memory safe code. With C# version 1.0 developers could pass arguments to methods by reference. As time passed and programming changed the world, new challenges rose that forced new features to be added to C#. In version 7.0 of C#, two very important features were added: the possibility to declare references to local variables the ability to return by reference from methods.
This guide will build a foundation for you to understand the two types of argument passing, namely by value and by reference, and clarify what structs are and how to return them from methods or functions.
Value Versus Reference
There are two main types of argument passing in C#: reference and Value. C# also has the ability to define custom types, but that is not in the scope of the guide. Value and reference types show different performance characteristics because of the nature of their implementation. Value types do not have any overhead in terms of memory because the data that you can store in a specific type like int, string, or double has a specific size. Reference types have two extra fields called ObjectHearder and MethodTable.
ObjectHeader is used by the CLR to store additional information, and it's basically a bitmask. MethodTable is a pointer to the Method Table, which is a set of metadata about a specific type. Calling a method from an object makes the CLR jump to the Method Table and get the address of the method's implementation to perform the call.
Structs
You can find a more detailed guide on structs here. For now, all you need to be aware of is that structs are very similar to classes but they are intended for storing data. Read-only structs are a bit different because their fields can only be modified once, when the constructor of the struct is called. After that a read-only struct stays intact. This guide is about read-only references, so read-only structs fall out of the picture.
This is an example of a read-only struct:
public readonly struct Pandemic
{
public string virusName { get; }
public double infectRatio { get; }
public int initialCases { get; }
public double deathRate { get; }
public Pandemic(string name, double ratio, int cases, double rate)
{
virusName = name;
infectRatio = ratio;
initialCases = cases;
deathRate = rate;
}
}
We see that the accessors are only defined with get, set is missing, and the readonly keyword is there to tell the CLR that the struct is going to be immutable. This means that over time the fields cannot be changed.
Now, you will need to combine the struct and passing values by reference to create a demonstration for the original topic of the guide.
Ref Read-only
The ref keyword indicates to a function that the value is passed by reference. It can be used in multiple contexts. For example, it can be used in a method signature and method call to pass an argument to a method. Or it could be used in a method signature to return a value to the caller by reference, which is what is needed at this point. This keyword allows you to notify the caller that the returned object is immutable, and modification to the value is not possible.
Take a look at this simple example before diving into read-only structs:
using System;
namespace Pluralsight
{
public class SimpleRefReadonly
{
static int[] a = new int[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
static ref int GetIndex(int index) {
return ref a[index];
}
public static void Main()
{
Console.WriteLine(GetIndex(0));
Console.ReadKey();
}
}
}
This code has a static array of integers with a size of 10. GetIndex, the method used here, is taking an index as an argument and returns an item from the array, which is read-only. If you pass an index bigger than the length of the array or smaller than zero you'll get an exception stating that a System.IndexOutOfRangeException exception occurred during runtime.
For a more complex example, you can build a struct that represents a school with rooms that have names and sizes. The struct must not allow modification to the size of the rooms once they are initialized.
using System;
namespace Pluralsight
{
public struct ClassRooms {
public ClassRooms(string name, int size) {
Name = name;
Size = size;
_currentUtilization = null;
}
private ClassRooms[] _currentUtilization;
public string Name { get; set; }
public int Size { get; set; }
public override string ToString()
{
return $"{this.Name} :: {this.Size}";
}
public void SetRoom(ClassRooms[] classrooms) => _currentUtilization = classrooms;
public ref readonly ClassRooms Getutilization(int x)
{
return ref _currentUtilization[x];
}
}
public class ComplexRefReadonly
{
public static void Main()
{
var Rooms = new ClassRooms[] { new ClassRooms("Mathematics", 20), new ClassRooms("Biologs", 15) };
var School = new ClassRooms();
School.SetRoom(Rooms);
Console.WriteLine(School.Getutilization(1));
Console.WriteLine(School.Getutilization(0));
Console.ReadKey();
}
}
}
Upon executing the example, the following should be on the console:
Biologs :: 15
Mathematics :: 20
Here you have a struct called ClassRooms that represents the school. This struct is not read-only! Each classroom has a name and a size that need to be passed to the constructor. The ToString() function has been overridden to allow each classroom to be printed prettier. The key here is the following function:
public ref readonly ClassRooms Getutilization(int x)
{
return ref _currentUtilization[x];
}
This function returns a read-only copy of a specific classroom without modification, and protects the original values assigned during initialization. The Main() function is responsible for building up the school; first the array of Rooms variables is initialized, then the School, which is made up of rooms. Finally the Getutilization(<index>) function allows retrieval of a read-only copy of the given classroom.
Conclusion
In this guide we went over the difference between value and reference types. We clarified what ref readonly means in concept, and throughout the code demonstration we showed how to combine them in order to create type and memory safe application. I hope this guide has been informative to you and I would like to thank you for reading it!