L’interface IEnumerable<T>
est très utilisée en C# pour représenter des collections, mais l’utiliser comme propriété d’un objet est une erreur courante, notamment chez les développeurs moins expérimentés. Cela peut entraîner des comportements inattendus, des problèmes de performance et compliquer le débogage. Voici pourquoi cette pratique est à éviter et quelles alternatives préférer.
IEnumerable<T>
ne stocke pas de données, mais représente une séquence qui s’évalue à la demande. Si la source sous-jacente change ou devient indisponible, cela peut provoquer des erreurs.
public class MaClasse
{
public IEnumerable<int> Nombres { get; set; }
}
var obj = new MaClasse
{
Nombres = Enumerable.Range(1, 5).Select(x => x * 2)
};
foreach (var n in obj.Nombres) // Chaque accès ré-évalue la séquence
{
Console.WriteLine(n);
}
Si Nombres
provient d’une base de données ou d’une API, chaque itération peut réexécuter la requête, impactant les performances et la fiabilité.
Avec IEnumerable<T>
, il est impossible de savoir si la collection est encore valide ou si elle a changé.
var data = new List<int> { 1, 2, 3 };
var obj = new MaClasse { Nombres = data };
data.Clear();
// obj.Nombres est maintenant vide !
int[] data = { 1, 2, 3, 4};
var enumerator = ((IEnumerable<int>)data).GetEnumerator();
static void EnumerateShared(IEnumerator<int> enumerator, string taskName)
{
while (true)
{
lock (enumerator)
{
if (!enumerator.MoveNext()) break;
Console.WriteLine($"{taskName}: {enumerator.Current}");
}
Task.Delay(10).Wait();
}
}
Task task1 = Task.Run(() => EnumerateShared(enumerator, "Task1"));
Task task2 = Task.Run(() => EnumerateShared(enumerator, "Task2"));
Task.WaitAll(task1, task2);
// Task2: 1
// Task1: 2
// Task2: 3
// Task1: 4
Une liste interne peut être modifiée à l’insu de l’objet qui expose IEnumerable<T>
.
IEnumerable<T>
ne permet pas d'ajouter ou de supprimer des éléments. Si une collection modifiable est attendue, utilisez ICollection<T>
ou List<T>
:
public class MaClasse
{
public List<int> Nombres { get; set; } = new();
}
Les collections en IEnumerable<T>
ne sont évaluées qu’à la demande, ce qui complique leur inspection dans un débogueur. Chaque accès peut modifier leur contenu ou provoquer des exceptions.
public class MaClasse
{
private readonly List<int> _nombres = new();
public IReadOnlyList<int> Nombres => _nombres;
}
public class MaClasse
{
public int[] Nombres { get; }
public MaClasse(int[] nombres)
{
Nombres = nombres;
}
}
public class MaClasse
{
public List<T> Nombres { get; }
public MaClasse(List<T> nombres)
{
Nombres = nombres;
}
}
Si l'objectif est d'itérer sur une collection sans la stocker, IEnumerable<T>
est acceptable en paramètre de méthode ou en valeur de retour :
public void TraiterElements(IEnumerable<int> elements)
{
foreach (var e in elements)
{
Console.WriteLine(e);
}
}
public IEnumerable<int> ObtenirNombres()
{
return Enumerable.Range(1, 10);
}
Cela permet de conserver les avantages de l’évaluation paresseuse sans en subir les inconvénients.
Stocker IEnumerable<T>
en propriété est une mauvaise pratique car cela induit une évaluation différée imprévisible, une perte de contrôle sur les données et des difficultés de débogage. Il est préférable d'utiliser List<T>
, IReadOnlyList<T>
, ICollection<T>
ou T[]
selon les besoins.
Résumé :
✅ Utiliser List<T>
ou ICollection<T>
pour une collection modifiable.
✅ Utiliser IReadOnlyList<T>
ou T[]
pour une collection immuable.
✅ Accepter IEnumerable<T>
en paramètre de méthode ou en valeur de retour uniquement.
❌ Ne pas stocker IEnumerable<T>
en propriété d'objet.