Dynamic parameter list of different out types in C#

Viewed 803

I have an object that provides several functions to write and read data from a packet, something like this:

class Packet
{
    void    Write(int value) {/*...*/}
    //...

    int     ReadInt()     {/*...*/}
    bool    ReadBool()    {/*...*/}
    string  ReadString()  {/*...*/}
    Vector3 ReadVector3() {/*...*/}
}

this class stores a byte[] that is sent over the network. If I want to access data previously written, I can do it like this

void MyFunction(Packet packet)
{
    int     myInt     = packet.ReadInt();
    bool    myBool    = packet.ReadBool();
    string  myString  = packet.ReadString();
    Vector3 myVector3 = packet.ReadVector3();
    //... do stuff
}

I was wondering if there is some syntactic sugar that would allow me to define a function taking a variable number of parameters of different types, detect which dynamic types they are at runtime, call the appropriate function and then return the parameter, something like this:

class Packet
{
    //...
    void ReadAll(out params object[] objects);
}

void MyFunction(Packet packet)
{
    packet.ReadAll(out int myInt, out bool myBool, out string myString, out Vector3 myVector3);
    //... do stuff with myInt, myBool, myString, myVector3
}

I looked at params with an object[], the out keyword, generics, and Convert.ChangeType() but I couldn't get anything to work so far. I'm not sure this is even possible, and if it is, if the runtime cost of reflection will highly outweigh the benefits of simpler/less code for something used as frequently as network packets.

Thank you all.

2 Answers

You can try using generics and ValueTuples:

public T Read<T>() where T:ITuple
{
    return default(T); // some magic to create and fill one
}

and usage:

var (i, j) = Read<(int, int)>();
// or
var x = Read<(int i, int j)>();

As for reflection - you can cache reflection "results" per type:

public T Read<T>() where T : struct, ITuple
{
    return TupleCreator<T>.Create(new ValueReader());
}

static class TupleCreator<T> where T : struct, ITuple
{
    private static Func<ValueReader, T> factory; 
    
    static TupleCreator()
    {
        var fieldTypes = typeof(T).GetFields()
            .Select(fi => fi.FieldType)
            .ToArray();
            
        if(fieldTypes.Length > 7)
        {
            throw new Exception("TODO");
        }

        var createMethod = typeof(ValueTuple).GetMethods()
            .Where(m => m.Name == "Create" && m.GetParameters().Length == fieldTypes.Length)
            .SingleOrDefault() ?? throw new NotSupportedException("ValueTuple.Create method not found.");

        var createGenericMethod = createMethod.MakeGenericMethod(fieldTypes);
        
        var par = Expression.Parameter(typeof(ValueReader));
        // you will need to figure out how to find your `read` methods
        // but it should be easy - just find a method starting with "Read"
        // And returning desired type
        // I can do as simple as:
        var readMethod = typeof(ValueReader).GetMethod("Read");
        var readCalls = fieldTypes
            .Select(t => readMethod.MakeGenericMethod(t)) // can cache here also but I think not needed to
            .Select(m => Expression.Call(par, m));
        var create = Expression.Call(createGenericMethod, readCalls);
        factory = Expression.Lambda<Func<ValueReader, T>>(create, par).Compile();
    }
    
    public static T Create(ValueReader reader) => factory(reader);
}

class ValueReader
{
    public T Read<T>()
    {
        return default; // to simulate your read
    }
}

Console.WriteLine(Read<(int i, double j)>()); // prints "(0, 0)"

NB

Also you can just implement the Read<T> method with "caching" the reflection as done here.

Yes, Reflection is always having a cost of Performance, but it's not always so big, especially when you are not using reflective calls via big size collections or dealing with a very complicated code (here is a Microsoft official tutorial about when to use the Reflection).

Anyway, back to our business, using a reflection here is what I think might help you:

class Packet {
    
    ...

    public T Read<T>()
    {
        var currentType = Type.GetType(this.GetType().ToString());

        var methodInfo = currentType?
            .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) // Getting all "private" read methods
            .FirstOrDefault(m =>
                m.Name.Contains("Read") && m.ReturnType == typeof(T));


        // here it is assuming that you will have to have only 1 Read method with the type int, bool, etc.
        return (T) methodInfo?.Invoke(this, null);
    }
}

public List<object> ReadAllAsCollection(params Type[] types) 
{
    var result = new List<object>(); // in order to hold various type you need to have a collection of `object` type elements
    foreach (Type type in types)
    {
        MethodInfo method = typeof(Packet).GetMethod("Read");

        MethodInfo genericMethod = method?.MakeGenericMethod(type);
        var res = genericMethod?.Invoke(this, null); // calling Read<T> with appropriate type
        result.Add(res);        
    }

    return result;
}

public T ReadAllAsTuple<T>() where T : ITuple
{
    T CreateValueTuple(List<object> items, Type[] inputTypes)
    {
        var methodInfo = typeof(ValueTuple)
            .GetMethods()
            .FirstOrDefault(m =>
                m.Name.Contains("Create") &&
                m.GetParameters().Length == items.Count);

        var invokeResult = methodInfo?.MakeGenericMethod(inputTypes)
            .Invoke(null, items.ToArray());

        return (T)invokeResult;
    }

    var tupleType = typeof(T);
    var types = tupleType.GetFields().Select(f => f.FieldType).ToArray();
    
    var result = types
        .Select(t =>
        {
            var method = typeof(Packet).GetMethod("Read");

            var genericMethod = method?.MakeGenericMethod(t);
            return genericMethod?.Invoke(this, null); // calling Read<T> with appropriate type
        }).ToList();

    return result.Any() ?
        CreateValueTuple(result, types)
        : default;
}

And a usage:

var p = new Packet();
var elm = p.Read<int>(); // single instance call, ReadInt() call is encapsulated
var resultAsCollection = p.ReadAllAsCollection(typeof(int), typeof(string));
var resultAsTuple = p.ReadAllAsTuple<ValueTuple<int, string, bool>>();

A few notes:

  1. As you probably want a single exposed public method (Read), it would be good to have other methods declared as private.
  2. In this case, you have simple method names, but to escape possible name conflicts, you can use some specific naming conventions, like: _ReadInt_() or any other namings you prefer. Though, this example will work even without specific namings.
  3. It is assumed that you will have only one method with the name Read... and return type of T (for example int ReadInt()) because, for the above case, we are using only the first match.
Related