c # générique, couvrant à la fois les tableaux et les listes?

Voici une extension très pratique, qui fonctionne pour un array quelconque:

 public static T AnyOne(this T[] ra) where T:class { int k = ra.Length; int r = Random.Range(0,k); return ra[r]; } 

Malheureusement, cela ne fonctionne pas pour une List . Voici la même extension qui fonctionne pour toute List

 public static T AnyOne(this List listy) where T:class { int k = listy.Count; int r = Random.Range(0,k); return listy[r]; } 

En fait, existe-t-il un moyen de généraliser les génériques couvrant à la fois les array et les List ? Ou est-il connu pour être impossible?


Il me semble que la réponse pourrait englober encore plus Collection s? Ou bien l’un des experts ci-dessous a-t-il déjà réalisé cela?


PS, je m’excuse de ne pas avoir mentionné explicitement que c’est dans le milieu d’Unity3D. “Random.Range” est une fonction unitaire, et un appel “AnyOne” se lit comme un moteur de jeu à 100% pour tous les ingénieurs du jeu. C’est la première extension que vous saisissez pour un projet de jeu et vous l’utilisez constamment dans le code du jeu (“toute explosion!” “Tout effet sonore de pièce!” Etc etc!)

De toute évidence, il pourrait bien sûr être utilisé dans n’importe quel milieu.

En fait, l’interface commune la plus appropriée entre T[] et List pour votre cas est IReadOnlyList

 public static T AnyOne(this IReadOnlyList list) where T:class { int k = list.Count; int r = Random.Range(0,k); return list[r]; } 

Comme mentionné dans une autre réponse, IList fonctionne également, mais la bonne pratique vous demande de demander à l’appelant la fonctionnalité minimale requirejse par la méthode, qui dans ce cas est la propriété Count et l’indexeur en lecture seule .

IEnumerable fonctionne également, mais permet à l’appelant de passer un iterator non collection, dans lequel les méthodes d’extension Count et ElementAt pourraient être très inefficaces – comme Enumerable.Range(0, 1000000) , une requête de firebase database, etc.


Remarque pour les ingénieurs Unity3D: Si vous regardez tout au bas de la documentation de l’interface IReadOnlyList‌ , celle-ci est disponible depuis .Net 4.5. Dans les versions antérieures de .Net, vous devez recourir à IList (disponible depuis la version 2.0). L’unité rest bien en retard sur les versions .Net. Pour 2016, Unity utilise uniquement .Net 2.0.5. Donc, pour Unity3D, vous devez utiliser IList .

T[] et List implémentent en réalité IList , qui fournit une énumération, une propriété Count et un indexeur.

 public static T AnyOne(this IList ra) { int k = ra.Count; int r = Random.Range(0,k); return ra[r]; } 

Juste une remarque: pour le milieu d’ Unity3D en particulier, c’est exactement la bonne réponse. En ce qui concerne l’amélioration supplémentaire de cette réponse, IReadOnlyList , celle-ci n’est pas disponible dans Unity3D. (En ce qui concerne l’extension (ingénieuse) de IEnumerable même pour couvrir les objects sans décompte / indexabilité, certainement dans une situation de moteur de jeu qui serait un concept distinct (tel que AnyOneEvenInefficiently ou AnyOneEvenFromUnsafeGroups ).)

Il est intéressant de voir comment certaines personnes choisissent IEnumerable , tandis que d’autres insistent sur IReadOnlyList .

Maintenant soyons honnêtes. IEnumerable est utile, très utile. Dans la plupart des cas, vous voulez simplement mettre cette méthode dans une bibliothèque et lancer votre fonction d’utilitaire à tout ce que vous pensez être une collection, puis vous en servir. Cependant, utiliser correctement IEnumerable est un peu délicat, comme je le ferai remarquer ici …

IEnumerable

