none
Änderungen an IEnumerable<T> werden nicht übernommen RRS feed

  • Frage

  • Hallo zusammen,

    ich kämpfe (mal wieder) mit einem Mysterium ;)

    Und zwar habe ich einen IEnumerable<T>. T ist eine ViewModel Klasse. Nun übergebe ich diesen instantiierten und gefüllten IEnumerable<T> (ca. 15.000 ViewModels) an eine Methode, die Änderungen an diesen ViewModels vornimmt.

    Wenn ich nun direkt nach diesem Methodenaufruf die ViewModels anschaue, so sind die Änderungen nicht übernommen worden. Ich habe den IEnumerable<T> testweise mal als ref Parameter übergeben, was leider keine Besserung brachte. Auch das Übergeben der ViewModels in einer List<T> brachte leider nicht den gewünschten Erfolg.

    Ich poste hier mal den Code:

     

     public class PLZOrtFiller {
          public static readonly PLZOrtFiller Instance = new PLZOrtFiller();
    
          private PLZOrtFiller() { }
    
          public void FillPLZOrtByLKZ(IEnumerable<CSVContactViewModel> entities, IEnumerable<PLZReadonlyEntity> plzEntities) {
             if(entities.IsNullOrEmpty() || plzEntities.IsNullOrEmpty()) {
    				return;
             }
    
             PLZReadonlyEntity plz;
             foreach(var item in entities) {
                if(item.LKZ.Equals("d", StringComparison.CurrentCultureIgnoreCase) || item.LKZ.Equals("deu", StringComparison.CurrentCultureIgnoreCase) || item.LKZ.Contains("D-")) {
                   item.LKZ = "DE";
                }
    
                if(string.IsNullOrWhiteSpace(item.LKZ)) {
                   PLZReadonlyEntity plzEntity = plzEntities.FirstOrDefault(t => t.PLZ.Equals(item.PLZ));
                   if(plzEntity != null) {
                      IEnumerable<string> orte = plzEntities.Where(u => u.PLZ.Equals(plzEntity.PLZ)).Select(u => u.Ort);
                      foreach(string ort in orte) {
                         if(item.Ort.Equals(ort, StringComparison.CurrentCultureIgnoreCase)) {
                            item.LKZ = "DE";
                            item.Ort = ort;
                            break;
                         }
                      }
                   }
                }
    
    				if(item.PLZ.Length == 4 && plzEntities.Any(t => t.Ort.Equals(item.Ort, StringComparison.CurrentCultureIgnoreCase))) {
    					item.PLZ = "0" + item.PLZ;
    					item.LKZ = "DE";
    				}
    
                if(item.LKZ.Equals("DE", StringComparison.CurrentCultureIgnoreCase) && string.IsNullOrWhiteSpace(item.Ort)) {
                   plz = plzEntities.FirstOrDefault(t => t.PLZ == item.PLZ);
                   if(plz != null) {
                      item.Ort = plz.Ort;
                   }
                }
             }
          }
       }
    

    Wer hatte das Problem auch mal und hat es gelöst? Und vor allem, wie?

     

    Vielen Dank und viele Grüße
    Holger M. Rößler

     

     


    Kaum macht man es richtig, schon funktioniert es

    Mittwoch, 7. September 2011 20:01

