Compartir a través de


Usar varianza en delegados (C#)

Al asignar un método a un delegado, la covarianza y la contravarianza proporcionan flexibilidad para hacer coincidir un tipo delegado con una firma de método. La covarianza permite que un método tenga un tipo de retorno más derivado que el definido en el delegado. Contravariance permite un método que tiene tipos de parámetros menos derivados que los del tipo delegado.

Ejemplo 1: Covarianza

Descripción

En este ejemplo se muestra cómo se pueden usar delegados con métodos que tienen tipos de retorno que son derivados del tipo de retorno en la firma del delegado. El tipo de datos devuelto por DogsHandler es de tipo Dogs, que deriva del tipo Mammals que se define en el delegado.

Código

class Mammals {}  
class Dogs : Mammals {}  
  
class Program  
{  
    // Define the delegate.  
    public delegate Mammals HandlerMethod();  
  
    public static Mammals MammalsHandler()  
    {  
        return null;  
    }  
  
    public static Dogs DogsHandler()  
    {  
        return null;  
    }  
  
    static void Test()  
    {  
        HandlerMethod handlerMammals = MammalsHandler;  
  
        // Covariance enables this assignment.  
        HandlerMethod handlerDogs = DogsHandler;  
    }  
}  

Ejemplo 2: Contravarianza

Descripción

En este ejemplo se muestra cómo se pueden usar delegados con métodos que tienen parámetros cuyos tipos son tipos base del tipo de parámetro de firma de delegado. Con la contravarianza, puede usar un controlador de eventos en lugar de controladores independientes. En el ejemplo siguiente se usa dos delegados:

  • Un delegado personalizado KeyEventHandler que define la firma de un evento de clave. Su firma es:

    public delegate void KeyEventHandler(object sender, KeyEventArgs e)
    
  • Un delegado personalizado MouseEventHandler que define la firma de un evento de ratón. Su firma es:

    public delegate void MouseEventHandler(object sender, MouseEventArgs e)
    

En el ejemplo se define un controlador de eventos con un EventArgs parámetro y se usa para controlar eventos de tecla y mouse. Esto funciona porque EventArgs es un tipo base de las clases personalizadas KeyEventArgs y MouseEventArgs definidas en el ejemplo. Contravariance permite usar un método que acepta un parámetro de tipo base para eventos que proporcionan parámetros de tipo derivado.

Funcionamiento de la contravarianza en este ejemplo

Al suscribirse a un evento, el compilador comprueba si el método del controlador de eventos es compatible con la firma de delegado del evento. Con contravarianza:

  1. El KeyDown evento espera un método que acepta KeyEventArgs.
  2. El evento MouseClick espera un método que acepte MouseEventArgs.
  3. El método MultiHandler toma el tipo base EventArgs.
  4. Como KeyEventArgs y MouseEventArgs ambos heredan de EventArgs, ellos se pueden pasar de forma segura a un método que espera EventArgs.
  5. El compilador permite esta asignación porque es segura: MultiHandler puede funcionar con cualquier EventArgs instancia.

Esto es contravarianza en acción: puede usar un método con un parámetro "menos específico" (tipo base) donde se espera un parámetro "más específico" (tipo derivado).

Código

// Custom EventArgs classes to demonstrate the hierarchy
public class KeyEventArgs(string keyCode) : EventArgs
{
    public string KeyCode { get; set; } = keyCode;
}

public class MouseEventArgs(int x, int y) : EventArgs  
{
    public int X { get; set; } = x;
    public int Y { get; set; } = y;
}

// Define delegate types that match the Windows Forms pattern
public delegate void KeyEventHandler(object sender, KeyEventArgs e);
public delegate void MouseEventHandler(object sender, MouseEventArgs e);

// A simple class that demonstrates contravariance with events
public class Button
{
    // Events that expect specific EventArgs-derived types
    public event KeyEventHandler? KeyDown;
    public event MouseEventHandler? MouseClick;

    // Method to simulate key press
    public void SimulateKeyPress(string key)
    {
        Console.WriteLine($"Simulating key press: {key}");
        KeyDown?.Invoke(this, new KeyEventArgs(key));
    }

    // Method to simulate mouse click  
    public void SimulateMouseClick(int x, int y)
    {
        Console.WriteLine($"Simulating mouse click at ({x}, {y})");
        MouseClick?.Invoke(this, new MouseEventArgs(x, y));
    }
}

public class Form1
{
    private Button button1;

    public Form1()
    {
        button1 = new Button();
        
        // Event handler that accepts a parameter of the base EventArgs type.
        // This method can handle events that expect more specific EventArgs-derived types
        // due to contravariance in delegate parameters.
        
        // You can use a method that has an EventArgs parameter,
        // although the KeyDown event expects the KeyEventArgs parameter.
        button1.KeyDown += MultiHandler;

        // You can use the same method for an event that expects 
        // the MouseEventArgs parameter.
        button1.MouseClick += MultiHandler;
    }

    // Event handler that accepts a parameter of the base EventArgs type.
    // This works for both KeyDown and MouseClick events because:
    // - KeyDown expects KeyEventHandler(object sender, KeyEventArgs e)
    // - MouseClick expects MouseEventHandler(object sender, MouseEventArgs e)  
    // - Both KeyEventArgs and MouseEventArgs derive from EventArgs
    // - Contravariance allows a method with a base type parameter (EventArgs)
    //   to be used where a derived type parameter is expected
    private void MultiHandler(object sender, EventArgs e)
    {
        Console.WriteLine($"MultiHandler called at: {DateTime.Now:HH:mm:ss.fff}");
        
        // You can check the actual type of the event args if needed
        switch (e)
        {
            case KeyEventArgs keyArgs:
                Console.WriteLine($"  - Key event: {keyArgs.KeyCode}");
                break;
            case MouseEventArgs mouseArgs:
                Console.WriteLine($"  - Mouse event: ({mouseArgs.X}, {mouseArgs.Y})");
                break;
            default:
                Console.WriteLine($"  - Generic event: {e.GetType().Name}");
                break;
        }
    }

    public void DemonstrateEvents()
    {
        Console.WriteLine("Demonstrating contravariance in event handlers:");
        Console.WriteLine("Same MultiHandler method handles both events!\n");
        
        button1.SimulateKeyPress("Enter");
        button1.SimulateMouseClick(100, 200);
        button1.SimulateKeyPress("Escape");
        button1.SimulateMouseClick(50, 75);
    }
}

Puntos clave sobre la contravarianza

// Demonstration of how contravariance works with delegates:
// 
// 1. KeyDown event signature: KeyEventHandler(object sender, KeyEventArgs e)
//    where KeyEventArgs derives from EventArgs
//
// 2. MouseClick event signature: MouseEventHandler(object sender, MouseEventArgs e)  
//    where MouseEventArgs derives from EventArgs
//
// 3. Our MultiHandler method signature: MultiHandler(object sender, EventArgs e)
//
// 4. Contravariance allows us to use MultiHandler (which expects EventArgs)
//    for events that provide more specific types (KeyEventArgs, MouseEventArgs)
//    because the more specific types can be safely treated as their base type.
//
// This is safe because:
// - The MultiHandler only uses members available on the base EventArgs type
// - KeyEventArgs and MouseEventArgs can be implicitly converted to EventArgs
// - The compiler knows that any EventArgs-derived type can be passed safely

Al ejecutar este ejemplo, verá que el mismo MultiHandler método controla correctamente los eventos clave y mouse, lo que muestra cómo la contravarianza permite un código de control de eventos más flexible y reutilizable.

Consulte también