0

I want to call a function on each member of an object in C#. What is the best way to do this?

Below are some details of my exact situation, I do not believe this is necessary to answer the question but some people have asked for more info:

I have a class with a lot of members, over 100. Unfortunately, this is unavoidable as this is a simulation software package. These variables are all captured in State objects and timestamped.

A second class called a Delta represents the differences between one State and another. For example, if State1 has x, y, z = 0, 0, 0 and State2 has x, y, z = 1, 0, 0: then the Delta from State1 to State2 has x, y, z = 1, null, null.

Deltas are then serialized into protocol buffers and sent across the network, to reconstruct the current state at the other end.

States have the following methods: GetDelta(State reference) which returns the Delta between the reference State (the older state) and the State object, ApplyDelta(Delta delta) which updates the state with the information from the delta (in the example above if you called State1.ApplyDelta(delta) you would receive State2), and Interpolate(State reference, double factor) which interpolates between the reference state and the state according to the factor (a number between 0 and 1).

States are actually an abstract idea. In the code, there are many different kinds of states - for the weather, the world, various simulation objects - but they all implement an interface that requires them to implement those 3 methods.

In a trivial case implementing all of these functions is simple. Even if it's a complex case, it's simple - but it is a lot of lines of code. 100 members require 100 lines to declare the members in the state, 100 lines to declare the members in the delta, 100 lines for each of the 3 methods above - that's 500 lines of code that are potentially error-prone.

How can I call the same function on every member of an object? -

I have considered putting all the members into a dictionary, list, or array, but I don't think it would be very clean since the data is heterogeneous. I could have one collection per type, which would be a lot cleaner but would tightly couple the data type to the way it's handled.

I know this is a long post for a simple question, but obviously a lot of architectural decisions have been made already and I didn't want to present an XY problem. Nothing is set in stone, if there's a better architecture then I will consider it.

Here is some example code of how states function: ```C# interface IInterpolatable<S, D> { D GetDelta(S reference); S ApplyDelta(D delta); S Interpolate(S reference, double factor); }

class State : IInterpolatable<State, State.Delta> { public int a; public decimal b; public boolean c;

class Delta
{
    public int? a;
    public decimal? b;
    public boolean? c;
}

public Delta GetDelta(State reference)
{
    if (reference == null)
    {
        return GetDelta(new State());
    }
    else
    {
        return new Delta
        {
            a = reference.a != a ? a : null,
            b = reference.b != b ? b : null,
            c = reference.c != c ? c : null,
        };
    }
}

public State ApplyDelta(Delta delta)
{
    if (delta == null)
    {
        return ApplyDelta(new Delta());
    }
    else
    {
        return new State
        {
            a = delta.a ?? a,
            b = delta.b ?? b,
            c = delta.c ?? c,
        };
    }
}

public State Interpolate(State reference, double factor)
{
    if (reference == null)
    {
        return Interpolate(new Ship(), factor);
    }
    else
    {
        // Note: There are linear interpolation functions for the used types
        return new Ship
        {
            a = lerp(reference.a, a, factor),
            b = lerp(reference.b, b, factor),
            c = lerp(reference.c, c, factor),
        };
    }
} } ```

You can imagine the data flow as something like:

  1. The simulation generates a new state
  2. A delta between that state and the previous is generated using GetDelta
  3. The delta is sent across the network to the client
  4. The client regenerates the new state by using ApplyDelta on the previous state
  5. The client then interpolates between the previous to the new state using Interpolate

Of course it's more complex than that, but that's the gist of it.

EDIT: Some people were interested to know how syncing between client and server works in this model.

Deltas are wrapped in a packet that contains a timestamp, and the timestamp of the reference state. Then they are sent to the client. When the client receives the packet they send back an acknowledgment with the timestamp. The server tracks the newest timestamp acknowledged by the client and generates the delta between that acknowledged state and the state it wants to send to the client.

Example (each number is 1 timestep, network latency is 1 step):

  1. The server grabs new state S1, since it has not received any acks from the client it generates a delta D1 between an empty state and S1. This state is sent across the network to the client. The client is waiting.
  2. The server grabs S2, still no acks so D2 is generated between an empty state and S2. The client receives D1, and uses it to generate the state S1. The client sends an ack back to the server.
  3. The server receives the ack from the client for S1. The server grabs S3, and generates D3. Since S1 was acked D3 is the delta between S3 and S1. The client is waiting - looks like D2 never showed up!
  4. The server grabs S4, and again since S1 was the latest state acked, D4 is the delta between S4 and S1. The client receives D3, generates S3, and sends back an ack.
  5. etc