Antworten

  • Hallo Holger,

    naja, die Nacht (und gestern) war nicht so doll, und das Programmieren lenkt ab - siehe auch unten ;-)

    Zunächst aber zum eigentlichen Problem:
    Zwar gilt grundsätzlich, dass man bei einem IEnumerable nicht voraussetzen sollte,
    dass eine identische Instanz zurückgeliefert wird. Aber in den Standard-Fällen kann
    man bei Referenz-Typen davon ausgehen, dass die gleiche Instanz geliefert wird.
    Zumindest solange man nicht selbst tätig wird, also irgendwo new <T>() { ... } verwendet.
    Bei Strukturen sieht das anders aus, aber die lasse ich mal beiseite.

    Interfaces verhalten sich wie eine Klasse und werden als Referenz übergeben.
    Detaillierter siehe Jon Skeet und Eric Lippert in:
    http://stackoverflow.com/questions/3236305/do-interfaces-derive-from-system-object-c-spec-says-yes-eric-says-no-reality

    Was hier aber keine Rolle spielt und so wäre ein ref in keinem Falle nützlich.
    Denn IEnumerable<T> stellt die Methoden bereit, um einen Iterator
    für einen Typ (IEnumerator<T>) via GetEnumerator abzurufen.

    Da die gezeigte Methode nur ein Konsument ist, bringt ein ref nichts.
    ref wäre nur notwendig, wenn Du eine Methode hättest, 
    die unterschiedliche IEnumerable<T> an einen Aufrufer zurückliefern soll.

    Wie bereits geschrieben:
    Der Codeabschnitt an sich stellt kein Problem dar, solange die zugrunde liegenden Klassen
    nicht ein Eigenleben entwickeln - wobei mir nichts gängiges einfällt (meine Vorstellung von Entities zugrunde gelegt).
    Auch wenn es nicht im Sinne des funktionalen Pradigmas (und LINQ) ist, eine Instanz zu verändern.

    Nicht umsonst ist nicht zulässig:

        foreach (var item in entities)
        {
            // kompiliert nicht!
            item = CSVContactEntityConverter(item, plzEntities);
        }
    
    

    Technisch weil ein IEnumerator.Current readonly ist - vom Konzept her,
    weil man keine zusätzliche Voraussetzung über Herkunft und Lebensdauer machen sollte.

    Sauberer wäre, die Konvertierungsroutinen eine Instanz zurückliefern zu lassen,
    ohne dabei darauf zu setzen, dass sie identisch (Object.ReferenceEquals) bleiben.

    Zum Schluß einen Teil vom Rest der Nacht:

        // Vereinfachte Hilfsklassen
        public class PLZReadonlyEntity
        {
            public string LKZ { get; set; }
            public string PLZ { get; set; }
            public string Ort { get; set; }
        }
    
        public class CSVContactEntity : PLZReadonlyEntity
        {
        }
    
        public class PLZOrtFiller
        {
            public static readonly PLZOrtFiller Instance = new PLZOrtFiller();
    
            private PLZOrtFiller() { }
    
            private static string[] LKZ_DE_Ersetzen = { "d", "deu", "D-" };  // Ersetzen durch DE
            private static string LKZ_DE = "DE";
    
            public void FillPLZOrtByLKZ2(IEnumerable<CSVContactEntity> entities, IEnumerable<PLZReadonlyEntity> plzEntities)
            {
                if (entities != null & plzEntities != null)
                {
                    foreach (var item in entities)
                    {
                        CSVContactEntityConverter(item, plzEntities);
                    }
                }
            }
    
            public static CSVContactEntity CSVContactEntityConverter(CSVContactEntity item, IEnumerable<PLZReadonlyEntity> plzEntities)
            {
                // Evtl. nach Ortvergleich, da beide DE
                if (LKZ_DE_Ersetzen.Any(lkz => lkz.Equals(item.LKZ, StringComparison.OrdinalIgnoreCase)))
                {
                    item.LKZ = LKZ_DE;
                }
    
                // Keine Null-Orte / auch keine Null PLZ???
                if (string.IsNullOrWhiteSpace(item.LKZ)
                    && !string.IsNullOrWhiteSpace(item.Ort))
                {
                    var plz = plzEntities.FirstOrDefault(p =>
                        p.PLZ.Equals(item.PLZ, StringComparison.OrdinalIgnoreCase)
                        && p.Ort.Equals(item.Ort, StringComparison.OrdinalIgnoreCase));
                    if (plz != null)
                    {
                        item.LKZ = plz.LKZ ?? LKZ_DE; // LKZ wenn vorhanden?
                        item.Ort = plz.Ort;
                    }
                }
    
                if (LKZ_DE.Equals(item.LKZ, StringComparison.OrdinalIgnoreCase)
                    && string.IsNullOrWhiteSpace(item.Ort))
                {
                    var plz = plzEntities.FirstOrDefault(p => p.PLZ.Equals(item.PLZ, StringComparison.OrdinalIgnoreCase));
                    if (plz != null)
                    {
                        item.Ort = plz.Ort;
                    }
                }
    
                // Keine Nullen??
                // vorher bereits ersetzt?
                // Evtl. vor Ort ersetzen?
                if (item.PLZ != null && item.PLZ.Length == 4
                    && !string.IsNullOrWhiteSpace(item.Ort)
                    && plzEntities.Any(p => p.Ort.Equals(item.Ort, StringComparison.OrdinalIgnoreCase)))
                {
                    item.PLZ = "0" + item.PLZ;
                    item.LKZ = LKZ_DE;
                }
                // wäre die saubere Variante
                return item;
            }
    
            internal static void Test()
            {
                List<PLZReadonlyEntity> plzEntities = new List<PLZReadonlyEntity>() 
                {
                    new PLZReadonlyEntity() { LKZ = "d", PLZ = "4711", Ort = "Ort" },
                    new PLZReadonlyEntity() { LKZ = "DE", PLZ = "04712", Ort = "Ort 2" }
                };
    
                List<CSVContactEntity> entities = new List<CSVContactEntity>() 
                {
                    new CSVContactEntity() { LKZ = "", PLZ = "4711", Ort = "Ort" },
                    new CSVContactEntity() { LKZ = "d", PLZ = "4711", Ort = "Ort" },
                    new CSVContactEntity() { LKZ = "d", PLZ = "04712", Ort = "" },
                };
    
                //Instance.FillPLZOrtByLKZ(entities, plzEntities);
                Instance.FillPLZOrtByLKZ2(entities, plzEntities);
                foreach (var entity in entities)
                    Console.WriteLine("{0} {1} {2}", entity.LKZ, entity.PLZ, entity.Ort);
            }
        }
    
    


    Darin sind einige "?" übriggeblieben, da die Ersetzungen nicht immer eindeutig sind.
    So überlappen einige Ersetzungen - was man für eine Importroutine in Kauf nehmen mag.
    Angedeutet ist eine Verwendung als eigenständiger Konverter, aber nicht zu Ende geführt.

    Zur den geänderten Zeichenvergleichen siehe: New Recommendations for Using Strings in Microsoft .NET 2.0
    wobei man dies (theoretisch) auch Konfigurierbar machen könnte, wenn man für die Eingabe
    zuverlässige Voraussagen machen kann - wenn nicht ist Ordinal(IgnoreCase) sinnvoller.

    Gruß Elmar

     

    Freitag, 9. September 2011 18:23
    Beantworter

