Laden...

C erbt von B erbt von A: Aufruf von base.M in C.M ruft A.M auf, auch wenn B.M hinzugefügt wird

Erstellt von inflames2k vor 11 Jahren Letzter Beitrag vor 11 Jahren 2.042 Views
Hinweis von herbivore vor 11 Jahren

Zum Verständnis des Threads ist es wichtig ist zu wissen, dass die Assembly (nennen wir sie X), die SubChild enthält, nicht neu kompiliert wurde, sondern nur die Assembly (nennen wir sie Y), die Child enthält, neu kompiliert wurde. Der Fehler tritt (nur) auf, wenn das alte X das neue Y benutzt. Wenn man auch X neu kompiliert, verschwindet das Problem.

inflames2k Themenstarter:in
2.298 Beiträge seit 2010
vor 11 Jahren
C erbt von B erbt von A: Aufruf von base.M in C.M ruft A.M auf, auch wenn B.M hinzugefügt wird

Hallo,

einem Kollegen fiel heut auf, das es beim überschreiben virtueller Methoden ein Problem gibt. Zuerst einmal die Schilderung des Sachverhalts:

Es existiert eine Basisklasse, welche virtuelle Methoden enthält. Für unser Beispiel z.B. folgende:


public class Base
{
      public virtual void Write()
      {
           Console.WriteLine("Base");
      }
}

Die Basisklasse liegt in einer eigenen Assembly. Hinzu kommt eine Klasse, die von Base erbt. Diese implementiert die Methode nicht und liegt in einer eigenen Assembly.


public class Child : Base
{
    
}

Desweiteren gibt es eine Klasse, die von Child erbt und die Methode implementiert und wiederum in einer eigenen Assembly liegt.


public class SubChild : Child
{
    public override Write()
    {
         Console.WriteLine("Child");
         base.Write();
    }
}

Wird jetzt nur die Klasse Child geändert und die Methode dort doch überschrieben, passiert es jedoch, das in SubChild nicht die Methode der Klasse Child, sondern direkt die der Klasse Base aufgerufen wird. Nach Analyse erkannten wir, das es am generierten IL-Code liegt, aber warum ist das so?

Wissen ist nicht alles. Man muss es auch anwenden können.

PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |

771 Beiträge seit 2009
vor 11 Jahren

Hi,

die Klassenvererbung wird ja zur Compilezeit aufgelöst - und nicht dynamisch zur Laufzeit. Konkret wird bei der Kompilierung von base.Write() die Basisklasse verwendet, welche die Write-Methode auch implementiert hat (und in deinem Beispiel eben nur Base und nicht Child). Sonst müßte der Compiler ja für alle nicht-implementierten virtuellen Methoden immer eine leere Dummy-Methode erzeugen.

Daher mußt du dann auch die SubChild-Assembly neu kompilieren.

6.911 Beiträge seit 2009
vor 11 Jahren

Hallo inflames2k,

das Verhalten rührt daher, dass eben nicht callvirt-Aufrufe erstellt werden, sondern durch base werden call-Aufrufe erstellt, da somit ein StackOverflow vermieden werden kann (der durch Rekursionen entstehen würde).

Mit callvirt wäre die Übersetzung wie folgt:


public class SubChild : Child
{
    public override Write()
    {
         Console.WriteLine("Child");
         this.Write();    // <- klar dass das nicht geht         
    }
}

und daher wird eben eine "direkter" Methodenaufruf mittels call verwendet.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

inflames2k Themenstarter:in
2.298 Beiträge seit 2010
vor 11 Jahren

Ok, soweit verstehe ich das.

Was ich nur gerade nicht verstehe ist, warum bei callvirt ein "this.Write()" entstehen sollte, wenn doch "base.Write()" aufgerufen wurde. Schließlich rufe ich hier ja explizit die Write-Methode der Basisklasse auf.

Ich glaub ... das ist gerade etwas zu hoch für mich. - Ich hab zwar auf englisch einiges gefunden, aber nichts was es mir klarer macht, sondern immer nur die Aussage das es zu this.Write wird und somit ein rekursiver Aufruf entstehen würde.

Wissen ist nicht alles. Man muss es auch anwenden können.

PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |

6.911 Beiträge seit 2009
vor 11 Jahren

Hallo inflames2k,

ein Methodenaufruf wie this.Write() wird zu einem callvirt übersetzt um polymorphes Verhalten zu ermöglichen. Zur Laufzeit wird dann geschaut welcher Typ es genau ist und dessen Methode aufgerufen.

