Verständnisproblem Funktionsaufruf call by value/referenz



  • Hallo,

    ich habe momentan ein Verständnisproblem wie das in C# jetzt genau abläuft mit dem Übergeben von Klassen-Instanzen an Funktionen. Und zwar wann eine Funktion jetzt genau mit dem original Objekt arbeitet (und dieses damit verändert) und wann nicht.

    Also zum Beispiel folgender Code-Ausschnitt aus einer normalen Windows-Form Anwendung:

    public partial class Form1 : Form
    {
        struct Test
        {
            public int value;
        }
    
        void Foo(Test test)
        {
            test.value++;
        }
    
        void Bar(Form1 form1)
        {
            form1.richTextBox1.Text = "hello";
        }
    
        public Form1()
        {
            InitializeComponent();
    
            Test test = new Test();
            test.value = 0;
            Foo(test);
            MessageBox.Show(test.value.ToString()); // Output: 0
    
            Bar(this);
            MessageBox.Show(this.richTextBox1.Text); // Output: "hello"
        }
    }
    

    Also meiner Meinung nach habe ich hier vom Prinzip her zwei gleiche Funktionen Foo und Bar: Beide nehmen ein Objekt entgegen und modifizieren dieses. In beiden Funktionen ist der Parameter weder als ref noch als out markiert (damit müsste das ja einem Call-by-Value entsprechen).

    Trotzdem ist das Verhalten anders als ich es jetzt erwarten würde: Nach dem Aufruf von Foo ist mein übergebenes Objekt (wie Erwartet) unverändert, nach dem Aufruf von Bar allerdings gibt es eine dauerhafte Veränderung. Nämlich bleibt der Text meiner Rich-Textbox gesetzt.

    Welche Regeln liegen diesem Verhalten zugrunde? Meiner Meinung nach macht das keinen Sinn, bzw. wofür gibt es denn dann das ref Schlüsselwort wenn "normal" übergebene Parameter sich auch ohne selbiges wie Referenzen verhalten?



  • Hallo,

    das unterschiedliche Verhalten liegt daran, dass du einmal eine Instanz vom Typ struct und einmal eine vom Typ class übergibst. Structs werden in C# immer als Kopie übergeben. Bei Klasseninstanzen wird der Zeiger auf die Instanz als Kopie übergeben, d.h. eine neue Zuweisung der Variable innerhalb der Funktion ruft keine Veränderung hervor. Änderst du aber z.B. eine Property wie in deinem Beispiel, dann ist diese Änderung dauerhaft, weil auf der gleichen Instanz gearbeitet wird wie beim Aufrufer der Funktion. Fazit: Benutze Struct in C# nur wenn du weißt was du tust, ansonsten ist immer class vorzuziehen.

    mfG
    KaPtainCugel



  • Hallo,

    danke schonmal für die Erklärung, mir war nicht bewusst dass sich class und struct hier so unterschiedlich verhalten... Im Umkehrschluss heißt das ja dann aber das Instanzen einer class immer per Referenz übergeben werden? Wie kann ich denn dann ein class objekt per Value übergeben (also so dass Zustandsveränderungen nach dem beenden der auferufenen Methode nicht erhalten bleiben)?

    Weil const-Parameter gibts ja auch nicht, aber irgendwie muss man doch syntaktisch ausdrücken können dass eine bestimmte Methode ein ihr übergebenes objekt vom typ class nicht verändert?



  • Hi,

    du kannst eine Instanz einer Klasse nicht by value übergeben. Du kannst lediglich eine Kopie erzeugen und diese an eine Methode übergeben, aber das ist in den meisten Fällen nicht sinnvoll. In C# würde man andere Mittel verwenden, um ein Verhalten wie in C++ zu bekommen. Du kannst z.B. den Parametertyp der Methode als Interface definieren welches die Klasse implementiert und bei diesem Interface genau festlegen, welche Operationen ausgeführt werden dürfen.

    mfG
    KaPtainCugel



  • happystudent schrieb:

    Wie kann ich denn dann ein class objekt per Value übergeben

    Leider gar nicht.

    happystudent schrieb:

    Weil const-Parameter gibts ja auch nicht, aber irgendwie muss man doch syntaktisch ausdrücken können dass eine bestimmte Methode ein ihr übergebenes objekt vom typ class nicht verändert?

    Leider geht auch das nicht, so richtig gar nicht.
    Du kannst, wie KPC schon geschrieben hat, ein Interface übergeben.
    Also sagen wir du hast ein Objekt Foo, dann kannst du ein IFooReadOnly machen wo man nur lesen kann und ein IFoo welches von IFooReadOnly erbt und zusätzlich Änderungen erlaubt.

    Bloss dummerweise kann eine Funktion die nen IFooReadOnly Parameter hat jederzeit einen Cast auf IFoo versuchen, und wenn der geklappt hat einfach die IFoo Funktionen verwenden um das Objekt zu modifizieren.

    Das ist auch einer meiner grössten Kritikpunkte am ganzen .NET Gedöns, Java und anderen Sprachen wo "const" fehlt. In der Praxis kommt man überraschend gut ohne aus. Nur gefallen muss es mir deswegen auch nicht 😉



  • Ok, das ist eine herbe Enttäuschung 😃

    Na gut, dann werd ich mich wohl damit abfinden müssen... Fand bis jetzt ja C# durchgehend schöner als C++, aber das gefällt mir jetzt auch nicht. Vor allem da es ja extra so schöne Schlüsselwörter wie ref oder out gibt, sehr verwirrend dass Klassen-Objekte dann auch ohne diese per Referenz übergeben werden...



  • ref und out beziehen sich bei Reference-Types dann auf die Referenz.

    Also Beispiel

    SomeRefType foo;
    bar.SomeFunction(out foo);
    foo.DoFoo(); // OK, da per out übergeben und damit jetzt initialisiert
    

    Das ist soweit schon OK. Nur halt doof dass const eingespart wurde.
    Hätte aber anscheinend das Typensystem zu kompliziert gemacht. Zumindest meine ich mich zu erinnern mal sowas gelesen zu haben.



  • hustbaer schrieb:

    Das ist soweit schon OK. Nur halt doof dass const eingespart wurde.

    Ja, ich finde das sogar sehr schön mit dem ref und out. Wenns jetzt noch const gäbe wärs eigentlich perfekt.

    hustbaer schrieb:

    Hätte aber anscheinend das Typensystem zu kompliziert gemacht. Zumindest meine ich mich zu erinnern mal sowas gelesen zu haben.

    Also Eric Lippert hält const für broken (in C/C++). Kann seiner Argumentation aber nicht so folgen, finde das ziemlich unlogisch was der da erzählt.



  • Ich halte ja grundsätzlich eher mehr als weniger von Eric Lippert. Aber das Statement das er da abgeliefert hat ist einfach nur lächerlich.

    Ja, const gibt keine Garantien.
    private und protected aber auch nicht, schon gar nicht mit der CLR.
    Hätte man die also auch streichen müssen.

    Im Endeffekt sind aber alle drei sehr praktisch um "ich schiesse mir unabsichtlich in den Fuss" zu verhindern. Um einfach ruhiger schlafen zu können.

    Und alleine dass er überhaupt anführt dass const für den "Consumer" keine Garantie ist dass sich das Objekt nicht unter seinem Hintern weg ändert, lässt mich leise vermuten dass er const überhaupt nicht verstanden hat.
    const heisst in C++ schliesslich "du darfst nicht ändern" und nicht "das wird sich nicht ändern".

    Und dann beantwortet er die Frage warum C# kein const hat damit dass sie es der CLR nicht aufs Auge drücken wollten -- behauptet dann aber gleichzeitig dass diese Entscheidung es leichter machen soll diverse Sprachen "auf die CLR zu bringen". Ähm. Ja. Klar. *facepalm*



  • hustbaer schrieb:

    Ich halte ja grundsätzlich eher mehr als weniger von Eric Lippert. Aber das Statement das er da abgeliefert hat ist einfach nur lächerlich.

    Dachte zuerst "wenn der Typ das sagt muss es ja stimmen". Aber ich glaub auch dass er es sich da ein bisschen zu leicht gemacht hat und sie in Wirklichkeit einfach keinen Bock auf const hatten 😃



  • Das ist Gewohnheitssache. Ich hab const nicht wirklich vermisst, als ich mehr C# programmiert habe. Und in C++ musste ich mich erst dran gewöhnen, fand es eine Zeit lang viel zu umständlich (muss man ja überall durchziehen, z.B. müssen die Methoden, die man aufrufen darf, auch const sein) ohne dass ich großartige Vorteile gesehen habe. Hab mich natürlich dran gewöhnt und würde in C++ darauf nicht verzichten wollen. Aber so wirklich vermissen tu ich das in C# auch nicht.



  • hustbaer schrieb:

    Bloss dummerweise kann eine Funktion die nen IFooReadOnly Parameter hat jederzeit einen Cast auf IFoo versuchen, und wenn der geklappt hat einfach die IFoo Funktionen verwenden um das Objekt zu modifizieren.

    Ähmja, das ist doch auch nicht anders in C++, wo du über const_cast<>() nach Belieben verändern kannst, wo du nicht darfst.

    hustbaer schrieb:

    Ich halte ja grundsätzlich eher mehr als weniger von Eric Lippert. Aber das Statement das er da abgeliefert hat ist einfach nur lächerlich.

    Finde ich nicht. Ich denke auch, daß er zu konservativ argumentiert, aber so wird man halt, wenn man jahrelang tolle Features in Compiler einbaut und dabei vermutlich in ein SNAFU nach dem anderen läuft, weil es halt doch nicht so einfach ist, wie sich alle Außenstehenden denken.

    Mit private und protected hast du recht, weil es keine starke Garantie ist, solange man mit Reflection trotzdem drankommt. Trotzdem sind private und protected deutlich wichtiger gewesen:

    - das wird von jedem Programmierer, der mit irgendeiner objektorientierten Sprache arbeitet, erwartet
    - über protected wird ja gestritten, aber zumindest private hat sich unbestreitbar als nützlich erwiesen
    - Java kann es, deshalb muß es in die CLR (aus demselben Grund wurde auch die kaputte Array-Kovarianz übernommen)

    hustbaer schrieb:

    Und dann beantwortet er die Frage warum C# kein const hat damit dass sie es der CLR nicht aufs Auge drücken wollten -- behauptet dann aber gleichzeitig dass diese Entscheidung es leichter machen soll diverse Sprachen "auf die CLR zu bringen". Ähm. Ja. Klar. *facepalm*

    Das ist schon so. Keine mir bekannte Sprache außer C und C++ hat sowas wie const . Wenn man const tatsächlich ins Typsystem integriert und in der BCL davon regen Gebrauch macht, müssen andere Sprachen lernen, damit umzugehen. Selbst wenn es nur eine folgenlose (d.h. von der CLR nicht erzwungene) Annotation sein soll, müssen andere Sprachen z.B. mit const /non- const -Überladungen klarkommen.

    Wie dem auch sei: ich glaube, der eigentliche Grund dafür, daß man im .NET-Umfeld sich nicht über const den Kopf zerbricht, ist, daß der Trend zu immutability geht, also " const für alle" mit den bekannten Vorteilen (konkurrenter Zugriff immer unproblematisch und so). Deshalb hatte Herr Lippert vermutlich auch die Assoziation mit den "guarantee[s] that you can actually rely upon", bei unveränderlichen Datenstrukturen hat man die nämlich.

    Mir geht es wie Mechanics; in C++ benutze ich gerne const und ärgere mich nur, wenn ich non- const -Overloads machen muß, und in C# stellt sich die Frage irgendwie gar nicht.



  • audacia schrieb:

    Ähmja, das ist doch auch nicht anders in C++, wo du über const_cast<>() nach Belieben verändern kannst, wo du nicht darfst.

    Kannst du doch gar nicht? Bzw. kannst du schon aber hast dann halt UB und dein Programm schmiert ab...



  • audacia schrieb:

    hustbaer schrieb:

    Bloss dummerweise kann eine Funktion die nen IFooReadOnly Parameter hat jederzeit einen Cast auf IFoo versuchen, und wenn der geklappt hat einfach die IFoo Funktionen verwenden um das Objekt zu modifizieren.

    Ähmja, das ist doch auch nicht anders in C++, wo du über const_cast<>() nach Belieben verändern kannst, wo du nicht darfst.

    Oha.
    Der riesen Unterschied ist dass const_cast sowie C-Style-Casts bzw. Function-Style-Casts in C++ sehr verpönt sind, und man sofort "hoppla!" denkt wenn man es sieht. Wohingegen Casts von einem Interface zum anderen in C# überhaupt nicht verpönt sind.

    audacia schrieb:

    Trotzdem sind private und protected deutlich wichtiger gewesen: (...)

    Ich bestreite ja gar nicht dass private und protected wichtiger sind. Ich meine nur: das Argument vonwegen "not enforcable" kann er sich einrexen. Bzw. es ist für mich weder hinreichend noch sonst irgendwie interessant wenn ich darüber nachdenke ob const umgesetzt werden soll oder nicht.
    Und vor allem: es spricht ja durchaus nichts dagegen es besser zu machen. Hat ja keiner verlangt dass const gleich "kaputt" übernommen wird wie es in C++ umgesetzt wurde.
    Was aber, neben anderem Unsinn, von Lippert unterstellt wird.

    Kurz: ich bin mit seiner Antwort nicht einverstanden.

    Ich bin durchaus der Meinung dass man sinnvoll gegen const argumentieren kann. Nur so sieht so ein Argument eben nicht aus. Und wenn er nix sinnvolles zu sagen hat, warum meldet er sich dann überhaupt?
    Das ist inetwa so sinnvoll wie die Deppen die auf Amazon auf eine Frage zu einem Produkt antworten "weiss ich leider nicht, sorry".
    (Ja, ich weiss wie diese Antworten auf Amazon zustande kommen, bloss ein bisschen Hirn einschalten wäre echt nicht zu viel verlangt.)



  • happystudent schrieb:

    audacia schrieb:

    Ähmja, das ist doch auch nicht anders in C++, wo du über const_cast<>() nach Belieben verändern kannst, wo du nicht darfst.

    Kannst du doch gar nicht? Bzw. kannst du schon aber hast dann halt UB und dein Programm schmiert ab...

    Klar kannst du.
    UB ist es nur wenn das Objekt selbst const ist.
    Wenn dagegen einfach nur ein T const* , auf ein T was selbst gar nicht const ist, übergeben wurde, dann kann man problemlos das const wegcasten und in dem T drinnen rummalen.



  • happystudent schrieb:

    aber hast dann halt UB und dein Programm schmiert ab...

    Aus UB folgt nicht, daß dein Programm abstürzt. Ein aggressiv optimierender Compiler mag zwar UB zum Anlaß für relativ absurde Optimierungen nehmen (indem er, grob gesagt, annimmt, daß der UB-Fall nie eintritt), aber es ist doch recht wahrscheinlich, daß genau das passiert, was man naiverweise erwarten würde.

    hustbaer schrieb:

    Der riesen Unterschied ist dass const_cast sowie C-Style-Casts bzw. Function-Style-Casts in C++ sehr verpönt sind, und man sofort "hoppla!" denkt wenn man es sieht. Wohingegen Casts von einem Interface zum anderen in C# überhaupt nicht verpönt sind.

    Mit dem Argument habe ich so halb gerechnet 🙂 Allerdings sehe ich das anders. Es gibt ein paar Stellen, wo Casts in C# nötig und legitim sind:

    - im Zusammenhang mit type erasure ( Control.Tag )
    - in den Innereien von irgendwelchem Bibliothekscode, der weiß, was er da tut (d.h. Casts auf internen Typen)
    - in Urzeitrelikten aus .NET 1.x, wo es noch keine Generics gab (das sind auch meistens die Stellen, wo man type erasure verwenden muß)
    - im params Foo[] items -Overload einer IEnumerable<Foo> items -Methode ist ein Cast nach IEnumerable<Foo> erlaubt

    Abgesehen von dieser wahrscheinlich nicht vollständigen Liste beäuge ich jeden einzelnen Cast mißtrauisch, genau wie ich einen const_cast<>() , dynamic_cast<>() oder reinterpret_cast<>() in C++ beäuge, wenn sie nicht auf den jeweiligen Ausnahmelisten stehen.

    hustbaer schrieb:

    Ich meine nur: das Argument vonwegen "not enforcable" kann er sich einrexen. Bzw. es ist für mich weder hinreichend noch sonst irgendwie interessant wenn ich darüber nachdenke ob const umgesetzt werden soll oder nicht.

    Dasselbe Argument spielt auch eine Rolle in der Diskussion auf Codeplex, ob man non-nullable references einführen soll (mit einem ! -Suffix analog zu ? für nullbare Werttypen). Hast du die verfolgt? Meiner Erinnerung nach sagten sie "wir können das [ohne eine tiefgreifende Änderung in der CLR] nicht erzwingen, deshalb sollte man das nicht in die Sprache einführen, sondern über Annotationen und Analysetools lösen".

    hustbaer schrieb:

    Und vor allem: es spricht ja durchaus nichts dagegen es besser zu machen. Hat ja keiner verlangt dass const gleich "kaputt" übernommen wird wie es in C++ umgesetzt wurde.
    Was aber, neben anderem Unsinn, von Lippert unterstellt wird.

    Gut, das stimmt. Ich glaube, eine nicht-kaputte (also von der CLR forcierbare) Lösung wird allerdings ziemlich teuer, und der Nutzen zu diesem Zeitpunkt bleibt vernachlässigbar, weil die BCL das Konzept nicht verwendet. Ähnliches Problem für nicht-nullbare Referenzen.



  • hustbaer schrieb:

    Klar kannst du.
    UB ist es nur wenn das Objekt selbst const ist.
    Wenn dagegen einfach nur ein T const* , auf ein T was selbst gar nicht const ist, übergeben wurde, dann kann man problemlos das const wegcasten und in dem T drinnen rummalen.

    Ups, stimmt da hab ich mich zu weit aus dem Fenster gelehnt.^^

    Aber man könnte es ja trotzdem anbieten, nur halt den (ohnehin bösen) const_cast weglassen.


Anmelden zum Antworten