2010-09-01 5 views
3

J'ai une classe de données Étudiant, et j'ai une classe agrégée Étudiants. L'étudiant a deux propriétés de type string: Name et City. Ce que je veux faire, c'est avoir la possibilité de choisir quelle propriété itérer en utilisant le mécanisme foreach.céder itérateur avec réflexion

Le code que j'ai écrit fonctionne et il est également lisible et agréable. Le problème principal est la performance: la ligne dans laquelle j'utilise le mot-clé yield n'est probablement pas très efficace, mais la question est de savoir combien? est-ce dramatique performance hit?

Existe-t-il un meilleur moyen d'obtenir cette fonctionnalité? (ajouté:. Je ne veux pas permettre à quelqu'un de modifier les retour des objets d'étudiants, de sorte que toutes les solutions Linq proposées ne sont pas bonnes ici pour la rendre plus claire, je veux:
propriété itération + intégration mécanisme foreach + classe d'étudiant et la liste des étudiants sont en lecture seule comment puis-je parvenir)

static void Main(string[] args) 
    {   
     Students students = new Students(); 

     students.AddStudent(new Student { Age = 20, Name = "Stud1" , City="City1" }); 
     students.AddStudent(new Student { Age = 46, Name = "Stud2" , City="City2"}); 
     students.AddStudent(new Student { Age = 32, Name = "Stud3" , City="City3" }); 
     students.AddStudent(new Student { Age = 34, Name = "Stud4" , City="city4" }); 

     students.PropertyToIterate = eStudentProperty.City; 
     foreach (string studentCity in students) 
     { 
      Console.WriteLine(studentcity); 
     } 

     students.PropertyToIterate = eStudentProperty.Name; 
     foreach (string studentName in students) 
     { 
      Console.WriteLine(studentName); 
     } 

    } 

public class Students :IEnumerable<object> 
{ 
    private List<Student> m_Students = new List<Student>(); 

    private eStudentProperty m_PropertyToIterate = eStudentProperty.Name; 

    public eStudentProperty PropertyToIterate 
    { 
     get { return m_PropertyToIterate; } 
     set { m_PropertyToIterate = value; } 
    } 

    public void AddStudent(Student i_Student) 
    { 
     m_Students.Add(i_Student); 
    } 

    public IEnumerator<object> GetEnumerator() 
    {    
     for (int i = 0; i < m_Students.Count; ++i) 
     { 
      yield return (object)m_Students[i].GetType().GetProperty(PropertyToIterate.ToString()).GetValue(m_Students[i], null); 
     }    
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     throw new NotImplementedException(); 
    } 
} 

public enum eStudentProperty 
{ 
    Name, 
    Age, 
    City 
} 

public class Student 
{ 
    public string Name { get; set; } 

    public string City { get; set; } 

    public int Age { get; set; } 
} 
+0

Vous ne pouvez pas lancer cette exception NotImplemented! – leppie

+6

belle apparence?!?! – flq

+0

Est-ce que cela aide? http://stackoverflow.com/questions/359495/how-to-make-ienumerablet-readonly – Odrade

Répondre

7

En réponse à votre édition, que diriez-vous quelque chose comme ça ...

Students students = new Students(); 
students.AddStudent(new Student { Age = 20, Name = "Stud1", City = "City1" }); 
students.AddStudent(new Student { Age = 46, Name = "Stud2", City = "City2" }); 
students.AddStudent(new Student { Age = 32, Name = "Stud3", City = "City3" }); 
students.AddStudent(new Student { Age = 34, Name = "Stud4", City = "city4" }); 

foreach (int studentAge in students.EnumerateBy(StudentProperty.Age)) 
{ 
    Console.WriteLine(studentAge); 
} 

foreach (string studentName in students.EnumerateBy(StudentProperty.Name)) 
{ 
    Console.WriteLine(studentName); 
} 

foreach (string studentCity in students.EnumerateBy(StudentProperty.City)) 
{ 
    Console.WriteLine(studentCity); 
} 

// ... 

public class Students 
{ 
    private List<Student> _students = new List<Student>(); 

    public void AddStudent(Student student) 
    { 
     _students.Add(student); 
    } 

    public IEnumerable<T> EnumerateBy<T>(StudentProperty<T> property) 
    { 
     return _students.Select(property.Selector); 
    } 
} 

public static class StudentProperty 
{ 
    public static readonly StudentProperty<int> Age = 
     new StudentProperty<int>(s => s.Age); 

    public static readonly StudentProperty<string> Name = 
     new StudentProperty<string>(s => s.Name); 

    public static readonly StudentProperty<string> City = 
     new StudentProperty<string>(s => s.City); 
} 

public sealed class StudentProperty<T> 
{ 
    internal Func<Student, T> Selector { get; private set; } 

