Search code examples
c#genericstype-constraints

Is there a way to introduce a constraint to a generic type inside the class defining it?


I've an interface for a block of memory that should be implemented both by a class managing the ram memory and a class managing a block of memory on disk. The first class should support also reference types, while the second only value types.

As for the interface example consider the following. I'm not setting a constraint for T to allow support for reference types of the first class:

public interface IMemoryBlock<T>
{
    T this[int index] {get; }
}

The second class has the check on T for being a value type (typeof(T).IsValueType) at initialization and should have something like the following:

public class DiskMemoryBlock<T> : IMemoryBlock<T>
{
    public T this[int index]
    {
        get
        {
            byte[] bytes = ReadBytes(index * sizeOfT, sizeOfT);
            return GenericBitConverter.ToValue<T>(bytes, 0);
        }
    }
}

which does not work because GenericBitConverter.ToValue<T> requires the value type constraint on T. Is there a way to introduce a constraint later on to solve this case? Otherwise, what do you suggest in such a case as anything different than writing a custom GenericBitConverter.ToValue<T> without the constraint?

EDIT

I forgot to specify that then I have a Buffer<T> class, which according to parameters should initialize either a DiskMemoryBlock<T> or a RamMemoryBlock<T>:

public class Buffer<T>
{
    IMemoryBlock<T> buffer;
    public Buffer(**params**)
    {
        buffer = (**conditions**) ? new DiskMemoryBlock<T>() : new RamMemoryBlock<T>();
    }
}

