Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No type inference with generic extension method

I have the following method:

public static TEventInvocatorParameters Until
    <TEventInvocatorParameters, TEventArgs>(this TEventInvocatorParameters p,
                                            Func<TEventArgs, bool> breakCond)
    where TEventInvocatorParameters : EventInvocatorParameters<TEventArgs>
    where TEventArgs : EventArgs
{
    p.BreakCondition = breakCond;
    return p;
}

And this class

public class EventInvocatorParameters<T>
    where T : EventArgs
{
    public Func<T, bool> BreakCondition { get; set; }
    // Other properties used below omitted for brevity.
}

Now, I have the following problems:

  1. This extension method shows on all types, even string.
  2. I can't write new EventInvocatorParameters<EventArgs>(EventABC).Until(e => false); It is telling me "The type arguments for method ... cannot be inferred from the usage."

Can't I use generic type parameters like this? How would you resolve this problem?
Important point: I need both of those generic parameters, because I need to return the same type this extension method was called on.


Broader picture (not necessary for answering the question!):
I am trying to create a fluent interface to invoking events. The base is this static class:

public static class Fire
{
   public static void Event<TEventArgs>(
       ConfiguredEventInvocatorParameters<TEventArgs> parameters)
    where TEventArgs : EventArgs
    {
        if (parameters.EventHandler == null)
        {
            return;
        }

        var sender = parameters.Sender;
        var eventArgs = parameters.EventArgs;
        var breakCondition = parameters.BreakCondition;

        foreach (EventHandler<TEventArgs> @delegate in 
                 parameters.EventHandler.GetInvocationList())
        {
            try
            {
                @delegate(sender, eventArgs);
                if (breakCondition(eventArgs))
                {
                    break;
                }
            }
            catch (Exception e)
            {
                var exceptionHandler = parameters.ExceptionHandler;
                if (!exceptionHandler(e))
                {
                    throw;
                }
            }
        }
    }
}

To make sure this method can only be called with fully configured parameters, it only accepts a ConfiguredEventInvocatorParameters<T> which derives from EventInvocatorParameters<T>:

public class ConfiguredEventInvocatorParameters<T>
    : EventInvocatorParameters<T>
    where T : EventArgs
{
    public ConfiguredEventInvocatorParameters(
        EventInvocatorParameters<T> parameters, object sender, T eventArgs)
        : base(parameters)
    {
        EventArgs = eventArgs;
        Sender = sender;
    }

    public T EventArgs { get; private set; }
    public object Sender { get; private set; }

}

The following would be valid calls:

Fire.Event(EventName.With(sender, eventArgs));
Fire.Event(EventName.With(sender, eventArgs).Until(e => e.Cancel));
Fire.Event(EventName.Until(e => e.Cancel).With(sender, eventArgs));

The following would be invalid:

// no sender or eventArgs have been specified, i.e. missing call to With(...)
Fire.Event(EventName.Until(e => e.Cancel));

To make this work, there exist extension methods named With, that accept either a EventHandler<TEventArgs or a TEventInvocatorParameters and return a ConfiguredEventInvocatorParameters<TEventArgs>. All calls following the With now also need to return the type ConfiguredEventInvocatorParameters<TEventArgs>, otherwise the second example of a valid call (with the Until at the end) wouldn't work.
If you have any thoughts on the API in general, please let me know. However, I want to avoid the following three things:

  • Fail only at runtime if the parameters have not been configured fully
  • Creating an inverse syntax like EventName.With(...).Until(...).Fire()
  • Use the infamous Do method to start off things: Fire(EventName).With(...).Until(...).Do();
like image 613
Daniel Hilgarth Avatar asked Aug 24 '11 05:08

Daniel Hilgarth


3 Answers

UPDATE from November 2020: The original answer below was written in 2011; the rules for generic method type inference, overload resolution, and how "final validation" of methods is done have had small but significant changes in recent versions of C#; this answer, and the link to an archived article on my original MSDN blog about it might no longer be accurate. Also, Microsoft deleted the comments on the original article for legal reasons; there was a huge amount of context and discussion in those comments. I hope to at some point have the time to revisit this article to clarify (1) the rules today, (2) how they have changed, and (3) how the ideas discussed in those deleted comments influenced those decisions, but that's a lot of work and I may not get to it for some time. Remember, I have not been on the C# language design team since November 2012.


Generic method type inference deliberately does not make any deductions from the constraints. Rather, deductions are made from the arguments and the formal parameters, and then the deduced type arguments are checked against the constraints.

For a detailed discussion of some of the design issues around constraints and method signatures, including several dozen people telling me that I'm wrong to think that the existing design is sensible, see my article on the subject:

https://learn.microsoft.com/en-gb/archive/blogs/ericlippert/constraints-are-not-part-of-the-signature

like image 55
Eric Lippert Avatar answered Nov 15 '22 17:11

Eric Lippert


For anyone interested, for now, I solved the original problem (fluent event invocation API) with a generic class hierarchy. This is basically Hightechrider's answer on steroids.

