Mark A. Donohoe
Mark A. Donohoe

Reputation: 30368

How can I create a privately-settable read-only struct (such as Size) in C#?

Not sure if this can be done, but I need to calculate and store a size in a base class, then expose that result as read-only to subclasses. Making the Size itself readonly is easy by hiding it behind a property with a protected getter and a private setter, like this...

private Size _someSize;
protected Size SomeSize
{
    get{ return _someSize; }
    private set{ _someSize = value; }
}

Then from within the base class, I can set it like this...

SomeSize = new Size(23.0, 14.7);

...but I can't do that from a subclass class because the setter is private to the base class.

However, I also don't want the subclass to be able to modify the members of the Size structure either.

Update

In the subclass, if you try and compile this...

SomeSize.Width = 17.0;

...you will get the error 'Cannot modify the return value of SomeSize because it is not a variable' so that does protect the value in the base class as I had hoped.

Still, if someone can figure out how to make the getter return a true read-only structure (if such a thing is even possible, which I doubt), I'll give you the answer. Granted, that wasn't actually needed for this problem, but that would still be a good thing to know if it can be done.

Upvotes: 3

Views: 1826

Answers (2)

supercat
supercat

Reputation: 81149

One of the major limitations of structure types in .net languages is that while the fields of a writable struct-type storage location (variable, field, parameter, array slot, etc.) are themselves writable storage locations (and if they are struct types, their fields are likewise writable storage locations), such access only works with struct type storage locations, and there is no means via which any type other than System.Array can expose a property that works like a storage location.

It is possible for a class to expose a privately-held storage location as a storage location under controlled circumstances, if it offers a method which can pass that storage location as a ref parameter to a supplied callback.

For example:

public delegate void ActByRef<T1>(ref T1 p1);
public delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
public delegate void ActByRef<T1,T2,T3>(ref T1 p1, ref T2 p2, ref T3 p3);

public void ActOnSize(ActByRef proc) 
  { proc( ref _someSize); }
public void ActOnSize<XT1>(ActByRef proc, ref XT1 xp1) 
  { proc( ref _someSize, ref xp1); }
public void ActOnSize<XT1,XT2>(ActByRef proc, ref XT1 xp1, ref XT2 xp2)
  { proc( ref _someSize, ref xp1, ref xp2); }

If one wished to add 5 to the width of a something which exposed such a method, one could then use

thing.ActOnSize((ref Size sz) => sz.Width += 5);

If one had a local variable 'HeightAdder', and one wished to add that to the object's height, one could use

thing.ActOnSize((ref Size sz, ref int adder) =>
  sz.Height += adder, ref HeightAdder);

Note that because the lambda expression as written would not capture any local variables, it could be evaluated as a static delegate (if the delegate had added HeightAdder, rather than ref parameter adder to the height, that would have required generating a closure to hold HeightAdder each time it entered scope, and would have required generating a delegate each time the method call is executed; passing the amount as a ref parameter avoids that overhead).

A similar approach could be used by a class similar to List<T> or Dictionary<TKey,TValue> to allow a callback method to act directly on a list slot or dictionary entry, if the access method includes a parameter for the index or key.

One nice feature of this approach is that it allows collections to provide for styles of concurrent access that are otherwise difficult. If the things in the collection are of a type which can be used with Interlocked methods (or are exposed-structure types whose fields are usable with such methods), client code may use such methods to perform thread-safe atomic operations on the underlying data. If the things in a collection are not of such a type, the collection may be able to implement locking somewhat more safely than would be possible with other approaches. For example, a ConcurrentIndexedSet<T> might have a family of ActOnItem(int item, int timeout, ActByRef<T> proc); methods which will invoke proc on an item within a lock (trusting that the client-supplied proc can be trusted to return in a reasonable timeframe). While such code couldn't guard against the possibility of proc getting deadlocked or otherwise waylaid, it could ensure that the lock will get released before returning control to the calling code.

Upvotes: 0

Steven Doggart
Steven Doggart

Reputation: 43743

You must not have tried to compile it, because what you have proposed already meets your needs. The Size type is a structure (value type), not a class (reference type), so the property getter will return a copy of the value stored in _someSize, not a reference to it. Therefore, if a sub class actually tried to change the SomeSize.Width property, it wouldn't actually be touching the private _someSize variable. It would just be changing the copy of that value that was returned. The compiler, however, recognizes that doing so is invalid, therefore, it won't even let the following line compile:

SomeSize.Width = 17.0;

The only way you could change the value and still get it to compile would be like this:

Size temp = SomeSize;
temp.Width = 17.0;

However, like I said, since that's just a copy of the value, it won't actually change the value of the SomeSize property--it will only change the value of temp.

If, however, the Size type was a class, you could still accomplish the same kind of protection by simply returning a clone of the object rather than a reference to the original object. For instance, if Size, was actually a class that looked like this:

public class MySize
{
    public MySize(float height, float width)
    {
        Height = height;
        Width = width;
    }

    public float Height { get; set; }
    public float Width { get; set; }

    public MySize GetCopy()
    {
        return (MySize)MemberwiseClone();
    }
}

Even though it's properties are not read-only, you could still make a pseudo read-only property out of it like this:

private MySize _someSize;
protected MySize SomeSize
{
    get { return _someSize.GetCopy(); }
    private set { _someSize = value; }
}

However, if you really want the properties of the returned object to be read-only, the only way to do that is to implement your own read-only version of the original type. For instance, the List<T> type supports the ability to get a read-only version of itself so that you can use that for read-only list properties. Because List<T> has that built-in functionality, you can easily do something like this:

private List<string> _list = new List<string>();
public ReadOnlyCollection<string> List
{
    get { return _list.AsReadOnly(); }
}

However, as you can see, the AsReadOnly method doesn't return a List<T> object. It instead returns a ReadOnlyCollection object which is a whole new type that is custom made as a read-only version of the list. So, in other words, the only way to truly make a read-only Size property would be to create your own ReadOnlySize type, like this:

public struct ReadOnlySize
{
    public ReadOnlySize(Size size)
    {
        _size = size;
    }

    private Size _size;

    public float Height 
    {
        get { return  _size.Height; } 
    }

    public float Width
    {
        get { return _size.Width; }
    }
}

And then you could make your read-only property like this:

private Size _someSize;
public ReadOnlySize SomeSize
{
    get { return new ReadOnlySize(_someSize); }
}

Upvotes: 5

Related Questions