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>();
}
}
@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:
T
directly as a value type (e.g. var block = new
DiskMemoryBlock<int>()
)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:
IMemoryBlock
, you create an extra IMemoryBlockFactory
class and you're done. 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).