Solution

  • @Benjamin Hodgson has answered the main part of your question the same way I would have, so I'll not waste space repeating him. This is only a response to your edit.

    You want to instantiate either a DiskMemoryBlock<T> or a RamMemoryBlock<T> based on some conditions that come from the Buffer<T> constructor parameters:

    public class Buffer<T>
    {
        IMemoryBlock<T> buffer;
        public Buffer(**params**)
        {
            buffer = (**conditions**) ? new DiskMemoryBlock<T>() : new RamMemoryBlock<T>();
        }
    }
    

    Unfortunately, when instantiating an object of a class (ignoring reflection jiggery-pokery), any type constraints on that class have need to be guaranteed at compile time. That means the only ways you can construct a DiskMemoryBlock<T> are:

    1. Specify T directly as a value type (e.g. var block = new DiskMemoryBlock<int>())
    2. Construct it inside a context which has a struct contraint on it.

    Your case is neither of these. Given that having a generic Buffer seems important, option 1 isn't any help, so option 2 is your only bet here.

    Let's see if we can fix this.

    We can try putting the difficult IMemoryBlock creation into a virtual method, and create a value-type-only subclass of Buffer which can create a DiskMemoryBlock without issue:

    public class Buffer<T>
    {
        IMemoryBlock<T> buffer;
    
        public Buffer(**params**)
        {
            buffer = (**conditions**) 
                ? CreateMemoryBlockPreferDisk() 
                : new RamMemoryBlock<T>();
        }
    
        protected virtual IMemoryBlock<T> CreateMemoryBlockPreferDisk()
        {
            return new RamMemoryBlock<T>(); 
        }
    }
    
    public class ValueBuffer<T> : Buffer<T> where T : struct
    {
        public ValueBuffer(**params**) : base(**params**) { }
    
        protected override IMemoryBlock<T> CreateMemoryBlockPreferDisk()
        {
            return new DiskMemoryBlock<T>();
        }
    }
    

    Ok, so we have something that works, but there's a bit of a problem - calling virtual methods from the constructor is not a great idea - the derived class' constructor hasn't run yet, so we could have all sorts of uninitialised things going on in ValueBuffer. It's ok in this case, because the override doesn't use any members of the derived class (in fact, there aren't any), but it leaves the door open for things to unexpectedly break in the future if there are any more subclasses.

    So maybe instead of having the base class call the derived class, we can have the derived class pass a function up to the base class?

    public class Buffer<T>
    {
        IMemoryBlock<T> buffer;
    
        public Buffer(**params**)
            : this(**params**, () => new RamMemoryBlock<T>())
        {
        }
    
        protected Buffer(**params**, Func<IMemoryBlock<T>> createMemoryBlockPreferDisk)
        {
            buffer = (**conditions**)
                ? createMemoryBlockPreferDisk()
                : new RamMemoryBlock<T>();
        }
    }
    
    public class ValueBuffer<T> : Buffer<T>
        where T : struct
    {
        public ValueBuffer(**params**)
            : base(**params**, () => new DiskMemoryBlock<T>())
        {
        }
    }
    

    This looks better - no virtual calls and everything's nice. Although...

    The problem we're having is trying to choose between DiskMemoryBlock and RamMemoryBlock at runtime. We've fixed that, but now if you want a Buffer, you have to choose between a Buffer and a ValueBuffer. No matter - we can do this same trick all the way up, right?

    Well, we could, but that's creating two versions of every class all the way up. That's a lot of work and a pain. And what if there's some third constraint - a Buffer that only deals with reference types, or a special space efficient bool buffer?

    The solution is similar to the approach of passing createMemoryBlockPreferDisk into the constructor of Buffer - make the Buffer completely agnostic to the type of IMemoryBlock it's using, and just give it a function which will create the relevant type for it. Better yet, wrap the function up in a Factory class in case we need more options later:

    public enum MemoryBlockCreationLocation
    {
        Disk,
        Ram
    }
    
    public interface IMemoryBlockFactory<T>
    {
        IMemoryBlock<T> CreateMemoryBlock(MemoryBlockCreationLocation preferredLocation);
    }
    
    public class Buffer<T>
    {
        IMemoryBlock<T> buffer;
    
        public Buffer(**params**, IMemoryBlockFactory<T> memoryBlockFactory)
        {
            var preferredLocation = (**conditions**)
                ? MemoryBlockCreationLocation.Disk
                : MemoryBlockCreationLocation.Ram;
    
            buffer = memoryBlockFactory.CreateMemoryBlock(preferredLocation);
        }
    }
    
    public class GeneralMemoryBlockFactory<T> : IMemoryBlockFactory<T>
    {
        public IMemoryBlock<T> CreateMemoryBlock(MemoryBlockCreationLocation preferredLocation)
        {
            // We can't create a DiskMemoryBlock in general - ignore the preferred location and return a RamMemoryBlock.
            return new RamMemoryBlock<T>();
        }
    }
    
    public class ValueTypeMemoryBlockFactory<T> : IMemoryBlockFactory<T>
        where T : struct
    {
        public IMemoryBlock<T> CreateMemoryBlock(MemoryBlockCreationLocation preferredLocation)
        {
            switch (preferredLocation)
            {
                case MemoryBlockCreationLocation.Ram:
                    return new RamMemoryBlock<T>();
    
                case MemoryBlockCreationLocation.Disk:
                default:
                    return new DiskMemoryBlock<T>();
            }
        }
    }
    

    We still need to decide somewhere which version of IMemoryBlockFactory we need, but as outlined above, there's no way around that - the type system needs to know which version of IMemoryBlock you're instantiating at compile time.

    On the plus side, all of the classes between that decision and your Buffer only need to know of the existence of a IMemoryBlockFactory. That means you can change things and keep the ripple effects fairly small:

    • If you need extra types of IMemoryBlock, you create an extra IMemoryBlockFactory class and you're done.
    • If you need the IMemoryBlock type to be decided on more complex factors, you can change CreateMemoryBlock to take different parameters with only the factories and Buffer implementation affected.

    If you don't need these advantages (the memory blocks are unlikely to change and you tend to instantiate Buffer objects with a concrete type), then by all means, don't bother with the extra complexity of having a factory, and use the version from about halfway through this answer (where a Func<IMemoryBlock> gets passed into the constructor).