C# Language
Les délégués
Recherche…
Remarques
Résumé
Un type de délégué est un type représentant une signature de méthode particulière. Une instance de ce type fait référence à une méthode particulière avec une signature correspondante. Les paramètres de méthode peuvent avoir des types de délégué, et donc une méthode à transmettre une référence à une autre méthode, qui peut ensuite être appelée
Types de délégué intégrés: Action<...>
, Predicate<T>
et Func<...,TResult>
L'espace de noms System
contient les délégués Action<...>
, Predicate<T>
et Func<...,TResult>
, où "..." représente entre 0 et 16 paramètres de type générique (pour 0 paramètre, Action
est non générique).
Func
représente des méthodes avec un type de retour correspondant à TResult
, et Action
représente des méthodes sans valeur de retour (void). Dans les deux cas, les paramètres de type générique supplémentaires correspondent, dans l'ordre, aux paramètres de la méthode.
Predicate
représente la méthode avec le type de retour booléen, T est le paramètre d'entrée.
Types de délégué personnalisés
Les types de délégué nommés peuvent être déclarés à l'aide du mot clé delegate
.
Invoquer des délégués
Les délégués peuvent être appelés en utilisant la même syntaxe que les méthodes: le nom de l'instance du délégué, suivi des parenthèses contenant des paramètres.
Affectation aux délégués
Les délégués peuvent être affectés aux manières suivantes:
- Assigner une méthode nommée
- Affectation d'une méthode anonyme à l'aide d'un lambda
- Affectation d'une méthode nommée à l'aide du mot clé
delegate
.
Combinaison de délégués
Plusieurs objets délégués peuvent être affectés à une instance de délégué à l'aide de l'opérateur +
. L'opérateur -
peut être utilisé pour supprimer un délégué de composant d'un autre délégué.
Références sous-jacentes des délégués de méthodes nommées
Lors de l'attribution de méthodes nommées aux délégués, ils font référence au même objet sous-jacent si:
Ils sont la même méthode d'instance, sur la même instance d'une classe
Ils sont la même méthode statique sur une classe
public class Greeter { public void WriteInstance() { Console.WriteLine("Instance"); } public static void WriteStatic() { Console.WriteLine("Static"); } } // ... Greeter greeter1 = new Greeter(); Greeter greeter2 = new Greeter(); Action instance1 = greeter1.WriteInstance; Action instance2 = greeter2.WriteInstance; Action instance1Again = greeter1.WriteInstance; Console.WriteLine(instance1.Equals(instance2)); // False Console.WriteLine(instance1.Equals(instance1Again)); // True Action @static = Greeter.WriteStatic; Action staticAgain = Greeter.WriteStatic; Console.WriteLine(@static.Equals(staticAgain)); // True
Déclaration d'un type de délégué
La syntaxe suivante crée un type de delegate
nommé NumberInOutDelegate
, représentant une méthode qui prend un int
et retourne un int
.
public delegate int NumberInOutDelegate(int input);
Cela peut être utilisé comme suit:
public static class Program
{
static void Main()
{
NumberInOutDelegate square = MathDelegates.Square;
int answer1 = square(4);
Console.WriteLine(answer1); // Will output 16
NumberInOutDelegate cube = MathDelegates.Cube;
int answer2 = cube(4);
Console.WriteLine(answer2); // Will output 64
}
}
public static class MathDelegates
{
static int Square (int x)
{
return x*x;
}
static int Cube (int x)
{
return x*x*x;
}
}
L' example
instance de délégué est exécuté de la même manière que la méthode Square
. Une instance de délégué agit littéralement en tant que délégué pour l'appelant: l'appelant appelle le délégué, puis le délégué appelle la méthode cible. Cette indirection dissocie l'appelant de la méthode cible.
Vous pouvez déclarer un type de délégué générique et, dans ce cas, vous pouvez spécifier que le type est covariant ( out
) ou contravariant ( in
) dans certains des arguments de type. Par exemple:
public delegate TTo Converter<in TFrom, out TTo>(TFrom input);
Comme pour les autres types génériques, les types de délégué génériques peuvent avoir des contraintes, par exemple where TFrom : struct, IConvertible where TTo : new()
.
Évitez la co-et la contravariance pour les types de délégué qui doivent être utilisés pour les délégués multidiffusion, tels que les types de gestionnaires d'événements. En effet, la concaténation ( +
) peut échouer si le type d'exécution est différent du type à la compilation en raison de la variance. Par exemple, évitez:
public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e);
Au lieu de cela, utilisez un type générique invariant:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Sont également pris en charge les délégués où certains paramètres sont modifiés par ref
ou out
, comme dans:
public delegate bool TryParser<T>(string input, out T result);
(exemple: TryParser<decimal> example = decimal.TryParse;
) ou délégué où le dernier paramètre a le modificateur params
. Les types de délégué peuvent avoir des paramètres facultatifs (valeurs par défaut de fourniture). Les types de délégué peuvent utiliser des types de pointeur comme int*
ou char*
dans leurs signatures ou leurs types de retour (utilisez le mot-clé unsafe
). Un type de délégué et ses paramètres peuvent comporter des attributs personnalisés.
Le func , Action et prédicat types de délégué
L'espace de noms Système contient les types de délégué Func<..., TResult>
avec entre 0 et 15 paramètres génériques, renvoyant le type TResult
.
private void UseFunc(Func<string> func)
{
string output = func(); // Func with a single generic type parameter returns that type
Console.WriteLine(output);
}
private void UseFunc(Func<int, int, string> func)
{
string output = func(4, 2); // Func with multiple generic type parameters takes all but the first as parameters of that type
Console.WriteLine(output);
}
L'espace de noms Système contient également des types de délégués Action<...>
avec un nombre différent de paramètres génériques (de 0 à 16). Il est similaire à Func<T1, .., Tn>
, mais il renvoie toujours un void
.
private void UseAction(Action action)
{
action(); // The non-generic Action has no parameters
}
private void UseAction(Action<int, string> action)
{
action(4, "two"); // The generic action is invoked with parameters matching its type arguments
}
Predicate<T>
est également une forme de Func
mais il renverra toujours bool
. Un prédicat est un moyen de spécifier un critère personnalisé. En fonction de la valeur de l'entrée et de la logique définie dans le prédicat, elle retournera soit true
soit false
. Predicate<T>
se comporte donc comme Func<T, bool>
et les deux peuvent être initialisés et utilisés de la même manière.
Predicate<string> predicate = s => s.StartsWith("a");
Func<string, bool> func = s => s.StartsWith("a");
// Both of these return true
var predicateReturnsTrue = predicate("abc");
var funcReturnsTrue = func("abc");
// Both of these return false
var predicateReturnsFalse = predicate("xyz");
var funcReturnsFalse = func("xyz");
Le choix d'utiliser ou non Predicate<T>
ou Func<T, bool>
est vraiment une question d'opinion. Predicate<T>
est sans doute plus expressif de l'intention de l'auteur, tandis que Func<T, bool>
est susceptible d'être familier à une plus grande proportion de développeurs C #.
En plus de cela, il existe certains cas où une seule des options est disponible, en particulier lors de l'interaction avec une autre API. Par exemple, List<T>
et Array<T>
prennent généralement Predicate<T>
pour leurs méthodes, tandis que la plupart des extensions LINQ n'acceptent que Func<T, bool>
.
Affectation d'une méthode nommée à un délégué
Les méthodes nommées peuvent être attribuées aux délégués avec des signatures correspondantes:
public static class Example
{
public static int AddOne(int input)
{
return input + 1;
}
}
Func<int,int> addOne = Example.AddOne
Example.AddOne
prend un int
et retourne un int
, sa signature correspond au délégué Func<int,int>
. Example.AddOne
peut être directement affecté à addOne
car ils ont des signatures correspondantes.
Égalité des délégués
L'appel de .Equals()
sur un délégué se compare par égalité de référence:
Action action1 = () => Console.WriteLine("Hello delegates");
Action action2 = () => Console.WriteLine("Hello delegates");
Action action1Again = action1;
Console.WriteLine(action1.Equals(action1)) // True
Console.WriteLine(action1.Equals(action2)) // False
Console.WriteLine(action1Again.Equals(action1)) // True
Ces règles s'appliquent également lors de l'exécution de +=
ou -=
sur un délégué multidiffusion, par exemple lors de l'inscription et de la désinscription à des événements.
Assigner à un délégué par lambda
Les Lambdas peuvent être utilisés pour créer des méthodes anonymes à affecter à un délégué:
Func<int,int> addOne = x => x+1;
Notez que la déclaration explicite de type est requise lors de la création d'une variable de cette manière:
var addOne = x => x+1; // Does not work
Passer des délégués en tant que paramètres
Les délégués peuvent être utilisés comme pointeurs de fonctions typés:
class FuncAsParameters
{
public void Run()
{
DoSomething(ErrorHandler1);
DoSomething(ErrorHandler2);
}
public bool ErrorHandler1(string message)
{
Console.WriteLine(message);
var shouldWeContinue = ...
return shouldWeContinue;
}
public bool ErrorHandler2(string message)
{
// ...Write message to file...
var shouldWeContinue = ...
return shouldWeContinue;
}
public void DoSomething(Func<string, bool> errorHandler)
{
// In here, we don't care what handler we got passed!
...
if (...error...)
{
if (!errorHandler("Some error occurred!"))
{
// The handler decided we can't continue
return;
}
}
}
}
Combiner des délégués (délégués à la multidiffusion)
Addition +
et soustraction -
opérations peuvent être utilisées pour combiner des instances de délégué. Le délégué contient une liste des délégués assignés.
using System;
using System.Reflection;
using System.Reflection.Emit;
namespace DelegatesExample {
class MainClass {
private delegate void MyDelegate(int a);
private static void PrintInt(int a) {
Console.WriteLine(a);
}
private static void PrintType<T>(T a) {
Console.WriteLine(a.GetType());
}
public static void Main (string[] args)
{
MyDelegate d1 = PrintInt;
MyDelegate d2 = PrintType;
// Output:
// 1
d1(1);
// Output:
// System.Int32
d2(1);
MyDelegate d3 = d1 + d2;
// Output:
// 1
// System.Int32
d3(1);
MyDelegate d4 = d3 - d2;
// Output:
// 1
d4(1);
// Output:
// True
Console.WriteLine(d1 == d4);
}
}
}
Dans cet exemple, d3
est une combinaison de délégués d1
et d2
. Par conséquent, lorsqu'il est appelé, le programme System.Int32
chaînes 1
et System.Int32
.
Combinaison de délégués avec des types de retour non vides :
Si un délégué multidiffusion a un type de retour non nonvoid
, l'appelant reçoit la valeur de retour de la dernière méthode à appeler. Les méthodes précédentes sont toujours appelées, mais leurs valeurs de retour sont ignorées.
class Program
{
public delegate int Transformer(int x);
static void Main(string[] args)
{
Transformer t = Square;
t += Cube;
Console.WriteLine(t(2)); // O/P 8
}
static int Square(int x) { return x * x; }
static int Cube(int x) { return x*x*x; }
}
t(2)
appellera d'abord Square
puis Cube
. La valeur de retour de Square est ignorée et la valeur renvoyée de la dernière méthode, c.-à-d. Cube
est conservé.
Appel de sécurité multicast délégué
Vous avez toujours voulu appeler un délégué multidiffusion mais vous souhaitez que la liste complète des invocations soit appelée même si une exception se produit dans la chaîne. Ensuite, vous avez de la chance, j'ai créé une méthode d’extension qui ne fait que cela, en lançant une AggregateException
uniquement une fois l’exécution de la liste complète terminée:
public static class DelegateExtensions
{
public static void SafeInvoke(this Delegate del,params object[] args)
{
var exceptions = new List<Exception>();
foreach (var handler in del.GetInvocationList())
{
try
{
handler.Method.Invoke(handler.Target, args);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if(exceptions.Any())
{
throw new AggregateException(exceptions);
}
}
}
public class Test
{
public delegate void SampleDelegate();
public void Run()
{
SampleDelegate delegateInstance = this.Target2;
delegateInstance += this.Target1;
try
{
delegateInstance.SafeInvoke();
}
catch(AggregateException ex)
{
// Do any exception handling here
}
}
private void Target1()
{
Console.WriteLine("Target 1 executed");
}
private void Target2()
{
Console.WriteLine("Target 2 executed");
throw new Exception();
}
}
Cela produit:
Target 2 executed
Target 1 executed
L'appel direct, sans SaveInvoke
, n'exécutera que Target 2.
Fermeture à l'intérieur d'un délégué
Les fermetures sont des méthodes anonymes en ligne qui ont la capacité d'utiliser des variables de méthode Parent
et d'autres méthodes anonymes définies dans la portée du parent.
Essentiellement, une fermeture est un bloc de code qui peut être exécuté ultérieurement, mais qui conserve l'environnement dans lequel il a été créé pour la première fois, c'est-à-dire qu'il peut toujours utiliser les variables locales de la méthode qui l'a créé. la méthode a terminé son exécution. - Jon Skeet
delegate int testDel();
static void Main(string[] args)
{
int foo = 4;
testDel myClosure = delegate()
{
return foo;
};
int bar = myClosure();
}
Exemple tiré des fermetures dans .NET .
Encapsulation des transformations dans les funcs
public class MyObject{
public DateTime? TestDate { get; set; }
public Func<MyObject, bool> DateIsValid = myObject => myObject.TestDate.HasValue && myObject.TestDate > DateTime.Now;
public void DoSomething(){
//We can do this:
if(this.TestDate.HasValue && this.TestDate > DateTime.Now){
CallAnotherMethod();
}
//or this:
if(DateIsValid(this)){
CallAnotherMethod();
}
}
}
Dans l'esprit du codage propre, l'encapsulation de vérifications et de transformations comme celle ci-dessus en tant que Func peut faciliter la lecture et la compréhension de votre code. Bien que l'exemple ci-dessus soit très simple, que se passe-t-il s'il y a plusieurs propriétés DateTime ayant chacune leurs règles de validation différentes et que nous voulions vérifier différentes combinaisons? Des fonctions simples, à une ligne, ayant chacune une logique de retour établie peuvent être à la fois lisibles et réduire la complexité apparente de votre code. Considérez les appels Func ci-dessous et imaginez à quel point le code encombrerait la méthode:
public void CheckForIntegrity(){
if(ShipDateIsValid(this) && TestResultsHaveBeenIssued(this) && !TestResultsFail(this)){
SendPassingTestNotification();
}
}