Search code examples
c#genericsgeneric-programming

Writing a generic method with two possible sets of constraints on the generic argument


I'm on a quest to write a TypedBinaryReader that would be able to read any type that BinaryReader normally supports, and a type that implements a specific interface. I have come really close, but I'm not quite there yet.

For the value types, I mapped the types to functors that call the appropriate functions.

For the reference types, as long as they inherit the interface I specified and can be constructed, the function below works.

However, I want to create an universal generic method call, ReadUniversal<T>() that would work for both value types and the above specified reference types.

This is attempt number one, it works, but It's not generic enought, I still have to cases.

public class TypedBinaryReader : BinaryReader {

        private readonly Dictionary<Type, object> functorBindings;

        public TypedBinaryReader(Stream input) : this(input, Encoding.UTF8, false) { }

        public TypedBinaryReader(Stream input, Encoding encoding) : this(input, encoding, false) { }

        public TypedBinaryReader(Stream input, Encoding encoding, bool leaveOpen) : base(input, encoding, leaveOpen) {
            functorBindings = new Dictionary<Type, object>() {
                {typeof(byte), new Func<byte>(ReadByte)},
                {typeof(int), new Func<int>(ReadInt32)},
                {typeof(short), new Func<short>(ReadInt16)},
                {typeof(long), new Func<long>(ReadInt64)},
                {typeof(sbyte), new Func<sbyte>(ReadSByte)},
                {typeof(uint), new Func<uint>(ReadUInt32)},
                {typeof(ushort), new Func<ushort>(ReadUInt16)},
                {typeof(ulong), new Func<ulong>(ReadUInt64)},
                {typeof(bool), new Func<bool>(ReadBoolean)},
                {typeof(float), new Func<float>(ReadSingle)}
            };
        }


        public T ReadValueType<T>() {
            return ((Func<T>)functorBindings[typeof(T)])();
        }

        public T ReadReferenceType<T>() where T : MyReadableInterface, new() {
            T item = new T();
            item.Read(this);
            return item;
        }

        public List<T> ReadMultipleValuesList<T, R>() {
            dynamic size = ReadValueType<R>();
            List<T> list = new List<T>(size);
            for (dynamic i = 0; i < size; ++i) {
                list.Add(ReadValueType<T>());
            }

            return list;
        }

        public List<T> ReadMultipleObjecsList<T, R>() where T : MyReadableInterface {
            dynamic size = ReadValueType<R>();
            List<T> list = new List<T>(size);
            for (dynamic i = 0; i < size; ++i) {
                list.Add(ReadReferenceType<T>());
            }

            return list;
        }
}

An idea that I came up with, that I don't really like, is to write generic class that boxes in the value types, like this one:

 public class Value<T> : MyReadableInterface {

        private T value;

        public Value(T value) {
            this.value = value;
        }

        internal Value(TypedBinaryReader reader) {
            Read(reader);
        }

        public T Get() {
            return value;
        }

        public void Set(T value) {
            if (!this.value.Equals(value)) {
                this.value = value;
            }
        }

        public override string ToString() {
            return value.ToString();
        }

        public void Read(TypedBinaryReader reader) {
            value = reader.ReadValueType<T>();
        }
    }

This way, I can use ReadReferencTypes<T>() even on value types, as long as I pass the type parameter as Value<int> instead of just int.

But this is still ugly since I again have to remember what I'm reading, just instead of having to remember function signature, I have to remember to box in the value types.

Ideal solution would be when I could add a following method to TypedBinaryReader class:

public T ReadUniversal<T>() {
    if ((T).IsSubclassOf(typeof(MyReadableInterface)) {
        return ReadReferenceType<T>();
    } else if (functorBindings.ContainsKey(typeof(T)) {
        return ReadValueType<T>();
    } else {
        throw new SomeException();
    }
}

However, due to different constraints on the generic argument T, this won't work. Any ideas on how to make it work?

Ultimate goal is to read any type that BinaryReader normally can or any type that implements the interface, using only a single method.


Solution

  • If you need a method to handle reference types and a method to handle value types, that's a perfectly valid reason to have two methods.

    What may help is to view this from the perspective of code that will call the methods in this class. From their perspective, do they benefit if they can call just one method regardless of the type instead of having to call one method for value types and another for value types? Probably not.

    What happens (and I've done this lots and lots of times) is that we get caught up in how we want a certain class to look or behave for reasons that aren't related to the actual software that we're trying to write. In my experience this happens a lot when we're trying to write generic classes. Generic classes help us when we see unnecessarily code duplication in cases where the types we're working with don't matter (like if we had one class for a list of ints, another for a list of doubles, etc.)

    Then when we get around to actually using the classes we've created we may find that our needs are not quite what we thought, and the time we spent polishing that generic class goes to waste.

    If the types we're working with do require entirely different code then forcing the handling of multiple unrelated types into a single generic method is going to make your code more complicated. (Whenever we feel forced to use dynamic it's a good sign that something may have become overcomplicated.)

    My suggestion is just to write the code that you need and not worry if you need to call different methods. See if it actually creates a problem. It probably won't. Don't try to solve the problem until it appears.