Someone gave an example: "what would the server do if x increments every state, but 5 packets fail to be sent to the client?" The answer is that since deltas are relative to something, it will first send x+1, then x+2, then x+3, then x+4, then x+5, then finally it receives an ack so it can send x+1 next.

Sorry if that isn't clear, I can provide more information to clear it up if there's a problem.

  • 3
    I think it would be better to show the code example of declaring these states. likely you could just write a helper method, but its hard to tell from the abstract wording of the question, where as it would be easier to just digest the code – TheGeneral Sep 09 '20 at 00:19
  • @MichaelRandall Hi, thanks for your comment. I have added some example code showing how the described mechanisms work! –  Sep 09 '20 at 02:13
  • *500 lines of code that are potentially error-prone* Even if you could loop through the members, you can't escape the need to declare them. Have you considered code generation, i.e. writing a program to write the code? . – John Wu Sep 09 '20 at 03:24
  • 1
    Out of interest, what happens if some delta packets get lost on the network? Imagining a simulation of filling a bath tub, where our only property is "Level" to show the % of how full the tub is: The "host" creates a new Tub object, set's Level to 0. The "client" gets that full state on startup. Now the "host" generates a new Delta (+1) every 1 second. After 100 seconds the "host" stops A network outage lasts 5 seconds. The "client" received "Level += 1" 95 times but missed those 5 in the middle. Is our end state that the "host" says 100% but the "client" says 95%? How do they sync up? – Jeff Whitty Sep 09 '20 at 04:04
  • @JeffWhitty Like I said, it's more complex than I posted :P There are fairly standard solutions to this problem. I will try and answer simply. Before being transmitted deltas are wrapped in a packet that has a timestamp and the timestamp of the referenced state (eg T = 100, R = 95). The client sends an ACK back to the server when they receive a delta, this ACK contains the timestamp of the packet that was received. Next time the server wants to generate a packet, it knows what the last ACK'ed timestamp was, and generates the delta between the newest state and that reference state. more in post –  Sep 09 '20 at 06:11
  • @JohnWu Yep, code generation is an option too. I'm not sure the best way to go about it in c#.net but would be interested in an answer! –  Sep 09 '20 at 06:28
  • I'm writing my own code generation in python, that might be the solution here, but I'm sure someone with C# code generation experience might be able to demonstrate something nicer! –  Sep 09 '20 at 07:16
  • Not entirely sure why this question was closed, if someone could suggest a more focused question that "calling a function on every member of an object" then I would be thankful. I can't really think of a way to focus it more. –  Sep 09 '20 at 07:17
  • If anyone has read this post and thought they can see a second question in there, please let me know so I can remove it!!! –  Sep 10 '20 at 01:19
  • @user-024673 - Fair enough, just wanted to see you'd considered it really. – Jeff Whitty Sep 10 '20 at 07:04

2 Answers2

1

As John Wu commented on your question, this is actually better tackled with code generation, far more maintainable, less noise in source control on changes, higher performance, etc.

But let's say code generation isn't a viable option, perhaps due to conflicting tooling (I'd assume the State is code-generated in the first place). In those cases, reflection is possible, and you can indeed iterate through each member and apply functions on them. The general pattern will be

foreach (var field in typeof(ClassA).GetFields()) //iterate through fields
{
    var valueA1 = field.GetValue(instanceA1); //get the value from instanceA1
    var valueA2 = field.GetValue(instanceA2); //get the value from instanceA2
    var fieldB = typeof(ClassB).GetField(field.Name); //counterpart field from other class
    var valueB = fieldB.GetValue(instanceB); //get the value from an instance of other class

    //do stuff with those values

    field.SetValue(instanceA3, newValue); //you can update any instance, including the previous
    fieldB.SetValue(instanceB2, newValue);
    }
}

With that, the else block of your GetDelta becomes

var delta = new Delta();
var fields = typeof(State).GetFields();
foreach (var field in fields)
{
    var refValue = field.GetValue(reference) as IComparable;
    var thisValue = field.GetValue(this) as IComparable;
    if (refValue.CompareTo(thisValue)!=0)
    {
        typeof(Delta).GetField(field.Name).SetValue(delta, thisValue);
    }
}
return delta;

This will work as long as all field implement IComparable. Otherwise, you'd either have to filter them out or add a special method to compare them.

The else block for SetDelta is just the other way around