Alle Antworten

  • Hallo Holger,

    wie verwendest Du die Methode in der Praxis?

    Denn würde man z. B. eine List<CSVContactViewModel> verwenden, so käme das Ergebnis schon an.
    Vorausgesetzt die Eingabedaten sind passend - so die eine oder andere Stelle könnte anders formuliert werden.

    Für heute früh ist das mir aber spät ;-) und zuerst wäre der Input zu klären.

    Gruß Elmar

     

    Donnerstag, 8. September 2011 00:07
    Beantworter
  • Guten Morgen Elmar,

    vielen Dank für deine Antwort.

    wth? 2:05?? Hardcorecoder! ;-)

    Ich versuche es genauer zu erläutern:

    Ich lese eine CSV Datei ein. Die Methode die die CSV Datensätze einliest/parst liefert ein IEnumerable<string[]>.  Diese CSV Daten werden zuerst in eine Entität geschrieben, und die Entität anschliessend an den Konstruktor der ViewModels übergeben. Diese ViewModels werden zuerst wo möglich ergänzt (die Methode die ich gepostet habe) und danach über eine Kette von Filtern gefiltert.

    Ich dachte gestern zuerst, dass Interfacetypen by value übergeben werden. Nachdem ich nun sowohl mit "ref IEnumerable<CSVContactViewModel>" als auch mit einer Liste "List<CSVContactViewModel>" als Parameter ebenso gescheitert bin, habe ich dies aber als mögliche Fehlerursache ausgeschlossen...

    Genauso weit bin ich momentan noch...ABER, ich arbeite dran. ;-)

    Vielen Dank und viele Grüße
    Holger M. Rößler


    Kaum macht man es richtig, schon funktioniert es
    Donnerstag, 8. September 2011 06:37
  • Hallo Holger,

    naja, die Nacht (und gestern) war nicht so doll, und das Programmieren lenkt ab - siehe auch unten ;-)

    Zunächst aber zum eigentlichen Problem:
    Zwar gilt grundsätzlich, dass man bei einem IEnumerable nicht voraussetzen sollte,
    dass eine identische Instanz zurückgeliefert wird. Aber in den Standard-Fällen kann
    man bei Referenz-Typen davon ausgehen, dass die gleiche Instanz geliefert wird.
    Zumindest solange man nicht selbst tätig wird, also irgendwo new <T>() { ... } verwendet.
    Bei Strukturen sieht das anders aus, aber die lasse ich mal beiseite.

    Interfaces verhalten sich wie eine Klasse und werden als Referenz übergeben.
    Detaillierter siehe Jon Skeet und Eric Lippert in:
    http://stackoverflow.com/questions/3236305/do-interfaces-derive-from-system-object-c-spec-says-yes-eric-says-no-reality

    Was hier aber keine Rolle spielt und so wäre ein ref in keinem Falle nützlich.
    Denn IEnumerable<T> stellt die Methoden bereit, um einen Iterator
    für einen Typ (IEnumerator<T>) via GetEnumerator abzurufen.

    Da die gezeigte Methode nur ein Konsument ist, bringt ein ref nichts.
    ref wäre nur notwendig, wenn Du eine Methode hättest, 
    die unterschiedliche IEnumerable<T> an einen Aufrufer zurückliefern soll.

    Wie bereits geschrieben:
    Der Codeabschnitt an sich stellt kein Problem dar, solange die zugrunde liegenden Klassen
    nicht ein Eigenleben entwickeln - wobei mir nichts gängiges einfällt (meine Vorstellung von Entities zugrunde gelegt).
    Auch wenn es nicht im Sinne des funktionalen Pradigmas (und LINQ) ist, eine Instanz zu verändern.

    Nicht umsonst ist nicht zulässig:

        foreach (var item in entities)
        {
            // kompiliert nicht!
            item = CSVContactEntityConverter(item, plzEntities);
        }
    
    

    Technisch weil ein IEnumerator.Current readonly ist - vom Konzept her,
    weil man keine zusätzliche Voraussetzung über Herkunft und Lebensdauer machen sollte.

    Sauberer wäre, die Konvertierungsroutinen eine Instanz zurückliefern zu lassen,
    ohne dabei darauf zu setzen, dass sie identisch (Object.ReferenceEquals) bleiben.

    Zum Schluß einen Teil vom Rest der Nacht:

        // Vereinfachte Hilfsklassen
        public class PLZReadonlyEntity
        {
            public string LKZ { get; set; }
            public string PLZ { get; set; }
            public string Ort { get; set; }
        }
    
        public class CSVContactEntity : PLZReadonlyEntity
        {
        }
    
        public class PLZOrtFiller
        {
            public static readonly PLZOrtFiller Instance = new PLZOrtFiller();
    
            private PLZOrtFiller() { }
    
            private static string[] LKZ_DE_Ersetzen = { "d", "deu", "D-" };  // Ersetzen durch DE
            private static string LKZ_DE = "DE";
    
            public void FillPLZOrtByLKZ2(IEnumerable<CSVContactEntity> entities, IEnumerable<PLZReadonlyEntity> plzEntities)
            {
                if (entities != null & plzEntities != null)
                {
                    foreach (var item in entities)
                    {
                        CSVContactEntityConverter(item, plzEntities);
                    }
                }
            }
    
            public static CSVContactEntity CSVContactEntityConverter(CSVContactEntity item, IEnumerable<PLZReadonlyEntity> plzEntities)
            {
                // Evtl. nach Ortvergleich, da beide DE
                if (LKZ_DE_Ersetzen.Any(lkz => lkz.Equals(item.LKZ, StringComparison.OrdinalIgnoreCase)))
                {
                    item.LKZ = LKZ_DE;
                }
    
                // Keine Null-Orte / auch keine Null PLZ???
                if (string.IsNullOrWhiteSpace(item.LKZ)
                    && !string.IsNullOrWhiteSpace(item.Ort))
                {
                    var plz = plzEntities.FirstOrDefault(p =>
                        p.PLZ.Equals(item.PLZ, StringComparison.OrdinalIgnoreCase)
                        && p.Ort.Equals(item.Ort, StringComparison.OrdinalIgnoreCase));
                    if (plz != null)
                    {
                        item.LKZ = plz.LKZ ?? LKZ_DE; // LKZ wenn vorhanden?
                        item.Ort = plz.Ort;
                    }
                }
    
                if (LKZ_DE.Equals(item.LKZ, StringComparison.OrdinalIgnoreCase)
                    && string.IsNullOrWhiteSpace(item.Ort))
                {
                    var plz = plzEntities.FirstOrDefault(p => p.PLZ.Equals(item.PLZ, StringComparison.OrdinalIgnoreCase));
                    if (plz != null)
                    {
                        item.Ort = plz.Ort;
                    }
                }
    
                // Keine Nullen??
                // vorher bereits ersetzt?
                // Evtl. vor Ort ersetzen?
                if (item.PLZ != null && item.PLZ.Length == 4
                    && !string.IsNullOrWhiteSpace(item.Ort)
                    && plzEntities.Any(p => p.Ort.Equals(item.Ort, StringComparison.OrdinalIgnoreCase)))
                {
                    item.PLZ = "0" + item.PLZ;
                    item.LKZ = LKZ_DE;
                }
                // wäre die saubere Variante
                return item;
            }
    
            internal static void Test()
            {
                List<PLZReadonlyEntity> plzEntities = new List<PLZReadonlyEntity>() 
                {
                    new PLZReadonlyEntity() { LKZ = "d", PLZ = "4711", Ort = "Ort" },
                    new PLZReadonlyEntity() { LKZ = "DE", PLZ = "04712", Ort = "Ort 2" }
                };
    
                List<CSVContactEntity> entities = new List<CSVContactEntity>() 
                {
                    new CSVContactEntity() { LKZ = "", PLZ = "4711", Ort = "Ort" },
                    new CSVContactEntity() { LKZ = "d", PLZ = "4711", Ort = "Ort" },
                    new CSVContactEntity() { LKZ = "d", PLZ = "04712", Ort = "" },
                };
    
                //Instance.FillPLZOrtByLKZ(entities, plzEntities);
                Instance.FillPLZOrtByLKZ2(entities, plzEntities);
                foreach (var entity in entities)
                    Console.WriteLine("{0} {1} {2}", entity.LKZ, entity.PLZ, entity.Ort);
            }
        }
    
    


    Darin sind einige "?" übriggeblieben, da die Ersetzungen nicht immer eindeutig sind.
    So überlappen einige Ersetzungen - was man für eine Importroutine in Kauf nehmen mag.
    Angedeutet ist eine Verwendung als eigenständiger Konverter, aber nicht zu Ende geführt.

    Zur den geänderten Zeichenvergleichen siehe: New Recommendations for Using Strings in Microsoft .NET 2.0
    wobei man dies (theoretisch) auch Konfigurierbar machen könnte, wenn man für die Eingabe
    zuverlässige Voraussagen machen kann - wenn nicht ist Ordinal(IgnoreCase) sinnvoller.

    Gruß Elmar

     

    Freitag, 9. September 2011 18:23
    Beantworter
  • Hallo Elmar,

    mal wieder vielen Dank für deine ausführlichen Erklärungen. Ich habe einige deiner Anregungen und Verbesserungsvorschläge aufgenommen. Vielen Dank dafür :-)

    Viele Grüße
    Holger M. Rößler


    Kaum macht man es richtig, schon funktioniert es
    Samstag, 10. September 2011 21:08
  • Hallo Holger,

    bitte schön, da war der Nachtzeitvertreib nicht ganz umsonst ;-)

    Und das eigentliche Problem, hast Du es gelöst oder es zumindest eingekreist?

    Gruß Elmar

    Samstag, 10. September 2011 21:33
    Beantworter
  • Hallo Elmar,

    leider nein. Es ist für mich unerklärlich. Folgender Code funktioniert nämlich 1a:

     

    using System;
    using System.Linq;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    
    namespace ConsoleApplication {
       public class Program {
          public class Person {
             public Person() {
                this.Vorname = string.Empty;
                this.Nachname = string.Empty;
             }
    
             public string Vorname {
                get;
                set;
             }
    
             public string Nachname {
                get;
                set;
             }
    
             public override string ToString() {
                return '{' + this.Vorname  + ", " + this.Nachname + '}';
             }
          }
    
          public static void Main(string[] args) {
             IEnumerable<Person> text = new List<Person>() { new Person(), new Person(), new Person(), new Person() };
             Program.AddDummyNamesToPerson(text);
    
             foreach(Person item in text) {
                Console.WriteLine(item);
             }
          }
    
          public static void AddDummyNamesToPerson(IEnumerable<Person> personCollection) {
             foreach(Person item in personCollection) {
                item.Vorname = "Vorname";
                item.Nachname = "Nachname";
             }
          }
       }
    }
    
    

     


    Hier werden die Änderungen ohne Probleme übernommen und auf der Konsole ausgegeben. Von daher ist mir schleierhaft, warum dies in meiner Filler-Methode nicht funktioniert!

    @Edit
    Achja, "gelöst" habe ich das Problem nun so, dass ich die ViewModels in eine separate Liste kopiere und diese als return value von der Methode zurückgeben lasse.

    Viele Grüße
    Holger M. Rößler


    Kaum macht man es richtig, schon funktioniert es
    Samstag, 10. September 2011 22:32