Home > Blockchain >  Passing an array of differently typed Action<T> to a method
Passing an array of differently typed Action<T> to a method

Time:04-11

I have the following code, which stores a list of actions to perform based on the type of value passed as an argument...

Action<int> intAction = x => Console.WriteLine($"You sent an int: {x}");
Action<bool> boolAction = x => Console.WriteLine($"You sent a bool: {x}");

var funcsByInputType = new Dictionary<Type, Action<object>>();
funcsByInputType[typeof(int)] = (object x) => intAction((int)x);
funcsByInputType[typeof(bool)] = (object x) => boolAction((bool)x);

PerformAction(true);
PerformAction(42);

void PerformAction<TValue>(TValue value)
{
    if (funcsByInputType!.TryGetValue(typeof(TValue), out Action<object>? action))
    {
        action(value!);
    }
}

This works as expected.

What I now want to do is create the dictionary from a single method that I only call once, so I can rewrite my code like this instead.

Action<int> intAction = x => Console.WriteLine($"You sent an int: {x}");
Action<bool> boolAction = x => Console.WriteLine($"You sent a bool: {x}");

funcsByInputType = MakeFuncsByInputType(intAction, boolAction);

PerformAction(true);
PerformAction(42);

void PerformAction<TValue>(TValue value)
{
    if (funcsByInputType!.TryGetValue(typeof(TValue), out Action<object>? action))
    {
        action(value!);
    }
}

How could I write the MakeFuncsByInputType method?

Note that I don't want to make multiple calls to the method, and I don't want to have my parameters defined as params object[]

CodePudding user response:

This is not possible, because via an Action<object> as an alias of Action<string> I would be able to pass 42 instead of a string.

So I had to go with a builder pattern instead.

public static partial class Reducer { public static Builder New() => new Builder();

public class Builder<TState>
{
    private bool Built;
    private ImmutableArray<KeyValuePair<Type, Func<TState, object, Result<TState>>>> TypesAndReducers;

    internal Builder() => TypesAndReducers = ImmutableArray.Create<KeyValuePair<Type, Func<TState, object, Result<TState>>>>();

    public Builder<TState> Add<TDelta>(Func<TState, TDelta, Result<TState>> reducer)
    {
        if (reducer is null)
            throw new ArgumentNullException(nameof(reducer));

        EnsureNotBuilt();
        TypesAndReducers = TypesAndReducers.Add(new(typeof(TDelta), (state, delta) => reducer(state, (TDelta)delta)));
        return this;
    }

    public Func<TState, object, Result<TState>> Build()
    {
        EnsureNotBuilt();
        if (TypesAndReducers.Length == 0)
            throw new InvalidOperationException("Must add at least one reducer to build.");

        Built = true;

        var dictionary = TypesAndReducers
            .GroupBy(x => x.Key)
            .ToDictionary(x => x.Key, x => x.Select(x => x.Value));

        return (TState state, object delta) =>
        {
            if (delta is null)
                throw new ArgumentNullException(nameof(delta));

            if (!dictionary.TryGetValue(delta.GetType(), out var reducers))
                return (false, state);

            bool anyChanged = false;
            TState newState = state;
            foreach(var reducer in reducers)
            {
                (bool changed, newState) = reducer(newState, delta);
                anyChanged |= changed;
            }

            return anyChanged
                ? (true, newState)
                : (false, state);
        };
    }

    private void EnsureNotBuilt()
    {
        if (Built)
            throw new InvalidOperationException("Reducer has already been built.");
    }

}

}

  • Related