    internal StudentProperty(Func<Student, T> selector) 
    { 
     Selector = selector; 
    } 
} 
+0

J'ai rendu mon message plus clair. Y a-t-il une solution maintenant? –

+0

Merci! .... On dirait que ça répond à toutes les exigences, mais ça a l'air compliqué, j'ai besoin de le lire plus à fond. –

+0

Intéressant! A l'air plutôt propre et, oserais-je dire, amusant! – wageoghe

4

vous pouvez utiliser lambda de faire quelque chose de similaire

PropertyToIterate alors prendre une Func<Student, object> que vous pouvez définir la façon suivante:.?

Students.PropertyToIterate = student => student.City; 

GetEnumerator peut être mis en œuvre avec LINQ comme ceci:

return from student in m_Students select PropertyToIterate(student); 

ou sans LINQ comme celui-ci

foreach(var student in students) 
{ 
    yield return PropertyToIterate(student); 
} 
+0

J'ai rendu mon message plus clair. Y a-t-il une solution maintenant? –

11

Pourquoi obtenir non seulement les propriétés avec LINQ et garder dénombrements des étudiants afin que vous peut itérer tous les élèves de la classe Students.

foreach (string studentCity in students.Select(s => s.City)) 
    { 
     Console.WriteLine(studentcity); 
    } 
    ... 
    foreach (string studentName in students.Select(s => s.Name)) 
    { 
     Console.WriteLine(studentName); 
    } 
+0

+1: la solution ogirale semble horrible sans explication. – Grozz

1

Vous pouvez rencontrer la perte de performance due à plusieurs reprises reflétant le type chaque itération. Pour éviter les appels inutiles à GetType(), retirez-le de la boucle. En outre, puisque le type est connu (par exemple, Étudiant), vous pouvez gagner de l'efficacité en temps de compilation en utilisant typeof.

public IEnumerator<object> GetEnumerator() 
{  
    var property = typeof(Student).GetProperty(PropertyToIterate.ToString()); 
    for (int i = 0; i < m_Students.Count; ++i) 
    { 
     yield return property.GetValue(m_Students[i], null); 
    }    
} 

Sur un côté, vous pouvez répondre à la GetEnumerator() non-générique en retournant la version générique.

IEnumerator IEnumerable.GetEnumerator() 
{ 
    return GetEnumerator<object>(); 
} 
+0

'yield return (string) propriété.GetValue (m_Students [i], null);' n'est pas fonctionnellement correct; cela ne fonctionnera pas pour 'Age', qui est un' int'. Vous devez soit supprimer la distribution, soit appeler '.ToString()' dans ce cas, vous pouvez améliorer les choses en changeant le type d'énumérateur à 'IEnumerator '. – Ani

+0

@Ani: assez bien. Je n'avais reproduit que l'appel de l'OP. Edité maintenant pour ignorer le typecast. – kbrimington

+0

Si je comprends bien, les métadonnées de l'objet contiennent déjà les informations de type, donc 'GetType()' est un très, très petit coup de performance. Cela dit, je ne vois aucune raison de l'utiliser ici. –

1

si l'étudiant peut être un struct alors qui traitera la partie en lecture seule de l'élément étudiant. Sinon, faites simplement les propriétés de la classe Student dans le constructeur et supprimez l'ensemble public.

éditer: ok, pas struct. changement d'étudiant en classe

public class Students : ReadOnlyCollection<Student> 
{ 
    public Students(IList<Student> students) : base(students) 
    {} 

    public IEnumerable<string> Names 
    { 
     get { return this.Select(x => x.Name); } 
    } 

    public IEnumerable<string> Cities 
    { 
     get { return this.Select(x => x.City); } 
    } 
} 

public class Student 
{ 
       public Student(string name, string city, int age) 
       { 
        this.Name = name; 
        this.City = city; 
        this.Age = age; 
       } 
    public string Name { get; private set; } 

    public string City { get; private set; } 

    public int Age { get; private set; } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     List<Student> students = new List<Student>(); 
     students.Add(new Student("Stud1", "City1",20)); 
     students.Add(new Student("Stud2", "City2",46)); 
     students.Add(new Student("Stud3", "City3",66)); 
     students.Add(new Student("Stud4","city4", 34)); 


     Students readOnlyStudents = new Students(students); 

     foreach (string studentCity in readOnlyStudents.Cities) 
     { 
      Console.WriteLine(studentCity); 
     } 

     foreach (string studentName in readOnlyStudents.Names) 
     { 
      Console.WriteLine(studentName); 
     } 
    } 
} 
+0

Je ne veux pas que Student soit une structure. Cela ne rentre pas dans mon application. –

1

Qu'en est-il de l'ajout d'une interface?Ensuite, vous pouvez utiliser la solution LINQ et les objets Etudiant ne peuvent pas être modifiés.

+0

+1, pour plus de sécurité, la classe Student peut être marquée comme interne, ou être une classe imbriquée privée d'étudiants. Cela empêcherait le downcasting. – TheFogger

+0

Je pensais à propos de la solution d'interfaces, mais comme vous l'avez dit, vous pouvez descendre à Student. mais cacher étudiant n'est pas bon pour moi. Je veux pouvoir utiliser le constructeur n'importe où dans l'application. –

2

Personnellement, j'aime beaucoup la réponse de LukeH. Le seul problème est de devoir définir la classe StudentProperty statique avec des objets de wrapper de délégué readonly statiques plutôt que votre enum eStudentProperty d'origine. Selon votre situation, cela pourrait ne pas être facilement utilisable par l'appelant.

L'avantage de ce code est que chaque objet StudentProperty<T> est fortement typé en fonction de sa propriété associée, permettant à la méthode EnumerateBy de renvoyer un IEnumerable<T> fortement typé.

Voici ce que j'ai trouvé. C'est très similaire à la réponse de LukeH en ce que j'ai fourni une méthode PropertyValues semblable à la méthode EnumerateBy de LukeH, bien que ma méthode renvoie un IEnumerable (non générique). Un problème avec ma mise en œuvre est que si vous itérez sur une propriété de type valeur (telle que Age), il y aura une boxing dans l'énumérateur. Cependant, si le consommateur n'en sait pas assez sur la propriété itérée pour appeler la méthode Ages que j'ai fournie plutôt que PropertyValues(eStudentProperty.Age), alors la boxe se produira très probablement dans le code de l'appelant, que je sois capable de retourner un IEnumerable<int> ou un IEnumerable. Donc, je dirais que tous ceux qui ont besoin d'appeler la méthode PropertyValues parce qu'ils ne peuvent pas appeler les méthodes Names, Cities ou Ages ne pourront pas éviter la boxe avec n'importe quelle implémentation.

class Program 
{ 
    static void Main(string[] args) 
    { 
     Students students = new Students(); 

     students.AddStudent(new Student { Age = 20, Name = "Stud1", City = "City1" }); 
     students.AddStudent(new Student { Age = 46, Name = "Stud2", City = "City2" }); 
     students.AddStudent(new Student { Age = 32, Name = "Stud3", City = "City3" }); 
     students.AddStudent(new Student { Age = 34, Name = "Stud4", City = "city4" }); 

     // in these two examples, you know exactly which property you want to iterate, 
     // so call the corresponding iterator method directly. 
     foreach (string studentCity in students.Cities()) 
     { 
      Console.WriteLine(studentCity); 
     } 

     foreach (string studentName in students.Names()) 
     { 
      Console.WriteLine(studentName); 
     } 

     // in these three cases, the DoSomethingWithPropertyValues method will not know which property is being iterated, 
     // so it will have to use the PropertyValues method 
     DoSomethingWithPropertyValues(students, eStudentProperty.Age); 
     DoSomethingWithPropertyValues(students, eStudentProperty.Name); 
     DoSomethingWithPropertyValues(students, eStudentProperty.City); 
    } 

    static void DoSomethingWithPropertyValues(Students students, eStudentProperty propertyToIterate) 
    { 
     // This method demonstrates use of the Students.PropertyValues method. 
     // The property being iterated is determined by the propertyToIterate parameter, 
     // therefore, this method cannot know which specific iterator method to call. 
     // It will use the PropertyValues method instead. 
     Console.WriteLine("Outputting values for the {0} property.", propertyToIterate); 
     int index = 0; 
     foreach (object value in students.PropertyValues(propertyToIterate)) 
     { 
      Console.WriteLine("{0}: {1}", index++, value); 
     } 
    } 
} 

public class Students 
{ 
    private List<Student> m_Students = new List<Student>(); 

    public void AddStudent(Student i_Student) 
    { 
     m_Students.Add(i_Student); 
    } 

    public IEnumerable PropertyValues(eStudentProperty property) 
    { 
     switch (property) 
     { 
      case eStudentProperty.Name: 
       return this.Names(); 
      case eStudentProperty.City: 
       return this.Cities(); 
      case eStudentProperty.Age: 
       return this.Ages(); 
      default: 
       throw new ArgumentOutOfRangeException("property"); 
     } 
    } 

    public IEnumerable<string> Names() 
    { 
     return m_Students.Select(s => s.Name); 
    } 

    public IEnumerable<string> Cities() 
    { 
     return m_Students.Select(s => s.City); 
    } 

    public IEnumerable<int> Ages() 
    { 
     return m_Students.Select(s => s.Age); 
    } 
} 

public enum eStudentProperty 
{ 
    Name, 
    Age, 
    City 
} 

public class Student 
{ 
    public string Name { get; set; } 

    public string City { get; set; } 

    public int Age { get; set; } 
}