public abstract class EventInvocatorParametersBase
    <TEventInvocatorParameters, TEventArgs>
    where TEventArgs : EventArgs
    where TEventInvocatorParameters :
        EventInvocatorParametersBase<TEventInvocatorParameters, TEventArgs>

{
    protected EventInvocatorParametersBase(
        EventHandler<TEventArgs> eventHandler,
        Func<Exception, bool> exceptionHandler,
        Func<TEventArgs, bool> breakCondition)
    {
        EventHandler = eventHandler;
        ExceptionHandler = exceptionHandler;
        BreakCondition = breakCondition;
    }

    protected EventInvocatorParametersBase(
        EventHandler<TEventArgs> eventHandler)
        : this(eventHandler, e => false, e => false)
    {
    }

    public Func<TEventArgs, bool> BreakCondition { get; set; }
    public EventHandler<TEventArgs> EventHandler { get; set; }
    public Func<Exception, bool> ExceptionHandler { get; set; }

    public TEventInvocatorParameters Until(
        Func<TEventArgs, bool> breakCondition)
    {
        BreakCondition = breakCondition;
        return (TEventInvocatorParameters)this;
    }

    public TEventInvocatorParameters WithExceptionHandler(
        Func<Exception, bool> exceptionHandler)
    {
        ExceptionHandler = exceptionHandler;
        return (TEventInvocatorParameters)this;
    }

    public ConfiguredEventInvocatorParameters<TEventArgs> With(
        object sender, 
        TEventArgs eventArgs)
    {
        return new ConfiguredEventInvocatorParameters<TEventArgs>(
            EventHandler, ExceptionHandler, BreakCondition,
            sender, eventArgs);
    }
}

public class EventInvocatorParameters<T> :
    EventInvocatorParametersBase<EventInvocatorParameters<T>, T>
    where T : EventArgs
{
    public EventInvocatorParameters(EventHandler<T> eventHandler)
        : base(eventHandler)
    {
    }
}

public class ConfiguredEventInvocatorParameters<T> :
    EventInvocatorParametersBase<ConfiguredEventInvocatorParameters<T>, T>
    where T : EventArgs
{
    public ConfiguredEventInvocatorParameters(
        EventHandler<T> eventHandler,
        Func<Exception, bool> exceptionHandler,
        Func<T, bool> breakCondition, object sender,
        T eventArgs)
        : base(eventHandler, exceptionHandler, breakCondition)
    {
        EventArgs = eventArgs;
        Sender = sender;
    }

    public ConfiguredEventInvocatorParameters(EventHandler<T> eventHandler,
                                              object sender,
                                              T eventArgs)
        : this(eventHandler, e => false, e => false, sender, eventArgs)
    {
    }

    public T EventArgs { get; private set; }
    public object Sender { get; private set; }
}

public static class EventExtensions
{
    public static EventInvocatorParameters<TEventArgs> Until<TEventArgs>(
        this EventHandler<TEventArgs> eventHandler,
        Func<TEventArgs, bool> breakCondition)
        where TEventArgs : EventArgs
    {
        return new EventInvocatorParameters<TEventArgs>(eventHandler).
            Until(breakCondition);
    }

    public static EventInvocatorParameters<TEventArgs> 
        WithExceptionHandler<TEventArgs>(
            this EventHandler<TEventArgs> eventHandler,
            Func<Exception, bool> exceptionHandler)
        where TEventArgs : EventArgs
    {
        return
            new EventInvocatorParameters<TEventArgs>(eventHandler).
                WithExceptionHandler(exceptionHandler);
    }

    public static ConfiguredEventInvocatorParameters<TEventArgs>
        With<TEventArgs>(
            this EventHandler<TEventArgs> eventHandler, object sender,
            TEventArgs eventArgs)
        where TEventArgs : EventArgs
    {
        return new ConfiguredEventInvocatorParameters<TEventArgs>(
            eventHandler, sender, eventArgs);
    }
}

This allows you to write code like this:

Fire.Event(EventName.WithExceptionHandler(e => false)
                    .Until(e => false).With(this, EventArgs.Empty));
Fire.Event(EventName.With(this, EventArgs.Empty));
Fire.Event(EventName.WithExceptionHandler(e => false)
                    .With(this, EventArgs.Empty).Until(e => false));
Fire.Event(EventName.With(this, EventArgs.Empty)
                    .WithExceptionHandler(e => false).Until(e => false));

But it doesn't allow you to write this, because not all necessary info (eventArgs and sender) has been provided:

Fire.Event(EventName.Until(e => false));
Fire.Event(EventName);
like image 32
Daniel Hilgarth Avatar answered Nov 15 '22 18:11

Daniel Hilgarth


Is there some reason you need to use an extension method? If you put Until on the EventInvocatorParameters<T> class you can avoid both of the problems mentioned:

public class EventInvocatorParameters<T>
    where T : EventArgs
{
    public Func<T, bool> BreakCondition { get; set; }
    // Other properties used below omitted for brevity.

    public EventInvocatorParameters<T> Until (Func<T, bool> breakCond)
    {
        this.BreakCondition = breakCond;
        return this;
    }
}
like image 38
Ian Mercer Avatar answered Nov 15 '22 18:11

Ian Mercer