0

I'm having a hard time to implement an Api for my Datastructure in C# using generics. Here's what I want to achieve:

    static void Main(string[] args) {
        Enterprise google = new Enterprise();
        new ConverterChain<Enterprise>(google)
            .convert(enterprise => enterprise.getWorkers())
            .convert(worker => worker.getAddresses())
            .doAction(address => Debug.Log(address.getStreet()));

    }

The fictional simplified example code does the following:

  • find all workers for google and display them on GUI
  • wait for user to select one worker
  • find all addresses for the selected worker and display them on GUI
  • wait for user to select one address
  • find all streets for the selected address and display them on GUI
  • wait for user to select one street and do some action with that street (i.e. write it to log)

As I want to do some asyncronous stuff I need to store all the Funcs and Actions in one datastructure for further execution. In order to have a nice Api I need to use generics. Here's what I have until now:

public class ConverterChain<I> {
    I input;

    public ConverterChain(I input) {
        this.input = input;
    }

    public Converter<I, O> convert<O>(Func<I, List<O>> c) {
        return new Converter<I, O>(c);
    }
}

public class Converter<I, O> {
    public Func<I, List<O>> c { get; private set; }

    public Converter(Func<I, List<O>> c) {
        this.c = c;
    }

    public Converter<O, N> convert<N>(Func<O, List<N>> c) {
        return new Converter<O, N>(c);
    }

    public void doAction(Action<O> action) {
        // and now bring somehow all converters into a single structure
    }
}

Coming from Java I would do something like that:

public void doAction(Action<O> action) {
    List<Converter<?, ?>> allConverters = getConvertersFromParents();
    object rootObject = getInputFromRoot();

    GUIHandler.doComplicatedAsynchronousStuff(rootObject, allConverters, action);
}

Without Converter<?, ?> no matter how I implement those data structures, I don't get the step done to convert all the Lambdas into one data structure. I fail to create the parent-child structure in Converter<I, O> in a clean generic way as each converter has different Is and Os.

Using something like Converter<object, object> fails as for whatever reason

Converter<I, O> myConverter = ...
Converter<object, object> genericConverter = (Converter<object, object>) myConverter;

is not allowed.

What is the C# way to implement my given scenario?

mibutec
  • 2,929
  • 6
  • 27
  • 39
  • Are those generics actually needed? For example does the Converter actually need to know that what it gets is a List of a certain kind via convert? Would a non generic IEnumerable be enough instead of a List. – Ralf Apr 06 '23 at 12:32
  • It looks like the `Converter` ctor has typos. It takes a `Func> m` parameter which is never used and it assigns a `c` which is not defined. But class property `c` is of type `Func`, not `Func>` so `m` can't be assigned to `this.c`. – Jonathan Dodds Apr 06 '23 at 12:35
  • Second thought isn't this just some Linq replacement and you just want `google.GetWorkers().SelectMany(x => x.getAddresses()).ToList().ForEach(x => Debug.Log(x.getStreet());`? (Syntax may be slightly wrong) – Ralf Apr 06 '23 at 12:41

3 Answers3

1

I think Java uses type erasure to implement generics. It was a long time I coded java, but I think <?> means "shut of the compile time type checks". In c# the type safety of generic is actually guaranteed by the runtime, so there is no direct equivalence. You could probably extract a list of all the converters if you added a non generic interface. But you can likely not use Func, each converter would need to actually reference each other. Such an interface would likely have to use object to describe input/output.

However, it seem to me like your code is needlessly complicated. If I where to write an asynchronous version of your specifications I would do something like:

public static async Task<Street> GetStreet(
    Enterprise enterprise, 
    Func<Task<T>, T[]> selection)
{
    var workers = enterprise.GetWorkers();
    var worker = await selection(workers );
    var addresses = worker.GetAddresses();
    var address = await selection(addresses);
    var streets = address.GetStreets();
    var street = await selection(streets);
    return street;
}

That seem to match your functionality description line for line, more so than your code example. You would ofc need to write a dialog to select items, but I assume that is out of scope.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • In the end you were right. Rewriting my GUI code so it is easier to use and get rid of that generic builder things saved me tons of code and made the solution even more flexible. – mibutec Apr 08 '23 at 06:17
0

Even if Converter<object, object> genericConverter = (Converter<object, object>) myConverter; could compile it would not be type-safe in general case (that is the reason why it does not compile, for more - check out variance in C# and answers like this or this one).

What you can do - introduce non-generic base class so you can put any converter in a single collection. For example like this:

public abstract class Converter
{
    public bool CanActOn<T>() => TryGetActor<T>() is not null;
    protected abstract IActOn<T>? TryGetActor<T>();

    public void DoAction<O>(Action<O> action)
    {
        if (TryGetActor<O>() is { } act)
        {
            act.doAction(action);
        }
        else
        {
            // throw/ignore ?
        }
    }
}

public class Converter<I, O> : Converter, IActOn<O>
{
    // ....

    // maybe make explicit interface implementation
    public void doAction(Action<O> action) {
        // and now bring somehow all converters into a single structure
    }

    protected override IActOn<T>? TryGetActor<T>() => this as IActOn<T>;
}

public interface IActOn<T>
{
    void doAction(Action<T> action);
}

If the goal is to only have the doAction exposed - then you can just use IActOn<O>.

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

The disappointing solution I was able to find was to introduce a Cast-methods converting my generic methods I only required for nice Api into their <object> equivalents.

public class ConverterChain<I> {
    ...

    internal ConverterChain<object> Cast() {
        return new ConverterChain<object>(input);
    }
}

public class Converter<I, O> {
    ...

    internal Converter<object, object> Cast() {
        Func<object, List<object>> newC = o => ((List<O>) c.DynamicInvoke(o)).Cast<object>().ToList();
        return new ConverterNode<object, object>(newC);
    }
}

Those <object> equivalents could be used to chain my converters and create the resulting data structure :-/

mibutec
  • 2,929
  • 6
  • 27
  • 39