-1

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

1 Answers1

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.