Juan M. Elosegui
Juan M. Elosegui

Reputation: 6981

Mixing Covariance and Contravariance

Goal:

Iterate over the following collection

var collection = new IImportTrigger<EventArgs>[]
{
    new FileSystemImportTrigger()
    , new TimerImportTrigger()
};

in this way

foreach (var trigger in collection)
{
    trigger.Import += trigger.OnImport;
}

This is what I have so far

public delegate void ImportTriggerEventHandler<in T>(object sender, T args) where T : EventArgs;

public interface IImportTrigger<out T> where T : EventArgs
{
    event ImportTriggerEventHandler<T> Import;
    void OnImport<T1>(object sender, T1 args) where T1 : EventArgs;
}

public class FileSystemImportTrigger : IImportTrigger<FileSystemEventArgs>
{
    public event ImportTriggerEventHandler<FileSystemEventArgs> Import;

    public void OnImport<T>(object sender, T args) where T : EventArgs {  }
}

public class TimerImportTrigger : IImportTrigger<ElapsedEventArgs>
{
    public event ImportTriggerEventHandler<ElapsedEventArgs> Import;

    public void OnImport<T>(object sender, T args) where T : EventArgs {  }
}

Expectations:

I would like to define the IImportTrigger with only one generic parameter.

Problem:

If I change my Interface definition to the following (notice the generic argument T is not covariant any more).

public interface IImportTrigger<T> where T : EventArgs
{
    event ImportTriggerEventHandler<T> Import;
    void OnImport(object sender, T args);
}

and hence

public class FileSystemImportTrigger : IImportTrigger<FileSystemEventArgs>
{
    public event ImportTriggerEventHandler<FileSystemEventArgs> Import;

    public void OnImport(object sender, FileSystemEventArgs args) { }
}

public class TimerImportTrigger : IImportTrigger<ElapsedEventArgs>
{

    public event ImportTriggerEventHandler<ElapsedEventArgs> Import;

    public void OnImport(object sender, ElapsedEventArgs args) { }
}

I wont be able to create a common type for my collection

var collection = new IImportTrigger<EventArgs>[]
{
    new FileSystemImportTrigger()
    , new TimerImportTrigger()
};

because the Generic parameter is not output-safe any more.

Question:

Is there any way to accomplish my scenario?

Upvotes: 3

Views: 125

Answers (1)

Scott Chamberlain
Scott Chamberlain

Reputation: 127553

By switching OnImport to be not generic at all then use a explicit interface, then make another more derived interface that is not covariant that has the generic verson of OnImport you could pull it off.

internal class Program
{
    private static void Main(string[] args)
    {
        var collection = new IImportTriggerBase<EventArgs>[]
        {
            new FileSystemImportTrigger()
            , new TimerImportTrigger()
        };

        foreach (var trigger in collection)
        {
            trigger.Import += trigger.OnImport;
        }
    }
}

public delegate void ImportTriggerEventHandler<in T>(object sender, T args) where T : EventArgs;

public interface IImportTriggerBase<out T> where T : EventArgs
{
    event ImportTriggerEventHandler<T> Import;
    void OnImport(object sender, EventArgs args);
}

public interface IImportTrigger<T> : IImportTriggerBase<T> where T : EventArgs
{
    void OnImport(object sender, T args);
}

public class FileSystemImportTrigger : IImportTrigger<FileSystemEventArgs>
{
    public event ImportTriggerEventHandler<FileSystemEventArgs> Import;

    public void OnImport(object sender, FileSystemEventArgs args) { }

    void IImportTriggerBase<FileSystemEventArgs>.OnImport(object sender, EventArgs args)
    {
        OnImport(sender, (FileSystemEventArgs)args);
    }
}

public class TimerImportTrigger : IImportTrigger<ElapsedEventArgs>
{
    public event ImportTriggerEventHandler<ElapsedEventArgs> Import;

    public void OnImport(object sender, ElapsedEventArgs args) { }

    void IImportTriggerBase<ElapsedEventArgs>.OnImport(object sender, EventArgs args)
    {
        OnImport(sender, (ElapsedEventArgs)args);
    }
}

However this does give you the extra cruft of the OnImport(object sender, EventArgs args) method which is visible on IImportTrigger<T>.


That was to solve your problem, if I where going to do this and I am assuming correctly you just want derived classes to be able to pick up on the fact that Import is getting fired and you actually do not need OnImport exposed I would just do

internal class Program
{
    private static void Main(string[] args)
    {
        var collection = new IImportTrigger<EventArgs>[]
        {
            new FileSystemImportTrigger()
            , new TimerImportTrigger()
        };
    }
}

public delegate void ImportTriggerEventHandler<in T>(object sender, T args) where T : EventArgs;

public interface IImportTrigger<out T> where T : EventArgs
{
    event ImportTriggerEventHandler<T> Import;
}

public abstract class OnImportBase<T> : IImportTrigger<T> where T: EventArgs
{

    public event ImportTriggerEventHandler<T> Import;

    protected virtual void OnImport(object sender, T args)
    {
        var tmp = Import;
        if (tmp != null)
        {
            tmp(this, args);
        }
    }
}

public class FileSystemImportTrigger : OnImportBase<FileSystemEventArgs>
{
    protected override void OnImport(object sender, FileSystemEventArgs args)
    {
        DoSomeExtraStuffBeforeImport();
        base.OnImport(sender, args);
    }

    private void DoSomeExtraStuffBeforeImport()
    {
    }
}

public class TimerImportTrigger : OnImportBase<ElapsedEventArgs>
{
    protected override void OnImport(object sender, ElapsedEventArgs args)
    {
        base.OnImport(sender, args);
        DoSomeExtraStuffAfterImport();
    }

    private void DoSomeExtraStuffAfterImport()
    {
    }
}

This gets rid of the event subscription and instead handles it as a override (Which is the normal pattern in .NET events).

Upvotes: 2

Related Questions