简体   繁体   中英

Variant-compliant C# events inside a covariant generic interface

I have the following interfaces and delegate:

// Base devices, non-generic
public interface IDevice
{
    event DeviceVarChange VarChange;
}

public interface IVar
{
    // ...
}

public delegate void DeviceVarChange(IVar theVar);

I want to be able to create more specific interfaces with more specific event arguments and, at the same time, have them compatible and "variant-compliant" with the base interfaces above. For example:

// Specific devices, non-generic
public interface ISpecificDevice : IDevice
{
    // Problem: I have my "VarChange" event, but it uses IVar!
    // I want it to use "ISpecificVar"!
}

public interface ISpecificVar
{
    // ...
}

So I went for a generic solution. Since I want my subinterfaces to be "variant-compliant" and clients only "consumes" variables from my devices, I tried to use covariance:

// Base devices, generic
public interface IDevice<out TVar> where TVar : IVar
{
    // Compilation error: TVar must be invariant but is covariant in this context
    event DeviceVarChange<TVar> VarChange;
}

public interface IVar
{
    // ...
}

// Problem: Delegate arguments can be contravariant, but not covariant
public delegate void DeviceVarChange<TVar>(TVar theVar) where TVar : IVar;

But I have the issue that delegate arguments cannot be covariant. Making the delegate arguments contravariant would resolve the compilation error, but then I would no longer be able to use a ISpecificDevice like a IDevice<IVar> for events (runtime error, delegates must be of the same type).

public interface IDevice<out TVar> where TVar : IVar
{
    // No more compilation error, "DeviceVarChange" is contravariant
    event DeviceVarChange<TVar> VarChange;
}

public interface IVar
{
    // ...
}

public delegate void DeviceVarChange<in TVar>(TVar theVar) where TVar : IVar;

public interface ISpecificDevice : IDevice<ISpecificVar>
{
    // The event is using "ISpecificVar" here, as wanted
}

public interface ISpecificVar : IVar
{
    // ...
}

// + a "SpecificDevice" class implementing "ISpecificDevice"

// Try to use a specific instance as a base one
public class Test
{
    public void DoTest()
    {
        var genericDevice = GetSpecificDeviceAsBase();

        // Runtime error: delegates must be of the same type
        genericDevice.VarChange += OnVarChange;
    }

    public IDevice<IVar> GetSpecificDeviceAsBase()
    {
        return new SpecificDevice();
    }

    public void OnVarChange(IVar theVar)
    {
        // ...
    }
}

Is there a way out of this issue?

Or is using the first, non-generic solution and force explicit casts in the "specific" events the only way?

I could as well go for a invariant generic DeviceVarChange<TVar> delegate, non-generic interfaces, and add a new event in every subinterfaces with the specific interface, but if I could dodge this, suggestions are welcome.

Here is my current workaround as a first, hopefully transient answer. This is what I will use while I hope that someone suggests me something better to work with.

public interface IDevice
{
    event DeviceVarChange<IVar> VarChange;
}

public interface IVar
{
    // ...
}

public delegate void DeviceVarChange<TVar>(TVar theVar) where TVar : IVar;

public interface ISpecificDevice : IDevice
{
    // Overload the event inside the interface with the specific class
    new event DeviceVarChange<ISpecificVar> VarChange;
}

public interface ISpecificVar : IVar
{
    // ...
}

public class SpecificDevice : ISpecificDevice
{
    // Dear maintainer,
    // Forgive me.
    // Regards
    private void FireVarChange(ISpecificVar theVar)
    {
        lock (delegatesForVarChange)
        {
            foreach (var theDelegate in delegatesForVarChange)
            {
                theDelegate.Invoke(theVar);
            }
        }
    }

    private List<dynamic> delegatesForVarChange = new List<dynamic>();

    event DeviceVarChange<IVar> IDevice.VarChange
    {
        add
        {
            lock (delegatesForVarChange)
            {
                delegatesForVarChange.Add(value);
            }
        }
        remove
        {
            lock (delegatesForVarChange)
            {
                delegatesForVarChange.Remove(value);
            }
        }
    }

    public event DeviceVarChange<ISpecificVar> VarChange
    {
        add
        {
            lock (delegatesForVarChange)
            {
                delegatesForVarChange.Add(value);
            }
        }
        remove
        {
            lock (delegatesForVarChange)
            {
                delegatesForVarChange.Remove(value);
            }
        }
    }
}

// Try to use a specific instance as a base one
public class Test
{
    public void DoTest()
    {
        var genericDevice = GetSpecificDeviceAsBase();

        // Success
        genericDevice.VarChange += OnVarChange;
    }

    public IDevice GetSpecificDeviceAsBase()
    {
        return new SpecificDevice();
    }

    public void OnVarChange(IVar theVar)
    {
        // ...
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM