How to live without const?

  • Tutorial
Often, passing an object to a method, we would like to tell it: “Here, hold this object, but you do not have the right to change it,” and somehow note this when called. The advantages are obvious: besides the fact that the code becomes more reliable, it also becomes more readable. We do not need to go into the implementation of each method in order to track how and where the object of interest to us changes. Moreover, if the constancy of the arguments passed is indicated in the method signature, then by the very signature itself, with one degree or another accuracy, we can already assume what it actually does. Another plus is thread safety, as we know that the object is read only.
In C / C ++, the const keyword exists for these purposes. Many will say that such a mechanism is too unreliable, however, in C # there is no such mechanism. And maybe it will appear in future versions (the developers do not deny this), but what about now?


1. Immutable objects

The most famous similar object in C # is a string. There is not a single method in it that leads to a change in the object itself, but only to the creation of a new one. And everything with them seems to be nice and beautiful (they are easy to use and reliable), until we recall the performance. For example, you can find a substring without copying the entire array of characters, however, what if we need, say, to replace characters in a string? But what if we need to process an array of thousands of such strings? In each case, a new row object will be created and the entire array will be copied. We no longer need the old lines, but the lines themselves do not know anything about this and continue to copy data. Only the developer, calling the method, may or may not give the right to change the object-arguments. Also, the use of immutable objects is not reflected in the signature of the method. How do we be?

2. Interface

One option is to create an interface for the read only object from which to exclude all methods that modify the object. And if this object is generic, then covariance can also be added to the interface. For an example with a vector it will look like this:

interface IVectorConst
{
    T this[int nIndex] { get; }
}
class Vector : IVectorConst
{
    private readonly T[] _vector;
    public Vector(int nSize)
    {
        _vector = new T[nSize];
    }
    public T this[int nIndex]
    {
        get { return _vector[nIndex]; }
        set { _vector[nIndex] = value; }
    }
}
void ReadVector(IVectorConst vector)
{
   ...
}


(By the way, between Vector and IVectorConst (or IVectorReader - as you like) you can also add a contravariant IVectorWriter.)

And everything would be fine, but nothing prevents ReadVector from making a downcast to Vector and changing it. However, if you recall the const from C ++, this method is no less reliable than the equally unreliable const, which does not prohibit any pointer conversion. If this is enough for you, you can stop; if not, move on.

3. Separation of a constant object through the "Adapter"

We can prohibit the aforementioned downcast in only one way: make sure that Vector does not inherit from IVectorConst, that is, separate it. One way to do this is to create a VectorConst “Adapter” (thanks to osmirnov for recalling this method, without it the article would be incomplete). It will look like this:

interface IVector
{
    T this[int nIndex] { set; }
}
interface IVectorConst
{
    T this[int nIndex] { get; }
}
class VectorConst : IVectorConst
{
    private readonly Vector _vector;
    public VectorConst(Vector vector)
    {
        _vector = vector;
    }
    public T this[int nIndex]
    {
        get { return _vector[nIndex]; }
    }
}
class Vector : IVector
{
    private readonly T[] _vector;
    public Vector(int nSize)
    {
        _vector = new T[nSize];
    }
    public T this[int nIndex]
    {
        get { return _vector[nIndex]; }
        set { _vector[nIndex] = value; }
    }
    public IVectorConst AsConst
    {
        get { return new VectorConst(this); }
    }
}


As we see, the constant object (VectorConst) is completely separated from the main one, and downcast cannot be made from it. Giving it to someone, we can sleep peacefully, being sure that our vector will remain unchanged.

VectorConst does not contain its own implementation (all of it is still in Vector), it simply redirects it to the Vector instance. But not everything here is as smooth as we would like ... When you call VectorConst, there is already not one call, but two, and this can already be costly from the so-called. performance. In addition, each time you change the interface of the main object, you will have to add / edit methods in IVectorConst and VectorConst, and when new suitable methods appear, the compiler will not force us to do this, we must remember this ourselves, and this is an extra headache. But if these disadvantages are not so critical, then this approach will probably be optimal. And in particular, if the main object is already written and it is impossible or inexpedient to rewrite it.

4. Real separation of a constant object

In the same example with a vector, it will look like this:

interface IVectorConst
{
    T this[int nIndex] { get; }
}
interface IVector
{
    T this[int nIndex] { set; }
}
struct VectorConst : IVectorConst
{
    private readonly T[] _vector;
    public VectorConst(T[] vector)
    {
        _vector = vector;
    }
    public T this[int nIndex]
    {
        get { return _vector[nIndex]; }
    }
}
struct Vector : IVector
{
    private readonly T[] _vector;
    private readonly VectorConst _reader;
    public Vector(int nSize)
    {
        _reader = new VectorConst(_vector = new T[nSize]);
    }
    public T this[int nIndex]
    {
        set { _vector[nIndex] = value; }
    }
    public VectorConst Reader
    {
        get { return _reader; }
    }
    public static implicit operator VectorConst(Vector vector)
    {
        return vector._reader;
    }
}


Now our VectorConst is not only separated, but the implementation of the vector itself is divided into two parts. All that we had to pay for it with t.z. performance, is the initialization of the VectorConst structure by copying the link to _vector and an additional link in memory. When VectorConst is passed to the method, a property call and the same copy are made. Thus, we can say that in terms of performance this is almost equivalent to passing T [] to the instance method, but with protection from changes (which is what we achieved). And so that when passing a Vector instance to methods that accept VectorConst, do not explicitly call the Reader property again, you can add a conversion operator to Vector:

public static implicit operator VectorConst(Vector vector)
{
    return vector._reader;
}


However, when using the object directly, we cannot do without calling the Reader property:

var v = new Vector(5);
v[0] = 0;
Console.WriteLine(v.Reader[0]);


And also we can not do without it if we need to use the covariance of IVectorConst (despite the presence of the conversion operator):

class A
{
}
class B : A
{
}
private static void ReadVector(IVectorConst vector)
{
    ...
}
var vector = new Vector();
ReadVector(vector.Reader);


And this is the main and perhaps the only minus of this approach: its use is somewhat unusual due to the need to call Reader in some cases. But while C # lacks const for arguments, in any case you have to sacrifice something.

Many will probably say that all these are common truths. But maybe for someone this article and these simple templates will be useful.

Also popular now: