3

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.

Arsen Khachaturyan
  • 7,904
  • 4
  • 42
  • 42
SimoGecko
  • 88
  • 6
  • You could do something like: public class MyPacket : Packet, IReadContent ------------------ IReadContent can then be an implementation that returns a generic type Then whatever packetType is relevant for the instance of "MyPacket" will then have a Read method? Perhaps dependency inject on the constructor a "reader" that can be of different types instead? – Morten Bork Aug 24 '20 at 10:25

2 Answers2

2

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
2

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.
Arsen Khachaturyan
  • 7,904
  • 4
  • 42
  • 42
  • I think this changes the syntax from `int myInt = packet.ReadInt()` to `int myInt = packet.Read()` but doesn't address having multiple dynamic parameters in a single function – SimoGecko Aug 24 '20 at 10:49
  • @SimoGecko, I've made an update, although it's still worth thinking about how to return results via collection. – Arsen Khachaturyan Aug 24 '20 at 10:56
  • @SimoGecko, after investigating various options, back and forth, I've found another way, which seems a bit more complex but, anyway as a solution. That is done via using tuples. Unfortunately, other solutions seem even more complex for me. – Arsen Khachaturyan Aug 24 '20 at 12:39