var state = new State();
var fields = typeof(State).GetFields();
foreach (var field in fields)
{
    var deltaValue = typeof(Delta).GetField(field.Name).GetValue(delta);
    var thisValue = field.GetValue(this);
    field.SetValue(state, deltaValue??thisValue);
}
return state;

Interpolate is a little bit involved, while

var ship = new Ship();
var fields = typeof(State).GetFields();
foreach (var field in fields)
{
    var refValue = field.GetValue(reference);
    var thisValue = field.GetValue(this);
    field.SetValue(ship, lerp(refValue,thisValue,factor));
}
return ship;

look straightforward, lerp essentially just pass those values as object, so you'll need to manually sort them out per type, there's no reflection magic you can use here, though you can use pattern matching to improve readability a bit

private object lerp(object a1, object a2, double factor)
{
    switch (a1)
    {
        case int intA1: return lerp(intA1, (int)a2, factor);
        case decimal decA1: return lerp(decA1, (decimal)a2, factor);
        case bool boolA1: return lerp(boolA1,(bool)a2,factor);
        default: return 0;
    }
}

While we're at it, you can even override the GetHashCode for State, treating the fields as a collection

public override int GetHashCode()
{
    unchecked
    {
        var hash = 19;
        foreach (var field in typeof(State).GetFields())
        {
            hash = hash * 31 + field.GetValue(this).GetHashCode();
        }
        return hash;
    }
}

so even without the ACK mechanism, you can include the resulting hashcode on transmission, and verify after applying delta that the resulting State produces identical hashcode.

Martheen
  • 4,893
  • 4
  • 28
  • 51
  • Thanks for the thorough answer. I actually wrote this code using reflection originally, but I received a lot of feedback that reflection is slow in c#. Net code needs to run many times per second, but not thousands or millions, so I'm not sure how much of a concern this is. I would be very interested in a code generation answer, I'm not experienced in c#.net code generation. Thanks again for your answer, it's a good read and a good solution! –  Sep 09 '20 at 06:33
  • 1
    Oh, reflection isn't that slow. With 203 members, it can run a thousand iterations of generating two State, randomizing every single member (through reflection), generating Delta from the first two State, apply the Delta, run one interpolation, all in less than a second https://dotnetfiddle.net/qpGd2l It's optimized a bit by not requesting the fields from reflection on every iteration, but even on my old i3 laptop the unoptimized version still takes less than a second. In production, things should be even faster since applying the delta happen only on the receiver. – Martheen Sep 09 '20 at 08:05
  • Thanks, I'll do some tests with reflection vs code generation and see what's fastest. Appreciate the information! –  Sep 09 '20 at 08:55
  • I've done exactly this when I needed something similar, making a deep copy of things passed across an uncontrolled boundary. While reflection is "much slower" than normal code, sometimes you need to consider that trade off on performance vs maintainability or just flat out ease of implementation. And, taking 3ms over 1ms is 200% slower, but are you calling it more than once every 3ms? (keep in mind those numbers are just for example, not a real measure of anything) – Jeff Whitty Sep 10 '20 at 07:08
  • 1
    Oh also a gotcha that caught me doing that - I added a property to the class that I didn't want copied, but with this looping reflection you need to write in some smarts to ignore certain properties. – Jeff Whitty Sep 10 '20 at 07:09
  • @JeffWhitty Yeah, the "much slower" used to make me hesitate on using reflection on a tight loop. But turns out even for a ridiculous number of fields, it's still speedy enough, each of those reflection calls must only take less than a millisecond since the whole benchmark never pass a second to run. – Martheen Sep 10 '20 at 08:03
  • At this point have you just turned the class into a inefficient dictionary? Why not use a dictionary instead? – MikeJ Sep 11 '20 at 16:27
0

I ended up just using python for code generation.

class DataTypes:
  INT = 'int'
  DOUBLE = 'double'
  ...

class Field:
  def __init__(self, name, data_type):
    self.name = name
    self.data_type = data_type

  @property
  def definition(self):
    return f'public {self.type} {self.name};'

  ...

class State:
  def __init__(self, name, fields):
    self.name = name
    self.fields = fields

  @property
  def definition(self):
    lines = []

    lines.append(f'public class {self.name}')
    lines.append('{')

    for field in self.fields:
      lines.append(field.definition)

    ...

    lines.append('}')

    return '\n'.join(lines)

  ...

foo = State('Foo', [
  Field('a', DataTypes.INT),
  Field('b', DataTypes.DOUBLE),
]

Works great, maintainable, fast, simple.