⬆️ ⬇️

How to live without const?

Often, transferring an object to any method, we would like to say to him: “Here, hold this object, but you don’t have the right to change it,” and somehow mark this when calling. The advantages are obvious: in addition to 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 to track how and where the object of interest changes. Moreover, if the constancy of the arguments passed is indicated in the signature of the method, then from the signature itself, with some accuracy or another, we can already assume what it actually does. Another plus is thread safety, since we know that the object is read only.

In C / C ++, the keyword const exists for this purpose. Many will say that such a mechanism is too unreliable, however, in C # there is no such thing either. 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 the string. There is not a single method in it that leads to the change of the object itself, but only to the creation of a new one. And everything seems to be nice and beautiful with them (they are easy to use and reliable) until we remember 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 line object will be created and the entire array will be copied. We don’t need old strings, but the strings themselves know nothing about it and continue to copy data. Only the developer, calling the method, may or may not give the right to change the argument objects. In addition, the use of immutable objects is not reflected in the method signature in any way. How can 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 a generic, then covariance can be added to the interface. In the example with the vector, it will look like this:

')

interface IVectorConst<out T> { T this[int nIndex] { get; } } class Vector<T> : IVectorConst<T> { 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<int> vector) { ... } 




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



And all would be nothing, but nothing prevents ReadVector from making a downcast to the Vector and changing it. However, if we recall const from C ++, this method is no less reliable, just as an unreliable const, which does not prohibit any conversion of pointers. If this is enough for you, you can stop, if not - go ahead.



3. Separating a constant object using an “Adapter”


We can ban the above-mentioned downcast in only one way: make sure that Vector does not inherit from IVectorConst, that is, to separate it. One way to do this is to create a VectorConst "Adapter" (thanks to osmirnov for reminding about this method, without it the article would be incomplete). It will look like this:



 interface IVector<in T> { T this[int nIndex] { set; } } interface IVectorConst<out T> { T this[int nIndex] { get; } } class VectorConst<T> : IVectorConst<T> { private readonly Vector<T> _vector; public VectorConst(Vector<T> vector) { _vector = vector; } public T this[int nIndex] { get { return _vector[nIndex]; } } } class Vector<T> : IVector<T> { 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<T> AsConst { get { return new VectorConst<T>(this); } } } 




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



VectorConst does not contain its own implementation (it is still all in Vector), it simply forwards it to the Vector instance. But not everything here is as smooth as we would like ... When accessing VectorConst, not one call takes place, but two, and this may already be unprofitable with so-called. performance. In addition, each time the main object interface changes, it is necessary to add / edit methods in IVectorConst and VectorConst, and when new suitable methods appear, the compiler does 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 has already been written and it is impossible or impractical to rewrite it.



4. The real separation of a constant object


On the same vector example, it will look like this:



 interface IVectorConst<out T> { T this[int nIndex] { get; } } interface IVector<in T> { T this[int nIndex] { set; } } struct VectorConst<T> : IVectorConst<T> { private readonly T[] _vector; public VectorConst(T[] vector) { _vector = vector; } public T this[int nIndex] { get { return _vector[nIndex]; } } } struct Vector<T> : IVector<T> { private readonly T[] _vector; private readonly VectorConst<T> _reader; public Vector(int nSize) { _reader = new VectorConst<T>(_vector = new T[nSize]); } public T this[int nIndex] { set { _vector[nIndex] = value; } } public VectorConst<T> Reader { get { return _reader; } } public static implicit operator VectorConst<T>(Vector<T> 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 tz. performance, is the initialization of the VectorConst structure by copying the _vector reference and an additional reference in memory. When passing VectorConst to a method, the property is called and the same copy is made. Thus, it can be said that in terms of performance, this is almost equivalent to the transfer to the method of the instance T [], but with protection against changes (which we were trying to achieve). And so that when passing an instance of Vector to methods that accept VectorConst, you do not explicitly call the Reader property once again, you can add a conversion operator to Vector:



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




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



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




And also, we cannot do without it if we need to use the covariance of IVectorConst (despite the presence of the transformation operator):



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




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



Many will probably say that all these are truisms. But perhaps for someone this article and these simple templates will be useful.

Source: https://habr.com/ru/post/199520/



All Articles