Supposons pour une seconde que l’OP utilise Linq et souhaite obtenir un élément aléatoire d’une séquence. En gros, il se retrouve avec le code de @Yannick, qui se trouve dans la bibliothèque de fonctions utilitaires:

 public static T AnyOne(this IEnumerable source) { int endExclusive = source.Count(); // #1 int randomIndex = Random.Range(0, endExclusive); return source.ElementAt(randomIndex); // #2 } 

Maintenant, cela fait essentiellement 2 choses:

  1. Comptez le nombre d’éléments dans la source. Si la source est un simple IEnumerable cela implique de parcourir tous les éléments de la liste, s’il s’agit de f.ex. une List , il utilisera la propriété Count .
  2. Réinitialisez l’énumérable, accédez à l’élément randomIndex , récupérez-le et renvoyez-le.

Il y a deux choses qui peuvent aller mal ici. Tout d’abord, votre IEnumerable peut être un stockage séquentiel lent, et cela peut ruiner les performances de votre application de manière inattendue. Par exemple, la diffusion en continu depuis un périphérique peut vous poser des problèmes. Cela dit, vous pouvez très bien dire que c’est à prévoir lorsque cela est inhérent à la caractéristique de la collection – et personnellement, je dirais que cet argument tiendra.

Deuxièmement, et c’est peut-être encore plus important, il n’ya aucune garantie que vous pouvez énumérer la même séquence à chaque itération (et qu’il n’ya donc aucune garantie que votre code ne plante pas). Par exemple, considérons cet élément de code d’aspect innocent, qui pourrait être utile à des fins de test:

 IEnumerable GenerateRandomDataset() { Random rnd = new Random(); int count = rnd.Next(10, 100); // randomize number of elements for (int i=0; i 

La première itération (appelant Count() ), vous pouvez générer 99 résultats. Vous choisissez l'élément 98. Ensuite, vous appelez ElementAt , la deuxième itération génère 12 résultats et votre application se bloque. Pas cool.

Correction de l'implémentation IEnumerable

Comme nous l’avons vu, le problème de l’implémentation de IEnumerable est qu’il faut parcourir les données 2 fois. Nous pouvons résoudre ce problème en parcourant les données une seule fois.

Le «truc» ici est en fait assez simple: si nous avons vu un élément, nous voulons certainement envisager de le renvoyer. Tous éléments pris en compte, il y a 50% de chances que cet élément soit renvoyé. Si nous voyons le troisième élément, il y a 33% / 33% / 33% de chances que cela soit retourné. Etc.

Par conséquent, une meilleure implémentation pourrait être celle-ci:

 public static T AnyOne(this IEnumerable source) { Random rnd = new Random(); double count = 1; T result = default(T); foreach (var element in source) { if (rnd.NextDouble() <= (1.0 / count)) { result = element; } ++count; } return result; } 

Remarque secondaire: si nous utilisons Linq, nous nous attendrions à ce que les opérations utilisent IEnumerable une fois (et une seule fois!). Maintenant tu sais pourquoi.

Le faire fonctionner avec des listes et des tableaux

Bien que ce soit une bonne astuce, notre performance sera désormais plus lente si nous travaillons sur une List , ce qui n’a aucun sens car nous soaps qu’une implémentation bien meilleure est disponible en raison de la propriété pour laquelle index et Count sont disponibles. nous.

Ce que nous recherchons, c'est le dénominateur commun de cette meilleure solution, utilisée dans le plus grand nombre de collections possible. Nous allons nous retrouver avec l' IReadOnlyList , qui implémente tout ce dont nous avons besoin.

En raison des propriétés que nous soaps vraies pour IReadOnlyList , nous pouvons maintenant utiliser en toute sécurité Count et indexer, sans courir le risque de planter l’application.

Cependant, bien IReadOnlyList semble attrayant, IList ne semble pas, pour une raison quelconque, le mettre en œuvre ... ce qui signifie essentiellement que IReadOnlyList est un peu un pari en pratique. À cet égard, je suis à peu près sûr qu'il existe beaucoup plus d'implémentations IList implémentations IReadOnlyList . Il semble donc préférable de simplement prendre en charge les deux interfaces.

Cela nous amène à la solution ici:

 public static T AnyOne(this IEnumerable source) { var rnd = new Random(); var list = source as IReadOnlyList; if (list != null) { int index = rnd.Next(0, list.Count); return list[index]; } var list2 = source as IList; if (list2 != null) { int index = rnd.Next(0, list2.Count); return list2[index]; } else { double count = 1; T result = default(T); foreach (var element in source) { if (rnd.NextDouble() <= (1.0 / count)) { result = element; } ++count; } return result; } } 

PS: Pour des scénarios plus complexes, consultez le modèle de stratégie.

au hasard

@Yannick Motton a fait remarquer qu'il fallait être prudent avec Random , car ce ne sera pas vraiment aléatoire si vous appelez des méthodes comme celle-ci plusieurs fois. Random est initialisé avec le RTC, donc si vous créez une nouvelle instance plusieurs fois, cela ne changera pas la valeur de départ.

Voici un moyen simple de contourner ce problème:

 private static int seed = 12873; // some number or a timestamp. // ... // initialize random number generator: Random rnd = new Random(Interlocked.Increment(ref seed)); 

De cette manière, chaque fois que vous appelez AnyOne, le générateur de nombres aléatoires recevra une autre graine et fonctionnera même dans des boucles serrées.

Résumer:

Donc, pour résumer:

  • IEnumerable doit être itéré une fois, et une seule fois. Faire autrement pourrait donner à l'utilisateur des résultats inattendus.
  • Si vous avez access à de meilleures fonctionnalités qu'un simple énumération, il n'est pas nécessaire de passer en revue tous les éléments. Le mieux est d’obtenir le bon résultat immédiatement.
  • Pensez aux interfaces que vous vérifiez très attentivement. Bien IReadOnlyList soit définitivement le meilleur candidat, il n'est pas hérité de IList ce qui signifie qu'il sera moins efficace en pratique.

Le résultat final est quelque chose que Just Works.

T[] et List partagent la même interface: IEnumerable .

IEnumerable n’a cependant pas de membre Length ni Count, mais il existe une méthode d’extension Count() . De plus, il n’y a pas d’indexeur sur les séquences, vous devez donc utiliser la méthode d’extension ElementAt(int) .

Quelque chose dans le genre de:

 public static T AnyOne(this IEnumerable source) { int endExclusive = source.Count(); int randomIndex = Random.Range(0, endExclusive); return source.ElementAt(randomIndex); } 

Vous pouvez changer un peu votre définition:

 public static T AnyOne(this IEnumerable ra) { if(ra==null) throw new ArgumentNullException("ra"); int k = ra.Count(); int r = Random.Range(0,k); return ra.ElementAt(r-1); } 

Vous définissez maintenant une méthode d’extension pour tous les types qui implémentent l’interface IEnumerable .