Ein Methodenaufruf wie base.Write() wird zu einem call übersetzt und das ist ein fixer Sprung zu der Methode der Basisklasse. Du schreibst ja selber, dass "explizit die Methode der Basisklasse" aufgerufen wird.
Würde hier auch erst zur Laufzeit die Objekthierarchie durchwandert werden, um - wie im Beispiel oben - zu bemerken, dass in Child doch die Methode überschrieben worden ist, so müsste nach IL-Spezifikation ein callvirt dastehen. Ein callvirt ist aber eben gleichbedeutend mit this, so dass der Methodenaufruf ein this.Write werden würde und da gibt eine Endlosrekursion bis der Stack nicht mehr will. Deshalb wird ein call erzeugt und wegen der fixen Sprungadresse wird die Änderung in Child nicht bemerk.t

Edit: ich wusste ja, dass sowas schon mal besprochen wurde - hier hast du es: Muss man in abgeleiteten Methoden oder Eigenschaften base benutzen?

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

2.187 Beiträge seit 2005
vor 11 Jahren

Hallo gfoidl,

Also auf gut Deutsch: Es ist in IL einfach nicht vorgesehen virtuell in die Vererbungshirarchie zu rufen. Es müsste ein spezieller IL-Befehl genau für diesen aufruf existieren. Und das lässt sich nicht einfach so einfügen.

Gruß
Juy Juka

6.911 Beiträge seit 2009
vor 11 Jahren

Hallo JuyJuka,

virtuell nach oben gehts nicht. Nach unten gehts mit callvirt.

Theoretisch wäre so ein Befehl schon möglich: schauen was der direkte Vorfahr ist, schauen ob der diese Methode besitzt, ggf. in der Hierarchie weiter nach oben gehen, ...

Aber das wäre womöglich eine Breaking-Chance, da ja das bisherige Verhalten der CLR geändert wird. So ein Befehl wurde vom CLR-Team schon öfters angedacht, aber aus diversen Gründen nie umgesetzt. Entweder müsste die gesamte CLR überarbeitet werden od. eben auf den Befehl verzichtet werden. Quelle: Putting a base in the middle

Stell dir nur vor in Child würde der Code so geändert werden:


public class Child : Base
{
    private new void Write() {...}
}

Dann kracht es zur Laufzeit.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

156 Beiträge seit 2010
vor 11 Jahren

Moin,

Daher mußt du dann auch die SubChild-Assembly neu kompilieren.

eigentlich soltle das die IDE selber machen - aber nur eigentlich.

Ein kleiner Querveweis an Java, welche das Problem mal von einer ähnlichen Seite beleuchtet -> Eincompilierte Belegungen der Klassenvariablen

6.911 Beiträge seit 2009
vor 11 Jahren

Hallo mogel,

die IDE macht das auch, wenn die ganze Solution od. ein abhängiges Projekt erstellt wird. Beim Beispiel von inflames2k wird die Assembly aber "isoliert" (Build Solution vs. Build Project) erstellt, dann kann die IDE die andere Projekte nicht neu erstellen.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

inflames2k Themenstarter:in
2.298 Beiträge seit 2010
vor 11 Jahren

Dann fahr ich also am besten, wenn ich in solchen Konstellationen in allen untergeordneten Klassen die Methode erbe. Ist dann wohl der einzige Weg sicher zu stellen, das ich Bibliotheken austauschen kann, ohne das halbe System austauschen zu müssen.

Wobei das für mich ein Punkt ist, der unter Umständen gar die Modularität einschränkt.

Mich wundert, das mir das bisher nicht passiert ist, so eine Konstellation zu entwickeln. So unwahrscheinlich ist der Fall ja doch nicht.

Mir fällt jetzt kein Konkretes Beispiel ein, aber an und für sich ist es ja auch nicht so unmöglich das eine Zwischenklasse die einen virtual Member nachträglich implementiert.

Nun gut, zumindest hab ich so wieder etwas gelernt.

@Mogel:
Nicht immer hat man alle Assemblies in einem Projekt. Einfachstes Beispiel wäre ein Applikationsframework, das in einem eigenen Projekt liegt. Du erstellst eigene Bibliotheken die darauf zugreifen, welche du wiederum in anderen Anwendungen verwendest. Somit würdest du schon einmal auf 3 unabhängige Projekte kommen. Gerade das ist doch das schöne an Modularität.

Wissen ist nicht alles. Man muss es auch anwenden können.

PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |

156 Beiträge seit 2010
vor 11 Jahren

Moin,

dass es recht komplexe Abhängigkeiten gibt (siehe [gelöst] VS10 - C# - Buildereignis vermisst: Reihenfolge des Compilierens mehrere Projekte), die VS nicht wissen kann ist mir klar. Wollte an der Stelle auch auf einen Spruch meines Profs hinaus: "Ab und zu muss man mal das Projekt aufräumen und alles nochmal erstellen".

Ansonsten kann man evt. eine Buildumgebung wie NAnt verwenden. Zur Not macht es auch eine Batch-Datei. Da kann man dann selber die Abhängigkeiten - auch von Dritten - explizit angeben.

hand, mogel