5

I'm writing a sort of special of System.IO.BinaryWriter. This writer should be able to handle integral types, including Enum, and also collection of these types.

abstract class MyBinaryWriter
{
    // ...

    #region Methods: Basic Types: Writing
    public abstract void Write(byte value);
    public abstract void Write(ushort value);
    public abstract void Write(uint value);
    public abstract void Write(ulong value);
    public abstract void Write(string value); 
    #endregion

    #region Methods: Complex Types: Writing
    public virtual void Write<T>(ICollection<T> collection)
    {
        // first write the 32-bit-unsigned-length prefix
        if (collection == null || collection.Count == 0)
        {
            Write((uint)0);
        }
        else
        {
            Write((uint)collection.Count);

            // then write the elements, if any
            foreach (var item in collection)
                ; // What here? Obviously Write(item) doesn't work...
        }
    }

    // ...
}

What is the best approach to handle this problem? There is better solution using generics than writing an overload for each integral type and each enum type I wish to handle? A possible solution follows, but I don't like so much and has potential performance problems.

    #region Methods: Complex Types: Writing
    public virtual void Write<T>(ICollection<T> collection) where T : IConvertible
    {
        // first write the 32-bit-unsigned-length prefix
        if (collection == null || collection.Count == 0)
        {
            Write((uint)0);
        }
        else
        {
            Write((uint)collection.Count);

            // get the method for writing an element
            Action<T> write = null;
            var type = typeof(T);
            if (type.IsEnum)
                type = Enum.GetUnderlyingType(type);

            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Byte:
                case TypeCode.SByte:
                    write = (x => Write((byte)(IConvertible)x.ToByte(null)));
                    break;

                case TypeCode.Int16:
                case TypeCode.UInt16:
                    write = (x => Write((ushort)(IConvertible)x.ToUInt16(null)));
                    break;

                case TypeCode.Int32:
                case TypeCode.UInt32:
                    write = (x => Write((uint)(IConvertible)x.ToUInt32(null)));
                    break;

                case TypeCode.Int64:
                case TypeCode.UInt64:
                    write = (x => Write((ulong)(IConvertible)x.ToUInt64(null)));
                    break;

                default:
                    Debug.Fail("Only supported for integral types.");
                    break;
            }

            // then write the elements, if any
            foreach (var item in collection)
                write(item);
        }
    }
John Saunders
  • 160,644
  • 26
  • 247
  • 397
unlikely
  • 398
  • 2
  • 8
  • I have edited your title. Please see, "[Should questions include “tags” in their titles?](http://meta.stackexchange.com/questions/19190/)", where the consensus is "no, they should not". – John Saunders Jun 30 '13 at 18:32

2 Answers2

1

One way to do this would be with compiled expressions:

// helper classes which compiles a fast, type-safe delegate for writing various types
static class MyBinaryWriterHelper<T> {
    public static readonly Action<MyBinaryWriter, T> WriteAction;

    // this initialization is a bit expensive, but it will occur only once
    // for each writable type T and will occur lazily
    static {
        // find the existing Write(T) on the MyBinaryWriter type
        var writeMethod = typeof(MyBinaryWriter).GetMethods()
            .FirstOrDefault(m => m.Name == "Write" 
                && m.GetArguments().Length == 1
                && m.GetArguments()[0](p => p.ParameterType == typeof(T)
        );

        // if there is no such method, fail
        if (writeMethod == null) { throw ... }

        // build up an expression (writer, t) => writer.Write(t)
        var writerParam = Expression.Parameter(typeof(MyBinaryWriter));
        var tParam = Expression.Parameter(typeof(T));
        var call = Expression.Call(writerParam, writeMethod, tParam);
        var lambda = Expression.Lambda<Action<MyBinaryWriter, T>>(call, new[] { writerParam, tParam });

        // compile the expression to a delegate, caching the result statically in the
        // readonly WriteAction field
        WriteAction = lambda.Compile();
    }
}

// then in your writer class
public void Write<T>(IEnumerable<T> collection) {
    // other collection writing logic (e. g. writing the count) ...

    // to write out the items, just use the static action field
    foreach (var t in collection) {
        MyBinaryWriterHelper<T>.WriteAction(this, t);
    }
}

While there's no way to use generics to enforce that the type is "numeric", you can use IConvertible (as in your example code) as a loose constraint for this purpose to add extra compile-time safety.

ChaseMedallion
  • 20,860
  • 17
  • 88
  • 152
  • This exactly an approach I was thing to. Thanks for providing ready to use code. Very interesting the way you cache the compiled lambda. At present is the best answer, in my opinion. – unlikely Jun 30 '13 at 19:24
0

Best thing I can think is to use dynamic:

public void Write<T>(ICollection<T> collection) {
    dynamic self = this;
    foreach (var value in collection) {
        self.Write(value);
    }
}

The performance of dynamic invocation has been discussed by Eric. In short, its performance is somewhat equal to using compiled expression tree.

Community
  • 1
  • 1
tia
  • 9,518
  • 1
  • 30
  • 44
  • That's a really great post! It's worth noting that, in the other poster's benchmarks, the compiled expression tree was still more than twice as fast as the dynamic approach. That obviously won't matter in most cases, but a custom binary writer seems like one place where it might. – ChaseMedallion Jun 30 '13 at 17:41
  • The use of dynamic is very simple, and provide a great way to prototype and check the code. But for my needs, as @ChaseMedallion wrote, I think I need a better performing strategy. Thanks. – unlikely Jun 30 '13 at 19:27