Skip to content

Commit 75198f1

Browse files
CopilotBillWagnerIEvangelist
authored
Improve contravariance example in "Using Variance in Delegates" documentation (#47052)
* Initial plan * Add complete contravariance example with better explanation Co-authored-by: BillWagner <[email protected]> * Address code review feedback: Use primary constructors, update code block syntax, remove unnecessary console output Co-authored-by: BillWagner <[email protected]> * Update target framework to .NET 9.0 as requested Co-authored-by: BillWagner <[email protected]> * Address review feedback: Add proper punctuation to list items and clarify custom types Co-authored-by: IEvangelist <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: BillWagner <[email protected]> Co-authored-by: IEvangelist <[email protected]>
1 parent cd4b2b6 commit 75198f1

File tree

3 files changed

+165
-25
lines changed

3 files changed

+165
-25
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System;
2+
3+
namespace ContravarianceExample
4+
{
5+
// <snippet1>
6+
// Custom EventArgs classes to demonstrate the hierarchy
7+
public class KeyEventArgs(string keyCode) : EventArgs
8+
{
9+
public string KeyCode { get; set; } = keyCode;
10+
}
11+
12+
public class MouseEventArgs(int x, int y) : EventArgs
13+
{
14+
public int X { get; set; } = x;
15+
public int Y { get; set; } = y;
16+
}
17+
18+
// Define delegate types that match the Windows Forms pattern
19+
public delegate void KeyEventHandler(object sender, KeyEventArgs e);
20+
public delegate void MouseEventHandler(object sender, MouseEventArgs e);
21+
22+
// A simple class that demonstrates contravariance with events
23+
public class Button
24+
{
25+
// Events that expect specific EventArgs-derived types
26+
public event KeyEventHandler? KeyDown;
27+
public event MouseEventHandler? MouseClick;
28+
29+
// Method to simulate key press
30+
public void SimulateKeyPress(string key)
31+
{
32+
Console.WriteLine($"Simulating key press: {key}");
33+
KeyDown?.Invoke(this, new KeyEventArgs(key));
34+
}
35+
36+
// Method to simulate mouse click
37+
public void SimulateMouseClick(int x, int y)
38+
{
39+
Console.WriteLine($"Simulating mouse click at ({x}, {y})");
40+
MouseClick?.Invoke(this, new MouseEventArgs(x, y));
41+
}
42+
}
43+
44+
public class Form1
45+
{
46+
private Button button1;
47+
48+
public Form1()
49+
{
50+
button1 = new Button();
51+
52+
// Event handler that accepts a parameter of the base EventArgs type.
53+
// This method can handle events that expect more specific EventArgs-derived types
54+
// due to contravariance in delegate parameters.
55+
56+
// You can use a method that has an EventArgs parameter,
57+
// although the KeyDown event expects the KeyEventArgs parameter.
58+
button1.KeyDown += MultiHandler;
59+
60+
// You can use the same method for an event that expects
61+
// the MouseEventArgs parameter.
62+
button1.MouseClick += MultiHandler;
63+
}
64+
65+
// Event handler that accepts a parameter of the base EventArgs type.
66+
// This works for both KeyDown and MouseClick events because:
67+
// - KeyDown expects KeyEventHandler(object sender, KeyEventArgs e)
68+
// - MouseClick expects MouseEventHandler(object sender, MouseEventArgs e)
69+
// - Both KeyEventArgs and MouseEventArgs derive from EventArgs
70+
// - Contravariance allows a method with a base type parameter (EventArgs)
71+
// to be used where a derived type parameter is expected
72+
private void MultiHandler(object sender, EventArgs e)
73+
{
74+
Console.WriteLine($"MultiHandler called at: {DateTime.Now:HH:mm:ss.fff}");
75+
76+
// You can check the actual type of the event args if needed
77+
switch (e)
78+
{
79+
case KeyEventArgs keyArgs:
80+
Console.WriteLine($" - Key event: {keyArgs.KeyCode}");
81+
break;
82+
case MouseEventArgs mouseArgs:
83+
Console.WriteLine($" - Mouse event: ({mouseArgs.X}, {mouseArgs.Y})");
84+
break;
85+
default:
86+
Console.WriteLine($" - Generic event: {e.GetType().Name}");
87+
break;
88+
}
89+
}
90+
91+
public void DemonstrateEvents()
92+
{
93+
Console.WriteLine("Demonstrating contravariance in event handlers:");
94+
Console.WriteLine("Same MultiHandler method handles both events!\n");
95+
96+
button1.SimulateKeyPress("Enter");
97+
button1.SimulateMouseClick(100, 200);
98+
button1.SimulateKeyPress("Escape");
99+
button1.SimulateMouseClick(50, 75);
100+
}
101+
}
102+
// </snippet1>
103+
104+
// <snippet2>
105+
// Demonstration of how contravariance works with delegates:
106+
//
107+
// 1. KeyDown event signature: KeyEventHandler(object sender, KeyEventArgs e)
108+
// where KeyEventArgs derives from EventArgs
109+
//
110+
// 2. MouseClick event signature: MouseEventHandler(object sender, MouseEventArgs e)
111+
// where MouseEventArgs derives from EventArgs
112+
//
113+
// 3. Our MultiHandler method signature: MultiHandler(object sender, EventArgs e)
114+
//
115+
// 4. Contravariance allows us to use MultiHandler (which expects EventArgs)
116+
// for events that provide more specific types (KeyEventArgs, MouseEventArgs)
117+
// because the more specific types can be safely treated as their base type.
118+
//
119+
// This is safe because:
120+
// - The MultiHandler only uses members available on the base EventArgs type
121+
// - KeyEventArgs and MouseEventArgs can be implicitly converted to EventArgs
122+
// - The compiler knows that any EventArgs-derived type can be passed safely
123+
// </snippet2>
124+
125+
class Program
126+
{
127+
static void Main()
128+
{
129+
var form = new Form1();
130+
form.DemonstrateEvents();
131+
}
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>

docs/csharp/programming-guide/concepts/covariance-contravariance/using-variance-in-delegates.md

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,43 +51,41 @@ class Program
5151

5252
This example demonstrates how delegates can be used with methods that have parameters whose types are base types of the delegate signature parameter type. With contravariance, you can use one event handler instead of separate handlers. The following example makes use of two delegates:
5353

54-
- A <xref:System.Windows.Forms.KeyEventHandler> delegate that defines the signature of the [Button.KeyDown](xref:System.Windows.Forms.Control.KeyDown) event. Its signature is:
54+
- A custom `KeyEventHandler` delegate that defines the signature of a key event. Its signature is:
5555

5656
```csharp
5757
public delegate void KeyEventHandler(object sender, KeyEventArgs e)
5858
```
5959

60-
- A <xref:System.Windows.Forms.MouseEventHandler> delegate that defines the signature of the [Button.MouseClick](xref:System.Windows.Forms.Control.MouseDown) event. Its signature is:
60+
- A custom `MouseEventHandler` delegate that defines the signature of a mouse event. Its signature is:
6161

6262
```csharp
6363
public delegate void MouseEventHandler(object sender, MouseEventArgs e)
6464
```
6565

66-
The example defines an event handler with an <xref:System.EventArgs> parameter and uses it to handle both the `Button.KeyDown` and `Button.MouseClick` events. It can do this because <xref:System.EventArgs> is a base type of both <xref:System.Windows.Forms.KeyEventArgs> and <xref:System.Windows.Forms.MouseEventArgs>.
66+
The example defines an event handler with an <xref:System.EventArgs> parameter and uses it to handle both key and mouse events. This works because <xref:System.EventArgs> is a base type of both the custom `KeyEventArgs` and `MouseEventArgs` classes defined in the example. Contravariance allows a method that accepts a base type parameter to be used for events that provide derived type parameters.
67+
68+
### How contravariance works in this example
69+
70+
When you subscribe to an event, the compiler checks if your event handler method is compatible with the event's delegate signature. With contravariance:
71+
72+
1. The `KeyDown` event expects a method that takes `KeyEventArgs`.
73+
1. The `MouseClick` event expects a method that takes `MouseEventArgs`.
74+
1. Your `MultiHandler` method takes the base type `EventArgs`.
75+
1. Since `KeyEventArgs` and `MouseEventArgs` both inherit from `EventArgs`, they can be safely passed to a method expecting `EventArgs`.
76+
1. The compiler allows this assignment because it's safe - the `MultiHandler` can work with any `EventArgs` instance.
77+
78+
This is contravariance in action: you can use a method with a "less specific" (base type) parameter where a "more specific" (derived type) parameter is expected.
6779

6880
### Code
69-
70-
```csharp
71-
// Event handler that accepts a parameter of the EventArgs type.
72-
private void MultiHandler(object sender, System.EventArgs e)
73-
{
74-
label1.Text = System.DateTime.Now.ToString();
75-
}
76-
77-
public Form1()
78-
{
79-
InitializeComponent();
80-
81-
// You can use a method that has an EventArgs parameter,
82-
// although the event expects the KeyEventArgs parameter.
83-
this.button1.KeyDown += this.MultiHandler;
84-
85-
// You can use the same method
86-
// for an event that expects the MouseEventArgs parameter.
87-
this.button1.MouseClick += this.MultiHandler;
88-
89-
}
90-
```
81+
82+
:::code language="csharp" source="snippets/using-variance-in-delegates/ContravarianceExample.cs" id="snippet1":::
83+
84+
### Key points about contravariance
85+
86+
:::code language="csharp" source="snippets/using-variance-in-delegates/ContravarianceExample.cs" id="snippet2":::
87+
88+
When you run this example, you'll see that the same `MultiHandler` method successfully handles both key and mouse events, demonstrating how contravariance enables more flexible and reusable event handling code.
9189

9290
## See also
9391

0 commit comments

Comments
 (0)